Commit 697ecce05e7 for nodejs

commit 697ecce05e7d72cf9278cae15278f51016d6e3b2
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Sun Aug 30 09:08:37 2026 +0200

    crypto: harden X.509 conversion and formatting

    Check X.509 ASN.1, BIO, time, OID, and DER conversion results.
    Preserve invalid-time diagnostics, reject incomplete BIO writes, format
    RSA exponents atomically, and avoid truncating long object identifiers.

    Preserve valid siblings in legacy name objects by omitting individual
    non-text AttributeValues. Continue treating structural failures and
    failed conversion of supported string types as conversion failures.
    Reject empty or malformed SAN and AIA extensions, and prevent TLS
    hostname checks from falling back to commonName after a malformed SAN.

    Guard missing public keys and non-NIST curve names. Scope fallible
    backend probes without consuming callback errors, parse checkIP()
    inputs to exact address bytes, and preserve unrelated backend errors.

    Assisted-by: Codex
    Co-authored-by: Matteo Collina <hello@matteocollina.com>
    Signed-off-by: Matteo Collina <hello@matteocollina.com>
    Signed-off-by: Filip Skokan <panva.ip@gmail.com>
    Refs: https://hackerone.com/reports/3910015
    Refs: https://hackerone.com/reports/3824331
    Refs: https://hackerone.com/reports/3944830
    Refs: https://hackerone.com/reports/3944847
    PR-URL: https://github.com/nodejs-private/node-private/pull/966
    Reviewed-By: James M Snell <jasnell@gmail.com>

diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc
index 1dbb6f43418..d35d601abc7 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -988,6 +988,19 @@ enum class AltNameOption {
   UTF8,
 };

+std::optional<std::string> ObjectToText(const ASN1_OBJECT* object,
+                                        bool no_name) {
+  if (object == nullptr) return std::nullopt;
+  int length = OBJ_obj2txt(nullptr, 0, object, no_name);
+  if (length <= 0 || length == INT_MAX) return std::nullopt;
+
+  std::string text(static_cast<size_t>(length) + 1, '\0');
+  int actual = OBJ_obj2txt(text.data(), length + 1, object, no_name);
+  if (actual != length) return std::nullopt;
+  text.resize(static_cast<size_t>(length));
+  return text;
+}
+
 bool IsSafeAltName(const char* name, size_t length, AltNameOption option) {
   for (size_t i = 0; i < length; i++) {
     char c = name[i];
@@ -1027,32 +1040,36 @@ bool IsSafeAltName(const char* name, size_t length, AltNameOption option) {
   return true;
 }

-void PrintAltName(const BIOPointer& out,
-                  const char* name,
-                  size_t length,
-                  AltNameOption option = AltNameOption::NONE,
-                  const char* safe_prefix = nullptr) {
+[[nodiscard]] bool PrintAltName(const BIOPointer& out,
+                                const char* name,
+                                size_t length,
+                                AltNameOption option = AltNameOption::NONE,
+                                const char* safe_prefix = nullptr) {
+  if (!out || (name == nullptr && length != 0)) return false;
   if (IsSafeAltName(name, length, option)) {
     // For backward-compatibility, append "safe" names without any
     // modifications.
     if (safe_prefix != nullptr) {
-      BIO_printf(out.get(), "%s:", safe_prefix);
+      if (!BIOPointer::PrintfAll(out, "%s:", safe_prefix)) return false;
+    }
+    if (length != 0 &&
+        !BIOPointer::WriteAll(out, std::string_view(name, length))) {
+      return false;
     }
-    BIO_write(out.get(), name, length);
   } else {
     // If a name is not "safe", we cannot embed it without special
     // encoding. This does not usually happen, but we don't want to hide
     // it from the user either. We use JSON compatible escaping here.
-    BIO_write(out.get(), "\"", 1);
+    if (!BIOPointer::WriteAll(out, "\"")) return false;
     if (safe_prefix != nullptr) {
-      BIO_printf(out.get(), "%s:", safe_prefix);
+      if (!BIOPointer::PrintfAll(out, "%s:", safe_prefix)) return false;
     }
     for (size_t j = 0; j < length; j++) {
       char c = static_cast<char>(name[j]);
       if (c == '\\') {
-        BIO_write(out.get(), "\\\\", 2);
+        if (!BIOPointer::WriteAll(out, "\\\\")) return false;
       } else if (c == '"') {
-        BIO_write(out.get(), "\\\"", 2);
+        if (!BIOPointer::WriteAll(out, "\\\"")) return false;
       } else if ((c >= ' ' && c != ',' && c <= '~') ||
                  (option == AltNameOption::UTF8 && (c & 0x80))) {
         // Note that the above condition explicitly excludes commas, which means
@@ -1061,48 +1078,62 @@ void PrintAltName(const BIOPointer& out,
         // it correctly either way. We only do this to account for third-party
         // code that might be splitting the string at commas (as Node.js itself
         // used to do).
-        BIO_write(out.get(), &c, 1);
+        if (!BIOPointer::WriteAll(out, std::string_view(&c, 1))) return false;
       } else {
         // Control character or non-ASCII character. We treat everything as
         // Latin-1, which corresponds to the first 255 Unicode code points.
         const char hex[] = "0123456789abcdef";
         char u[] = {'\\', 'u', '0', '0', hex[(c & 0xf0) >> 4], hex[c & 0x0f]};
-        BIO_write(out.get(), u, sizeof(u));
+        if (!BIOPointer::WriteAll(out, std::string_view(u, sizeof(u)))) {
+          return false;
+        }
       }
     }
-    BIO_write(out.get(), "\"", 1);
+    if (!BIOPointer::WriteAll(out, "\"")) return false;
   }
+  return true;
 }

 // This function emulates the behavior of i2v_GENERAL_NAME in a safer and less
 // ambiguous way. "othername:" entries use the GENERAL_NAME_print format.
 bool PrintGeneralName(const BIOPointer& out, const GENERAL_NAME* gen) {
+  if (!out || gen == nullptr) return false;
   if (gen->type == GEN_DNS) {
     ASN1_IA5STRING* name = gen->d.dNSName;
-    BIO_write(out.get(), "DNS:", 4);
+    if (name == nullptr) return false;
+    if (!BIOPointer::WriteAll(out, "DNS:")) return false;
     // Note that the preferred name syntax (see RFCs 5280 and 1034) with
     // wildcards is a subset of what we consider "safe", so spec-compliant DNS
     // names will never need to be escaped.
-    PrintAltName(out,
-                 reinterpret_cast<const char*>(ASN1_STRING_get0_data(name)),
-                 ASN1_STRING_length(name));
+    int length = ASN1_STRING_length(name);
+    return length >= 0 && PrintAltName(out,
+                                       reinterpret_cast<const char*>(
+                                           ASN1_STRING_get0_data(name)),
+                                       static_cast<size_t>(length));
   } else if (gen->type == GEN_EMAIL) {
     ASN1_IA5STRING* name = gen->d.rfc822Name;
-    BIO_write(out.get(), "email:", 6);
-    PrintAltName(out,
-                 reinterpret_cast<const char*>(ASN1_STRING_get0_data(name)),
-                 ASN1_STRING_length(name));
+    if (name == nullptr) return false;
+    if (!BIOPointer::WriteAll(out, "email:")) return false;
+    int length = ASN1_STRING_length(name);
+    return length >= 0 && PrintAltName(out,
+                                       reinterpret_cast<const char*>(
+                                           ASN1_STRING_get0_data(name)),
+                                       static_cast<size_t>(length));
   } else if (gen->type == GEN_URI) {
     ASN1_IA5STRING* name = gen->d.uniformResourceIdentifier;
-    BIO_write(out.get(), "URI:", 4);
+    if (name == nullptr) return false;
+    if (!BIOPointer::WriteAll(out, "URI:")) return false;
     // The set of "safe" names was designed to include just about any URI,
     // with a few exceptions, most notably URIs that contains commas (see
     // RFC 2396). In other words, most legitimate URIs will not require
     // escaping.
-    PrintAltName(out,
-                 reinterpret_cast<const char*>(ASN1_STRING_get0_data(name)),
-                 ASN1_STRING_length(name));
+    int length = ASN1_STRING_length(name);
+    return length >= 0 && PrintAltName(out,
+                                       reinterpret_cast<const char*>(
+                                           ASN1_STRING_get0_data(name)),
+                                       static_cast<size_t>(length));
   } else if (gen->type == GEN_DIRNAME) {
+    if (gen->d.dirn == nullptr) return false;
     // Earlier versions of Node.js used X509_NAME_oneline to print the X509_NAME
     // object. The format was non standard and should be avoided. The use of
     // X509_NAME_oneline is discouraged by OpenSSL but was required for backward
@@ -1115,9 +1146,9 @@ bool PrintGeneralName(const BIOPointer& out, const GENERAL_NAME* gen) {
     // format may contain Unicode characters and it is likely to contain commas,
     // which require escaping. Fortunately, the recently safeguarded function
     // PrintAltName handles all of that safely.
-    BIO_printf(out.get(), "DirName:");
-    BIOPointer tmp(BIO_new(BIO_s_mem()));
-    NCRYPTO_ASSERT_TRUE(tmp);
+    if (!BIOPointer::WriteAll(out, "DirName:")) return false;
+    BIOPointer tmp = BIOPointer::NewMem();
+    if (!tmp) return false;
     if (X509_NAME_print_ex(
             tmp.get(), gen->d.dirn, 0, kX509NameFlagsRFC2253WithinUtf8JSON) <
         0) {
@@ -1125,38 +1156,46 @@ bool PrintGeneralName(const BIOPointer& out, const GENERAL_NAME* gen) {
     }
     char* oline = nullptr;
     long n_bytes = BIO_get_mem_data(tmp.get(), &oline);  // NOLINT(runtime/int)
-    NCRYPTO_ASSERT_TRUE(n_bytes >= 0);
-    PrintAltName(out,
-                 oline,
-                 static_cast<size_t>(n_bytes),
-                 ncrypto::AltNameOption::UTF8,
-                 nullptr);
+    if (n_bytes < 0) return false;
+    return PrintAltName(out,
+                        oline,
+                        static_cast<size_t>(n_bytes),
+                        ncrypto::AltNameOption::UTF8,
+                        nullptr);
   } else if (gen->type == GEN_IPADD) {
-    BIO_printf(out.get(), "IP Address:");
+    if (!BIOPointer::WriteAll(out, "IP Address:")) return false;
     const ASN1_OCTET_STRING* ip = gen->d.ip;
+    if (ip == nullptr) return false;
     const unsigned char* b = ASN1_STRING_get0_data(ip);
     int ip_len = ASN1_STRING_length(ip);
+    if (ip_len < 0 || (b == nullptr && ip_len != 0)) return false;
     if (ip_len == 4) {
-      BIO_printf(out.get(), "%d.%d.%d.%d", b[0], b[1], b[2], b[3]);
+      return BIOPointer::PrintfAll(out, "%d.%d.%d.%d", b[0], b[1], b[2], b[3]);
     } else if (ip_len == 16) {
       for (unsigned int j = 0; j < 8; j++) {
         uint16_t pair = (b[2 * j] << 8) | b[2 * j + 1];
-        BIO_printf(out.get(), (j == 0) ? "%X" : ":%X", pair);
+        if (!BIOPointer::PrintfAll(out, (j == 0) ? "%X" : ":%X", pair)) {
+          return false;
+        }
       }
     } else {
-#if OPENSSL_VERSION_MAJOR >= 3
-      BIO_printf(out.get(), "<invalid length=%d>", ip_len);
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+      return BIOPointer::PrintfAll(out, "<invalid length=%d>", ip_len);
 #else
-      BIO_printf(out.get(), "<invalid>");
+      return BIOPointer::WriteAll(out, "<invalid>");
 #endif
     }
   } else if (gen->type == GEN_RID) {
     // Unlike OpenSSL's default implementation, never print the OID as text and
     // instead always print its numeric representation.
-    char oline[256];
-    OBJ_obj2txt(oline, sizeof(oline), gen->d.rid, true);
-    BIO_printf(out.get(), "Registered ID:%s", oline);
+    auto oid = ObjectToText(gen->d.rid, true);
+    return oid.has_value() && BIOPointer::WriteAll(out, "Registered ID:") &&
+           BIOPointer::WriteAll(out, oid.value());
   } else if (gen->type == GEN_OTHERNAME) {
+    if (gen->d.otherName == nullptr || gen->d.otherName->type_id == nullptr ||
+        gen->d.otherName->value == nullptr) {
+      return false;
+    }
     // The format that is used here is based on OpenSSL's implementation of
     // GENERAL_NAME_print (as of OpenSSL 3.0.1). Earlier versions of Node.js
     // instead produced the same format as i2v_GENERAL_NAME, which was somewhat
@@ -1165,7 +1204,7 @@ bool PrintGeneralName(const BIOPointer& out, const GENERAL_NAME* gen) {
     const char* prefix = nullptr;
     // OpenSSL 1.1.1 does not support othername in GENERAL_NAME_print and may
     // not define these NIDs.
-#if OPENSSL_VERSION_MAJOR >= 3
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
     int nid = OBJ_obj2nid(gen->d.otherName->type_id);
     switch (nid) {
       case NID_id_on_SmtpUTF8Mailbox:
@@ -1185,35 +1224,41 @@ bool PrintGeneralName(const BIOPointer& out, const GENERAL_NAME* gen) {
         prefix = "NAIRealm";
         break;
     }
-#endif  // OPENSSL_VERSION_MAJOR >= 3
+#endif  // NCRYPTO_USE_OPENSSL3_PROVIDER
     int val_type = gen->d.otherName->value->type;
     if (prefix == nullptr || (unicode && val_type != V_ASN1_UTF8STRING) ||
         (!unicode && val_type != V_ASN1_IA5STRING)) {
-      BIO_printf(out.get(), "othername:<unsupported>");
+      return BIOPointer::WriteAll(out, "othername:<unsupported>");
     } else {
-      BIO_printf(out.get(), "othername:");
+      if (!BIOPointer::WriteAll(out, "othername:")) return false;
       if (unicode) {
         auto name = gen->d.otherName->value->value.utf8string;
-        PrintAltName(out,
-                     reinterpret_cast<const char*>(ASN1_STRING_get0_data(name)),
-                     ASN1_STRING_length(name),
-                     AltNameOption::UTF8,
-                     prefix);
+        if (name == nullptr) return false;
+        int length = ASN1_STRING_length(name);
+        return length >= 0 && PrintAltName(out,
+                                           reinterpret_cast<const char*>(
+                                               ASN1_STRING_get0_data(name)),
+                                           static_cast<size_t>(length),
+                                           AltNameOption::UTF8,
+                                           prefix);
       } else {
         auto name = gen->d.otherName->value->value.ia5string;
-        PrintAltName(out,
-                     reinterpret_cast<const char*>(ASN1_STRING_get0_data(name)),
-                     ASN1_STRING_length(name),
-                     AltNameOption::NONE,
-                     prefix);
+        if (name == nullptr) return false;
+        int length = ASN1_STRING_length(name);
+        return length >= 0 && PrintAltName(out,
+                                           reinterpret_cast<const char*>(
+                                               ASN1_STRING_get0_data(name)),
+                                           static_cast<size_t>(length),
+                                           AltNameOption::NONE,
+                                           prefix);
       }
     }
   } else if (gen->type == GEN_X400) {
     // TODO(tniessen): this is what OpenSSL does, implement properly instead
-    BIO_printf(out.get(), "X400Name:<unsupported>");
+    return BIOPointer::WriteAll(out, "X400Name:<unsupported>");
   } else if (gen->type == GEN_EDIPARTY) {
     // TODO(tniessen): this is what OpenSSL does, implement properly instead
-    BIO_printf(out.get(), "EdiPartyName:<unsupported>");
+    return BIOPointer::WriteAll(out, "EdiPartyName:<unsupported>");
   } else {
     // This is safe because X509V3_EXT_d2i would have returned nullptr in this
     // case already.
@@ -1226,20 +1271,30 @@ bool PrintGeneralName(const BIOPointer& out, const GENERAL_NAME* gen) {

 bool SafeX509SubjectAltNamePrint(const BIOPointer& out,
                                  const X509_EXTENSION* ext) {
+  if (!out || ext == nullptr) return false;
   // const_cast needed for OpenSSL < 4.0 which lacks const-correctness
   auto* mext = const_cast<X509_EXTENSION*>(ext);
-  auto ret = OBJ_obj2nid(X509_EXTENSION_get_object(mext));
+  const ASN1_OBJECT* object = X509_EXTENSION_get_object(mext);
+  if (object == nullptr) return false;
+  auto ret = OBJ_obj2nid(object);
   if (ret != NID_subject_alt_name) return false;

   GENERAL_NAMES* names = static_cast<GENERAL_NAMES*>(X509V3_EXT_d2i(mext));
   if (names == nullptr) return false;

-  bool ok = true;
+  const OPENSSL_SIZE_T count = sk_GENERAL_NAME_num(names);
+  bool ok = count > 0;

-  for (OPENSSL_SIZE_T i = 0; i < sk_GENERAL_NAME_num(names); i++) {
+  for (OPENSSL_SIZE_T i = 0; ok && i < count; i++) {
     GENERAL_NAME* gen = sk_GENERAL_NAME_value(names, i);
+    if (gen == nullptr) {
+      ok = false;
+      break;
+    }

-    if (i != 0) BIO_write(out.get(), ", ", 2);
+    if (i != 0 && !(ok = BIOPointer::WriteAll(out, ", "))) {
+      break;
+    }

     if (!(ok = ncrypto::PrintGeneralName(out, gen))) {
       break;
@@ -1251,33 +1306,47 @@ bool SafeX509SubjectAltNamePrint(const BIOPointer& out,
 }

 bool SafeX509InfoAccessPrint(const BIOPointer& out, const X509_EXTENSION* ext) {
+  if (!out || ext == nullptr) return false;
   // const_cast needed for OpenSSL < 4.0 which lacks const-correctness
   auto* mext = const_cast<X509_EXTENSION*>(ext);
-  auto ret = OBJ_obj2nid(X509_EXTENSION_get_object(mext));
+  const ASN1_OBJECT* object = X509_EXTENSION_get_object(mext);
+  if (object == nullptr) return false;
+  auto ret = OBJ_obj2nid(object);
   if (ret != NID_info_access) return false;

   AUTHORITY_INFO_ACCESS* descs =
       static_cast<AUTHORITY_INFO_ACCESS*>(X509V3_EXT_d2i(mext));
   if (descs == nullptr) return false;

-  bool ok = true;
+  const OPENSSL_SIZE_T count = sk_ACCESS_DESCRIPTION_num(descs);
+  bool ok = count > 0;

-  for (OPENSSL_SIZE_T i = 0; i < sk_ACCESS_DESCRIPTION_num(descs); i++) {
+  for (OPENSSL_SIZE_T i = 0; ok && i < count; i++) {
     ACCESS_DESCRIPTION* desc = sk_ACCESS_DESCRIPTION_value(descs, i);
+    if (desc == nullptr || desc->method == nullptr ||
+        desc->location == nullptr) {
+      ok = false;
+      break;
+    }

-    if (i != 0) BIO_write(out.get(), "\n", 1);
+    if (i != 0 && !(ok = BIOPointer::WriteAll(out, "\n"))) {
+      break;
+    }

-    char objtmp[80];
-    i2t_ASN1_OBJECT(objtmp, sizeof(objtmp), desc->method);
-    BIO_printf(out.get(), "%s - ", objtmp);
+    auto method = ObjectToText(desc->method, false);
+    if (!(ok = method.has_value() &&
+               BIOPointer::WriteAll(out, method.value()) &&
+               BIOPointer::WriteAll(out, " - "))) {
+      break;
+    }
     if (!(ok = ncrypto::PrintGeneralName(out, desc->location))) {
       break;
     }
   }
   sk_ACCESS_DESCRIPTION_pop_free(descs, ACCESS_DESCRIPTION_free);

-#if OPENSSL_VERSION_MAJOR < 3
-  BIO_write(out.get(), "\n", 1);
+#if NCRYPTO_USE_LEGACY_OPENSSL || NCRYPTO_USE_BORINGSSL
+  if (ok && !BIOPointer::WriteAll(out, "\n")) return false;
 #endif

   return ok;
@@ -1314,7 +1383,7 @@ X509View X509Pointer::view() const {
 }

 BIOPointer X509View::toPEM() const {
-  ClearErrorOnReturn clearErrorOnReturn;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr) return {};
   BIOPointer bio(BIO_new(BIO_s_mem()));
   if (!bio) return {};
@@ -1323,7 +1392,7 @@ BIOPointer X509View::toPEM() const {
 }

 BIOPointer X509View::toDER() const {
-  ClearErrorOnReturn clearErrorOnReturn;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr) return {};
   BIOPointer bio(BIO_new(BIO_s_mem()));
   if (!bio) return {};
@@ -1332,85 +1401,115 @@ BIOPointer X509View::toDER() const {
 }

 const X509Name X509View::getSubjectName() const {
-  ClearErrorOnReturn clearErrorOnReturn;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr) return {};
   return X509Name(X509_get_subject_name(cert_));
 }

 const X509Name X509View::getIssuerName() const {
-  ClearErrorOnReturn clearErrorOnReturn;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr) return {};
   return X509Name(X509_get_issuer_name(cert_));
 }

 BIOPointer X509View::getSubject() const {
-  ClearErrorOnReturn clearErrorOnReturn;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr) return {};
-  BIOPointer bio(BIO_new(BIO_s_mem()));
+  const X509_NAME* name = X509_get_subject_name(cert_);
+  if (name == nullptr) return {};
+  BIOPointer bio = BIOPointer::NewMem();
   if (!bio) return {};
-  if (X509_NAME_print_ex(bio.get(),
-                         X509_get_subject_name(cert_),
-                         0,
-                         kX509NameFlagsMultiline) <= 0) {
+  if (X509_NAME_print_ex(bio.get(), name, 0, kX509NameFlagsMultiline) < 0) {
     return {};
   }
   return bio;
 }

-BIOPointer X509View::getSubjectAltName() const {
-  ClearErrorOnReturn clearErrorOnReturn;
+BIOPointer X509View::getSubjectAltName(bool* has_extension) const {
+  MarkPopErrorOnReturn mark_pop_error_on_return;
+  if (has_extension != nullptr) *has_extension = false;
   if (cert_ == nullptr) return {};
-  BIOPointer bio(BIO_new(BIO_s_mem()));
-  if (!bio) return {};
   int index = X509_get_ext_by_NID(cert_, NID_subject_alt_name, -1);
-  if (index < 0 ||
-      !SafeX509SubjectAltNamePrint(bio, X509_get_ext(cert_, index))) {
+  if (index < 0) return {};
+  if (has_extension != nullptr) *has_extension = true;
+
+  const X509_EXTENSION* extension = X509_get_ext(cert_, index);
+  BIOPointer bio = BIOPointer::NewMem();
+  if (!bio || extension == nullptr ||
+      !SafeX509SubjectAltNamePrint(bio, extension)) {
     return {};
   }
   return bio;
 }

 BIOPointer X509View::getIssuer() const {
-  ClearErrorOnReturn clearErrorOnReturn;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr) return {};
-  BIOPointer bio(BIO_new(BIO_s_mem()));
+  const X509_NAME* name = X509_get_issuer_name(cert_);
+  if (name == nullptr) return {};
+  BIOPointer bio = BIOPointer::NewMem();
   if (!bio) return {};
-  if (X509_NAME_print_ex(
-          bio.get(), X509_get_issuer_name(cert_), 0, kX509NameFlagsMultiline) <=
-      0) {
+  if (X509_NAME_print_ex(bio.get(), name, 0, kX509NameFlagsMultiline) < 0) {
     return {};
   }
   return bio;
 }

 BIOPointer X509View::getInfoAccess() const {
-  ClearErrorOnReturn clearErrorOnReturn;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr) return {};
   BIOPointer bio(BIO_new(BIO_s_mem()));
   if (!bio) return {};
   int index = X509_get_ext_by_NID(cert_, NID_info_access, -1);
   if (index < 0) return {};
-  if (!SafeX509InfoAccessPrint(bio, X509_get_ext(cert_, index))) {
+  const X509_EXTENSION* extension = X509_get_ext(cert_, index);
+  if (extension == nullptr || !SafeX509InfoAccessPrint(bio, extension)) {
     return {};
   }
   return bio;
 }

+namespace {
+bool IsX509TimePrintable(const ASN1_TIME* time) {
+#if NCRYPTO_USE_BORINGSSL
+  // BoringSSL's ASN1_TIME_check() accepts legacy timezone offsets that its
+  // ASN1_TIME_print() deliberately rejects. Use the same strict conversion
+  // path as its printer so the fallback is selected before writing output.
+  int64_t ignored{};
+  return ASN1_TIME_to_posix(time, &ignored) == 1;
+#else
+  return ASN1_TIME_check(time) == 1;
+#endif
+}
+}  // namespace
+
 BIOPointer X509View::getValidFrom() const {
-  ClearErrorOnReturn clearErrorOnReturn;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr) return {};
-  BIOPointer bio(BIO_new(BIO_s_mem()));
+  const ASN1_TIME* time = X509_get0_notBefore(cert_);
+  if (time == nullptr) return {};
+  BIOPointer bio = BIOPointer::NewMem();
   if (!bio) return {};
-  ASN1_TIME_print(bio.get(), X509_get0_notBefore(cert_));
+  if (IsX509TimePrintable(time)) {
+    if (ASN1_TIME_print(bio.get(), time) <= 0) return {};
+  } else if (!BIOPointer::WriteAll(bio, "Bad time value")) {
+    return {};
+  }
   return bio;
 }

 BIOPointer X509View::getValidTo() const {
-  ClearErrorOnReturn clearErrorOnReturn;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr) return {};
-  BIOPointer bio(BIO_new(BIO_s_mem()));
+  const ASN1_TIME* time = X509_get0_notAfter(cert_);
+  if (time == nullptr) return {};
+  BIOPointer bio = BIOPointer::NewMem();
   if (!bio) return {};
-  ASN1_TIME_print(bio.get(), X509_get0_notAfter(cert_));
+  if (IsX509TimePrintable(time)) {
+    if (ASN1_TIME_print(bio.get(), time) <= 0) return {};
+  } else if (!BIOPointer::WriteAll(bio, "Bad time value")) {
+    return {};
+  }
   return bio;
 }

@@ -1424,6 +1523,7 @@ std::optional<std::string_view> X509View::getSignatureAlgorithm() const {
 }

 std::optional<std::string> X509View::getSignatureAlgorithmOID() const {
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr) return std::nullopt;
   const X509_ALGOR* alg = nullptr;
   X509_get0_signature(nullptr, &alg, cert_);
@@ -1431,36 +1531,41 @@ std::optional<std::string> X509View::getSignatureAlgorithmOID() const {
   const ASN1_OBJECT* obj = nullptr;
   X509_ALGOR_get0(&obj, nullptr, nullptr, alg);
   if (obj == nullptr) return std::nullopt;
-  std::array<char, 128> buf{};
-  int len = OBJ_obj2txt(buf.data(), buf.size(), obj, 1);
-  if (len < 0 || static_cast<size_t>(len) >= buf.size()) return std::nullopt;
-  return std::string(buf.data(), static_cast<size_t>(len));
+  return ObjectToText(obj, true);
 }

-int64_t X509View::getValidToTime() const {
-#ifdef OPENSSL_IS_BORINGSSL
+std::optional<int64_t> X509View::getValidToTime() const {
+  MarkPopErrorOnReturn mark_pop_error_on_return;
+  if (cert_ == nullptr) return std::nullopt;
+  const ASN1_TIME* time = X509_get0_notAfter(cert_);
+  if (time == nullptr) return std::nullopt;
+#if NCRYPTO_USE_BORINGSSL
   // Boringssl does not implement ASN1_TIME_to_tm in a public way,
   // and only recently added ASN1_TIME_to_posix. Some boringssl
   // users on older version may still need to patch around this
   // or use a different implementation.
-  int64_t tp;
-  ASN1_TIME_to_posix(X509_get0_notAfter(cert_), &tp);
+  int64_t tp{};
+  if (ASN1_TIME_to_posix(time, &tp) != 1) return std::nullopt;
   return tp;
 #else
-  struct tm tp;
-  ASN1_TIME_to_tm(X509_get0_notAfter(cert_), &tp);
+  struct tm tp {};
+  if (ASN1_TIME_to_tm(time, &tp) != 1) return std::nullopt;
   return PortableTimeGM(&tp);
 #endif
 }

-int64_t X509View::getValidFromTime() const {
-#ifdef OPENSSL_IS_BORINGSSL
-  int64_t tp;
-  ASN1_TIME_to_posix(X509_get0_notBefore(cert_), &tp);
+std::optional<int64_t> X509View::getValidFromTime() const {
+  MarkPopErrorOnReturn mark_pop_error_on_return;
+  if (cert_ == nullptr) return std::nullopt;
+  const ASN1_TIME* time = X509_get0_notBefore(cert_);
+  if (time == nullptr) return std::nullopt;
+#if NCRYPTO_USE_BORINGSSL
+  int64_t tp{};
+  if (ASN1_TIME_to_posix(time, &tp) != 1) return std::nullopt;
   return tp;
 #else
-  struct tm tp;
-  ASN1_TIME_to_tm(X509_get0_notBefore(cert_), &tp);
+  struct tm tp {};
+  if (ASN1_TIME_to_tm(time, &tp) != 1) return std::nullopt;
   return PortableTimeGM(&tp);
 #endif
 }
@@ -1520,7 +1625,7 @@ bool X509View::checkPublicKey(const EVPKeyPointer& pkey) const {
 X509View::CheckMatch X509View::checkHost(const std::string_view host,
                                          int flags,
                                          DataPointer* peerName) const {
-  ClearErrorOnReturn clearErrorOnReturn;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr) return CheckMatch::NO_MATCH;
   char* peername;
   switch (X509_check_host(
@@ -1543,7 +1648,7 @@ X509View::CheckMatch X509View::checkHost(const std::string_view host,

 X509View::CheckMatch X509View::checkEmail(const std::string_view email,
                                           int flags) const {
-  ClearErrorOnReturn clearErrorOnReturn;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr) return CheckMatch::NO_MATCH;
   switch (X509_check_email(
       const_cast<X509*>(cert_), email.data(), email.size(), flags)) {
@@ -1558,11 +1663,12 @@ X509View::CheckMatch X509View::checkEmail(const std::string_view email,
   }
 }

-X509View::CheckMatch X509View::checkIp(const std::string_view ip,
+X509View::CheckMatch X509View::checkIp(const Buffer<const unsigned char>& ip,
                                        int flags) const {
-  ClearErrorOnReturn clearErrorOnReturn;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr) return CheckMatch::NO_MATCH;
-  switch (X509_check_ip_asc(const_cast<X509*>(cert_), ip.data(), flags)) {
+  if (ip.len != 4 && ip.len != 16) return CheckMatch::INVALID_NAME;
+  switch (X509_check_ip(const_cast<X509*>(cert_), ip.data, ip.len, flags)) {
     case 0:
       return CheckMatch::NO_MATCH;
     case 1:
@@ -1634,49 +1740,73 @@ Result<X509Pointer, int> X509Pointer::Parse(

 bool X509View::enumUsages(UsageCallback callback) const {
   if (cert_ == nullptr) return false;
-  StackOfASN1 eku(static_cast<STACK_OF(ASN1_OBJECT)*>(
-      X509_get_ext_d2i(cert_, NID_ext_key_usage, nullptr, nullptr)));
+  StackOfASN1 eku;
+  {
+    MarkPopErrorOnReturn mark_pop_error_on_return;
+    eku.reset(static_cast<STACK_OF(ASN1_OBJECT)*>(
+        X509_get_ext_d2i(cert_, NID_ext_key_usage, nullptr, nullptr)));
+  }
   if (!eku) return false;
   const int count = sk_ASN1_OBJECT_num(eku.get());
-  char buf[256]{};

   for (int i = 0; i < count; i++) {
-    if (OBJ_obj2txt(buf, sizeof(buf), sk_ASN1_OBJECT_value(eku.get(), i), 1) >=
-        0) {
-      callback(buf);
+    std::optional<std::string> usage;
+    {
+      MarkPopErrorOnReturn mark_pop_error_on_return;
+      usage = ObjectToText(sk_ASN1_OBJECT_value(eku.get(), i), true);
     }
+    if (!usage.has_value()) return false;
+    callback(usage->c_str());
   }
   return true;
 }

 bool X509View::ifRsa(KeyCallback<Rsa> callback) const {
   if (cert_ == nullptr) return true;
-  OSSL3_CONST EVP_PKEY* pkey = X509_get0_pubkey(cert_);
+  OSSL3_CONST EVP_PKEY* pkey;
+  {
+    MarkPopErrorOnReturn mark_pop_error_on_return;
+    pkey = X509_get0_pubkey(cert_);
+  }
+  if (pkey == nullptr) return true;
   if (EVPKeyPointer::isRsaVariant(pkey)) {
+    std::optional<Rsa> rsa;
+    {
+      MarkPopErrorOnReturn mark_pop_error_on_return;
 #if NCRYPTO_USE_OPENSSL3_PROVIDER
-    Rsa rsa(pkey);
+      rsa.emplace(pkey);
 #else
-    Rsa rsa(EVP_PKEY_get0_RSA(pkey));
+      rsa.emplace(EVP_PKEY_get0_RSA(pkey));
 #endif
-    if (!rsa) [[unlikely]]
+    }
+    if (!*rsa) [[unlikely]]
       return true;
-    return callback(rsa);
+    return callback(*rsa);
   }
   return true;
 }

 bool X509View::ifEc(KeyCallback<Ec> callback) const {
   if (cert_ == nullptr) return true;
-  OSSL3_CONST EVP_PKEY* pkey = X509_get0_pubkey(cert_);
+  OSSL3_CONST EVP_PKEY* pkey;
+  {
+    MarkPopErrorOnReturn mark_pop_error_on_return;
+    pkey = X509_get0_pubkey(cert_);
+  }
+  if (pkey == nullptr) return true;
   if (EVPKeyPointer::isA(pkey, KeyAlgorithm::EC)) {
+    std::optional<Ec> ec;
+    {
+      MarkPopErrorOnReturn mark_pop_error_on_return;
 #if NCRYPTO_USE_OPENSSL3_PROVIDER
-    Ec ec(pkey);
+      ec.emplace(pkey);
 #else
-    Ec ec(EVP_PKEY_get0_EC_KEY(pkey));
+      ec.emplace(EVP_PKEY_get0_EC_KEY(pkey));
 #endif
-    if (!ec) [[unlikely]]
+    }
+    if (!*ec) [[unlikely]]
       return true;
-    return callback(ec);
+    return callback(*ec);
   }
   return true;
 }
@@ -1825,6 +1955,18 @@ int BIOPointer::Write(BIOPointer* bio, std::string_view message) {
   return BIO_write(bio->get(), message.data(), message.size());
 }

+bool BIOPointer::WriteAll(const BIOPointer& bio, std::string_view message) {
+  if (!bio) return false;
+  while (!message.empty()) {
+    const size_t chunk_size =
+        std::min(message.size(), static_cast<size_t>(INT_MAX));
+    const int length = static_cast<int>(chunk_size);
+    if (BIO_write(bio.get(), message.data(), length) != length) return false;
+    message.remove_prefix(chunk_size);
+  }
+  return true;
+}
+
 // ============================================================================
 // DHPointer

@@ -8297,7 +8439,7 @@ DataPointer xofHashDigest(const Buffer<const unsigned char>& buf,
 X509Name::X509Name() : name_(nullptr), total_(0) {}

 X509Name::X509Name(const X509_NAME* name)
-    : name_(name), total_(X509_NAME_entry_count(name)) {}
+    : name_(name), total_(name == nullptr ? 0 : X509_NAME_entry_count(name)) {}

 X509Name::Iterator::Iterator(const X509Name& name, int pos)
     : name_(name), loc_(pos) {}
@@ -8319,40 +8461,67 @@ bool X509Name::Iterator::operator!=(const Iterator& other) const {
   return loc_ != other.loc_;
 }

-std::pair<std::string, std::string> X509Name::Iterator::operator*() const {
-  if (loc_ == name_.total_) return {{}, {}};
+std::optional<X509Name::Entry> X509Name::Iterator::operator*() const {
+  MarkPopErrorOnReturn mark_pop_error_on_return;
+  if (loc_ == name_.total_) return std::nullopt;

   const X509_NAME_ENTRY* entry = X509_NAME_get_entry(name_, loc_);
   if (entry == nullptr) [[unlikely]]
-    return {{}, {}};
+    return std::nullopt;

   const ASN1_OBJECT* name = X509_NAME_ENTRY_get_object(entry);
   const ASN1_STRING* value = X509_NAME_ENTRY_get_data(entry);

   if (name == nullptr || value == nullptr) [[unlikely]] {
-    return {{}, {}};
+    return std::nullopt;
   }

   int nid = OBJ_obj2nid(name);
   std::string name_str;
   if (nid != NID_undef) {
-    name_str = std::string(OBJ_nid2sn(nid));
+    const char* short_name = OBJ_nid2sn(nid);
+    if (short_name == nullptr) return std::nullopt;
+    name_str = short_name;
   } else {
-    char buf[80];
-    OBJ_obj2txt(buf, sizeof(buf), name, 0);
-    name_str = std::string(buf);
+    auto name_text = ObjectToText(name, false);
+    if (!name_text.has_value()) [[unlikely]] {
+      return std::nullopt;
+    }
+    name_str = std::move(name_text.value());
+  }
+
+  switch (ASN1_STRING_type(value)) {
+    case V_ASN1_UTF8STRING:
+    case V_ASN1_NUMERICSTRING:
+    case V_ASN1_PRINTABLESTRING:
+    case V_ASN1_T61STRING:
+    case V_ASN1_IA5STRING:
+    case V_ASN1_UTCTIME:
+    case V_ASN1_GENERALIZEDTIME:
+    case V_ASN1_ISO64STRING:
+    case V_ASN1_UNIVERSALSTRING:
+    case V_ASN1_BMPSTRING:
+      break;
+    default:
+      return Entry{std::move(name_str), std::nullopt};
   }

-  unsigned char* value_str;
+  unsigned char* value_str = nullptr;
   int value_str_size = ASN1_STRING_to_UTF8(&value_str, value);
+  DataPointer value_data(
+      value_str, value_str_size > 0 ? static_cast<size_t>(value_str_size) : 0);
   if (value_str_size < 0) [[unlikely]] {
-    return {{}, {}};
+    return std::nullopt;
   }

-  std::string out(reinterpret_cast<const char*>(value_str), value_str_size);
-  OPENSSL_free(value_str);  // free after copy
+  std::string out;
+  if (value_str_size > 0) {
+    if (!value_data) return std::nullopt;
+    out.assign(reinterpret_cast<const char*>(value_data.get()),
+               static_cast<size_t>(value_str_size));
+  }

-  return {std::move(name_str), std::move(out)};
+  return Entry{std::move(name_str), std::move(out)};
 }

 // ============================================================================
diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h
index 30dccfc0c79..9a501a551b4 100644
--- a/deps/ncrypto/ncrypto.h
+++ b/deps/ncrypto/ncrypto.h
@@ -917,10 +917,37 @@ class BIOPointer final {

   static int Write(BIOPointer* bio, std::string_view message);

+  // Writes the complete message and returns false if any bytes were not
+  // written.
+  static bool WriteAll(const BIOPointer& bio, std::string_view message);
+
+  // Returns the number of bytes written, or a negative value on error.
+  template <typename... Args>
+  static int Printf(BIOPointer* bio, const char* format, Args... args) {
+    if (bio == nullptr || !*bio) return -1;
+    return BIO_printf(bio->get(), format, std::forward<Args>(args)...);
+  }
+
+  // Formats and writes the complete message, returning false if formatting
+  // fails or any bytes were not written.
   template <typename... Args>
-  static void Printf(BIOPointer* bio, const char* format, Args... args) {
-    if (bio == nullptr || !*bio) return;
-    BIO_printf(bio->get(), format, std::forward<Args...>(args...));
+  static bool PrintfAll(const BIOPointer& bio,
+                        const char* format,
+                        Args... args) {
+    if (!bio) return false;
+
+    BIOPointer formatted = NewMem();
+    if (!formatted) return false;
+    int length =
+        BIO_printf(formatted.get(), format, std::forward<Args>(args)...);
+    if (length < 0) return false;
+
+    BUF_MEM* mem = formatted;
+    if (mem == nullptr || mem->length != static_cast<size_t>(length)) {
+      return false;
+    }
+    if (length == 0) return true;
+    return WriteAll(bio, std::string_view(mem->data, mem->length));
   }

  private:
@@ -1573,6 +1600,13 @@ class SSLPointer final {

 class X509Name final {
  public:
+  // A missing Entry means the name entry could not be inspected. A missing
+  // value marks an AttributeValue that the legacy UTF-8 view cannot represent.
+  struct Entry {
+    std::string key;
+    std::optional<std::string> value;
+  };
+
   X509Name();
   explicit X509Name(const X509_NAME* name);
   NCRYPTO_DISALLOW_COPY_AND_MOVE(X509Name)
@@ -1593,7 +1627,7 @@ class X509Name final {
     operator bool() const;
     bool operator==(const Iterator& other) const;
     bool operator!=(const Iterator& other) const;
-    std::pair<std::string, std::string> operator*() const;
+    std::optional<Entry> operator*() const;

    private:
     const X509Name& name_;
@@ -1632,15 +1666,15 @@ class X509View final {
   const X509Name getSubjectName() const;
   const X509Name getIssuerName() const;
   BIOPointer getSubject() const;
-  BIOPointer getSubjectAltName() const;
+  BIOPointer getSubjectAltName(bool* has_extension = nullptr) const;
   BIOPointer getIssuer() const;
   BIOPointer getInfoAccess() const;
   BIOPointer getValidFrom() const;
   BIOPointer getValidTo() const;
   std::optional<std::string_view> getSignatureAlgorithm() const;
   std::optional<std::string> getSignatureAlgorithmOID() const;
-  int64_t getValidFromTime() const;
-  int64_t getValidToTime() const;
+  std::optional<int64_t> getValidFromTime() const;
+  std::optional<int64_t> getValidToTime() const;
   DataPointer getSerialNumber() const;
   Result<EVPKeyPointer, int> getPublicKey() const;
   StackOfASN1 getKeyUsage() const;
@@ -1664,7 +1698,7 @@ class X509View final {
                        int flags,
                        DataPointer* peerName = nullptr) const;
   CheckMatch checkEmail(std::string_view email, int flags) const;
-  CheckMatch checkIp(std::string_view ip, int flags) const;
+  CheckMatch checkIp(const Buffer<const unsigned char>& ip, int flags) const;

   using UsageCallback = std::function<void(const char*)>;
   bool enumUsages(UsageCallback callback) const;
diff --git a/src/crypto/crypto_x509.cc b/src/crypto/crypto_x509.cc
index 1198d05dab2..c7dbe956149 100644
--- a/src/crypto/crypto_x509.cc
+++ b/src/crypto/crypto_x509.cc
@@ -8,9 +8,13 @@
 #include "ncrypto.h"
 #include "node_errors.h"
 #include "util-inl.h"
+#include "uv.h"
 #include "v8.h"

+#include <array>
+#include <limits>
 #include <string>
+#include <string_view>
 #include <vector>

 namespace node {
@@ -20,6 +24,7 @@ using ncrypto::BIOPointer;
 using ncrypto::ClearErrorOnReturn;
 using ncrypto::DataPointer;
 using ncrypto::Digest;
+using ncrypto::MarkPopErrorOnReturn;
 using ncrypto::SSLPointer;
 using ncrypto::X509Name;
 using ncrypto::X509Pointer;
@@ -65,12 +70,20 @@ ManagedX509& ManagedX509::operator=(const ManagedX509& that) {

 void ManagedX509::MemoryInfo(MemoryTracker* tracker) const {
   if (!cert_) return;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   // This is an approximation based on the der encoding size.
-  int size = i2d_X509(cert_.get(), nullptr);
-  tracker->TrackFieldWithSize("cert", size);
+  const int size = i2d_X509(cert_.get(), nullptr);
+  if (size > 0) tracker->TrackFieldWithSize("cert", size);
 }

 namespace {
+enum class MalformedSubjectAltNameMode {
+  kThrow,
+  kReturnLegacySentinel,
+};
+
+constexpr std::string_view kMalformedSubjectAltName = "DNS:";
+
 MaybeLocal<Value> GetFingerprintDigest(Environment* env,
                                        const Digest& method,
                                        const X509View& cert) {
@@ -167,12 +180,29 @@ MaybeLocal<Value> GetDer(Environment* env, const X509View& view) {
   return ret;
 }

-MaybeLocal<Value> GetSubjectAltNameString(Environment* env,
-                                          const X509View& view) {
+MaybeLocal<Value> GetSubjectAltNameString(
+    Environment* env,
+    const X509View& view,
+    MalformedSubjectAltNameMode malformed_name_mode) {
   Local<Value> ret;
-  auto bio = view.getSubjectAltName();
-  if (!bio) [[unlikely]]
-    return Undefined(env->isolate());
+  bool has_extension = false;
+  auto bio = view.getSubjectAltName(&has_extension);
+  if (!bio) [[unlikely]] {
+    // An absent SAN maps to undefined. A malformed, present SAN throws for
+    // direct X509Certificate inspection, while the legacy TLS object uses a
+    // deliberately nonmatching value to prevent commonName fallback.
+    if (!has_extension) return Undefined(env->isolate());
+    if (malformed_name_mode ==
+        MalformedSubjectAltNameMode::kReturnLegacySentinel) {
+      // The legacy object is used by TLS hostname verification. Return an
+      // empty DNS name so verification cannot fall back to the common name,
+      // while keeping getPeerCertificate() usable for malformed certificates.
+      return OneByteString(env->isolate(), kMalformedSubjectAltName);
+    }
+    THROW_ERR_CRYPTO_OPERATION_FAILED(
+        env, "Failed to format subject alternative name");
+    return {};
+  }
   if (!ToV8Value(env->context(), bio).ToLocal(&ret)) return {};
   return ret;
 }
@@ -211,13 +241,19 @@ MaybeLocal<Value> GetValidTo(Environment* env, const X509View& view) {
 }

 MaybeLocal<Value> GetValidFromDate(Environment* env, const X509View& view) {
-  int64_t validFromTime = view.getValidFromTime();
-  return Date::New(env->context(), validFromTime * 1000.);
+  const auto valid_from_time = view.getValidFromTime();
+  const double milliseconds = valid_from_time.has_value()
+                                  ? valid_from_time.value() * 1000.
+                                  : std::numeric_limits<double>::quiet_NaN();
+  return Date::New(env->context(), milliseconds);
 }

 MaybeLocal<Value> GetValidToDate(Environment* env, const X509View& view) {
-  int64_t validToTime = view.getValidToTime();
-  return Date::New(env->context(), validToTime * 1000.);
+  const auto valid_to_time = view.getValidToTime();
+  const double milliseconds = valid_to_time.has_value()
+                                  ? valid_to_time.value() * 1000.
+                                  : std::numeric_limits<double>::quiet_NaN();
+  return Date::New(env->context(), milliseconds);
 }

 MaybeLocal<Value> GetSignatureAlgorithm(Environment* env,
@@ -296,7 +332,9 @@ void SubjectAltName(const FunctionCallbackInfo<Value>& args) {
   X509Certificate* cert;
   ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
   Local<Value> ret;
-  if (GetSubjectAltNameString(env, cert->view()).ToLocal(&ret)) {
+  if (GetSubjectAltNameString(
+          env, cert->view(), MalformedSubjectAltNameMode::kThrow)
+          .ToLocal(&ret)) {
     args.GetReturnValue().Set(ret);
   }
 }
@@ -517,11 +555,29 @@ void CheckEmail(const FunctionCallbackInfo<Value>& args) {
 }

 void CheckIP(const FunctionCallbackInfo<Value>& args) {
-  CheckX509Subject(args,
-                   [](X509View& cert,
-                      std::string_view subject,
-                      uint32_t flags,
-                      DataPointer*) { return cert.checkIp(subject, flags); });
+  CheckX509Subject(
+      args,
+      [](X509View& cert,
+         std::string_view subject,
+         uint32_t flags,
+         DataPointer*) {
+        if (subject.find('\0') != std::string_view::npos ||
+            subject.find('%') != std::string_view::npos) {
+          return X509View::CheckMatch::INVALID_NAME;
+        }
+
+        const std::string input(subject);
+        std::array<unsigned char, 16> address{};
+        size_t length = 0;
+        if (uv_inet_pton(AF_INET, input.c_str(), address.data()) == 0) {
+          length = 4;
+        } else if (uv_inet_pton(AF_INET6, input.c_str(), address.data()) == 0) {
+          length = 16;
+        } else {
+          return X509View::CheckMatch::INVALID_NAME;
+        }
+        return cert.checkIp({.data = address.data(), .len = length}, flags);
+      });
 }

 void GetIssuerCert(const FunctionCallbackInfo<Value>& args) {
@@ -606,9 +662,20 @@ static MaybeLocal<Value> GetX509NameObject(Environment* env,
       Object::New(env->isolate(), Null(env->isolate()), nullptr, nullptr, 0);
   if (result.IsEmpty()) return {};

-  for (auto i : name) {
-    if (!ToV8Value(env, i.first).ToLocal(&v8_name) ||
-        !ToV8Value(env, i.second).ToLocal(&v8_value)) {
+  for (auto entry : name) {
+    if (!entry.has_value()) [[unlikely]] {
+      THROW_ERR_CRYPTO_OPERATION_FAILED(env,
+                                        "Failed to convert X509 name to UTF-8");
+      return {};
+    }
+
+    const auto& [key, value] = *entry;
+    // The legacy object can only represent string values. Preserve valid
+    // sibling attributes and omit values that cannot be converted to UTF-8.
+    if (!value.has_value()) continue;
+
+    if (!ToV8Value(env, key).ToLocal(&v8_name) ||
+        !ToV8Value(env, *value).ToLocal(&v8_value)) {
       return {};
     }

@@ -669,11 +736,13 @@ MaybeLocal<Value> GetModulusString(Environment* env, const BIGNUM* n) {

 MaybeLocal<Value> GetExponentString(Environment* env, const BIGNUM* e) {
   if (e == nullptr) return Null(env->isolate());
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   auto bio = BIOPointer::NewMem();
   if (!bio) [[unlikely]]
     return {};
-  BIO_puts(bio.get(), "0x");
-  BN_print(bio.get(), e);
+  if (!BIOPointer::WriteAll(bio, "0x") || BN_print(bio.get(), e) != 1) {
+    return {};
+  }
   return ToV8Value(env->context(), bio);
 }

@@ -699,10 +768,11 @@ MaybeLocal<Value> GetECGroupBits(Environment* env, const EC_GROUP* group) {

 template <const char* (*nid2string)(int nid)>
 MaybeLocal<Value> GetCurveName(Environment* env, const int nid) {
-  const char* name = nid2string(nid);
-  return name != nullptr
-             ? MaybeLocal<Value>(OneByteString(env->isolate(), name))
-             : MaybeLocal<Value>(Undefined(env->isolate()));
+  const char* const curve_name = nid2string(nid);
+  if (curve_name == nullptr) return Undefined(env->isolate());
+  const std::string_view name = curve_name;
+  return name.size() ? MaybeLocal<Value>(OneByteString(env->isolate(), name))
+                     : MaybeLocal<Value>(Undefined(env->isolate()));
 }

 MaybeLocal<Object> X509ToObject(Environment* env, const X509View& cert) {
@@ -735,10 +805,21 @@ MaybeLocal<Object> X509ToObject(Environment* env, const X509View& cert) {
     env->set_x509_dictionary_template(tmpl);
   }

+  Local<Value> subject;
+  if (!GetX509NameObject(env, cert.getSubjectName()).ToLocal(&subject)) {
+    return {};
+  }
+
+  Local<Value> issuer;
+  if (!GetX509NameObject(env, cert.getIssuerName()).ToLocal(&issuer)) {
+    return {};
+  }
+
   MaybeLocal<Value> values[] = {
-      GetX509NameObject(env, cert.getSubjectName()),
-      GetX509NameObject(env, cert.getIssuerName()),
-      GetSubjectAltNameString(env, cert),
+      subject,
+      issuer,
+      GetSubjectAltNameString(
+          env, cert, MalformedSubjectAltNameMode::kReturnLegacySentinel),
       GetInfoAccessString(env, cert),
       Boolean::New(env->isolate(), cert.isCA()),
       Undefined(env->isolate()),  // modulus
@@ -757,6 +838,19 @@ MaybeLocal<Object> X509ToObject(Environment* env, const X509View& cert) {
       Undefined(env->isolate()),  // nistcurve
   };

+  // X509_get0_pubkey() fails when the linked OpenSSL cannot decode the
+  // certificate's public key (e.g. an unsupported curve in FIPS builds).
+  // ifRsa()/ifEc() assume a decodable key, so leave the key-specific
+  // fields undefined in that case.
+  const bool has_public_key = [&] {
+    MarkPopErrorOnReturn mark_pop_error_on_return;
+    return X509_get0_pubkey(cert.get()) != nullptr;
+  }();
+  if (!has_public_key) {
+    return scope.EscapeMaybe(
+        NewDictionaryInstance(env->context(), tmpl, values));
+  }
+
   cert.ifRsa([&](const ncrypto::Rsa& rsa) {
     auto pub_key = rsa.getPublicKey();
     values[5] = GetModulusString(env, pub_key.n);   // modulus
diff --git a/test/cctest/test_node_crypto.cc b/test/cctest/test_node_crypto.cc
index 6e1e687df0d..df387ab00f9 100644
--- a/test/cctest/test_node_crypto.cc
+++ b/test/cctest/test_node_crypto.cc
@@ -5,8 +5,10 @@
 #include "crypto/crypto_cipher.h"
 #include "crypto/crypto_context.h"
 #include "gtest/gtest.h"
+#include "ncrypto.h"
 #include "node_options.h"
 #include "openssl/err.h"
+#include "openssl/x509v3.h"
 #include "util.h"

 #include <climits>
@@ -27,6 +29,49 @@ using ncrypto::EVPKeyCtxPointer;
 using ncrypto::EVPKeyPointer;
 using ncrypto::KeyAlgorithm;

+namespace {
+
+struct GeneralNamesDeleter {
+  void operator()(GENERAL_NAMES* names) const {
+    sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free);
+  }
+};
+
+struct AccessDescriptionsDeleter {
+  void operator()(AUTHORITY_INFO_ACCESS* descriptions) const {
+    sk_ACCESS_DESCRIPTION_pop_free(descriptions, ACCESS_DESCRIPTION_free);
+  }
+};
+
+using GeneralNamePointer =
+    ncrypto::DeleteFnPtr<GENERAL_NAME, GENERAL_NAME_free>;
+using GeneralNamesPointer = std::unique_ptr<GENERAL_NAMES, GeneralNamesDeleter>;
+using AccessDescriptionPointer =
+    ncrypto::DeleteFnPtr<ACCESS_DESCRIPTION, ACCESS_DESCRIPTION_free>;
+using AccessDescriptionsPointer =
+    std::unique_ptr<AUTHORITY_INFO_ACCESS, AccessDescriptionsDeleter>;
+using ASN1ObjectPointer = ncrypto::DeleteFnPtr<ASN1_OBJECT, ASN1_OBJECT_free>;
+using ASN1StringPointer =
+    ncrypto::DeleteFnPtr<ASN1_IA5STRING, ASN1_IA5STRING_free>;
+using ASN1TimePointer = ncrypto::DeleteFnPtr<ASN1_TIME, ASN1_TIME_free>;
+using X509ExtensionPointer =
+    ncrypto::DeleteFnPtr<X509_EXTENSION, X509_EXTENSION_free>;
+using X509NamePointer = ncrypto::DeleteFnPtr<X509_NAME, X509_NAME_free>;
+
+std::string LongObjectIdentifier() {
+  std::string oid = "1.2";
+  for (int i = 0; i < 30; i++) oid += ".123456789";
+  return oid;
+}
+
+std::string BioContents(const ncrypto::BIOPointer& bio) {
+  BUF_MEM* mem = bio;
+  if (mem == nullptr) return {};
+  return std::string(mem->data, mem->length);
+}
+
+}  // namespace
+
 /*
  * This test verifies that a call to NewRootCertDir with the build time
  * configuration option --openssl-system-ca-path set to an missing file, will
@@ -889,3 +934,305 @@ TEST(NodeCrypto, PrivateKeyEncodingProviderOnlyCipher) {
   EXPECT_EQ(EVP_CIPHER_is_a(cipher.get(), "AES-128-CBC-CTS"), 1);
 }
 #endif
+
+TEST(NodeCrypto, BIOPointerRequiresCompleteWrites) {
+  BIO* raw_output = nullptr;
+  BIO* raw_input = nullptr;
+  ASSERT_EQ(BIO_new_bio_pair(&raw_output, 1, &raw_input, 1), 1);
+  ncrypto::BIOPointer output(raw_output);
+  ncrypto::BIOPointer input(raw_input);
+  EXPECT_FALSE(ncrypto::BIOPointer::WriteAll(output, "ab"));
+
+  raw_output = nullptr;
+  raw_input = nullptr;
+  ASSERT_EQ(BIO_new_bio_pair(&raw_output, 1, &raw_input, 1), 1);
+  output.reset(raw_output);
+  input.reset(raw_input);
+  EXPECT_FALSE(ncrypto::BIOPointer::PrintfAll(output, "%s", "ab"));
+}
+
+TEST(NodeCrypto, PreservesInvalidX509TimeDiagnostic) {
+  ncrypto::X509Pointer cert(X509_new());
+  ASN1TimePointer invalid_time(ASN1_STRING_type_new(V_ASN1_UTCTIME));
+  ASSERT_TRUE(cert);
+  ASSERT_TRUE(invalid_time);
+  ASSERT_EQ(ASN1_STRING_set(invalid_time.get(), "221303214037Z", -1), 1);
+  ASSERT_EQ(X509_set1_notBefore(cert.get(), invalid_time.get()), 1);
+
+  const ncrypto::BIOPointer valid_from = cert.view().getValidFrom();
+  ASSERT_TRUE(valid_from);
+  EXPECT_EQ(BioContents(valid_from), "Bad time value");
+  EXPECT_FALSE(cert.view().getValidFromTime().has_value());
+}
+
+#if NCRYPTO_USE_BORINGSSL
+TEST(NodeCrypto, PreservesBoringSSLTimezoneOffsetDiagnostic) {
+  ncrypto::X509Pointer cert(X509_new());
+  ASN1TimePointer offset_time(ASN1_STRING_type_new(V_ASN1_UTCTIME));
+  ASSERT_TRUE(cert);
+  ASSERT_TRUE(offset_time);
+  ASSERT_EQ(ASN1_STRING_set(offset_time.get(), "700101000000+0400", -1), 1);
+  ASSERT_EQ(ASN1_TIME_check(offset_time.get()), 1);
+  ASSERT_EQ(X509_set1_notBefore(cert.get(), offset_time.get()), 1);
+  ASSERT_EQ(X509_set1_notAfter(cert.get(), offset_time.get()), 1);
+
+  const ncrypto::BIOPointer valid_from = cert.view().getValidFrom();
+  const ncrypto::BIOPointer valid_to = cert.view().getValidTo();
+  ASSERT_TRUE(valid_from);
+  ASSERT_TRUE(valid_to);
+  EXPECT_EQ(BioContents(valid_from), "Bad time value");
+  EXPECT_EQ(BioContents(valid_to), "Bad time value");
+  EXPECT_FALSE(cert.view().getValidFromTime().has_value());
+  EXPECT_FALSE(cert.view().getValidToTime().has_value());
+}
+#endif
+
+TEST(NodeCrypto, PrintsEmptyX509Names) {
+  ncrypto::X509Pointer cert(X509_new());
+  X509NamePointer empty_name(X509_NAME_new());
+  ASSERT_TRUE(cert);
+  ASSERT_TRUE(empty_name);
+  ASSERT_EQ(X509_set_subject_name(cert.get(), empty_name.get()), 1);
+  ASSERT_EQ(X509_set_issuer_name(cert.get(), empty_name.get()), 1);
+
+  const ncrypto::BIOPointer subject = cert.view().getSubject();
+  const ncrypto::BIOPointer issuer = cert.view().getIssuer();
+  ASSERT_TRUE(subject);
+  ASSERT_TRUE(issuer);
+  EXPECT_EQ(BioContents(subject), "");
+  EXPECT_EQ(BioContents(issuer), "");
+}
+
+TEST(NodeCrypto, X509FormattingPreservesUnrelatedErrors) {
+  ncrypto::X509Pointer cert(X509_new());
+  X509NamePointer empty_name(X509_NAME_new());
+  ASSERT_TRUE(cert);
+  ASSERT_TRUE(empty_name);
+  ASSERT_EQ(X509_set_subject_name(cert.get(), empty_name.get()), 1);
+
+  ERR_clear_error();
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+  ERR_raise(ERR_LIB_USER, 1);
+#else
+  ERR_put_error(ERR_LIB_USER, 0, 1, __FILE__, __LINE__);
+#endif
+  const unsigned long expected = ERR_peek_error();  // NOLINT(runtime/int)
+  ASSERT_NE(expected, 0UL);
+
+  const ncrypto::BIOPointer subject = cert.view().getSubject();
+  ASSERT_TRUE(subject);
+  EXPECT_EQ(ERR_peek_error(), expected);
+  ERR_clear_error();
+}
+
+TEST(NodeCrypto, X509KeyVisitorsHandleMissingPublicKey) {
+  ncrypto::X509Pointer cert(X509_new());
+  ASSERT_TRUE(cert);
+
+  const auto visit = [&cert] {
+    EXPECT_TRUE(cert.view().ifRsa([](const ncrypto::Rsa&) {
+      ADD_FAILURE() << "RSA callback called without a public key";
+      return false;
+    }));
+    EXPECT_TRUE(cert.view().ifEc([](const ncrypto::Ec&) {
+      ADD_FAILURE() << "EC callback called without a public key";
+      return false;
+    }));
+  };
+
+  ERR_clear_error();
+  visit();
+  EXPECT_EQ(ERR_peek_error(), 0UL);
+
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+  ERR_raise(ERR_LIB_USER, 1);
+#else
+  ERR_put_error(ERR_LIB_USER, 0, 1, __FILE__, __LINE__);
+#endif
+  const unsigned long expected = ERR_peek_error();  // NOLINT(runtime/int)
+  ASSERT_NE(expected, 0UL);
+
+  visit();
+  EXPECT_EQ(ERR_peek_error(), expected);
+  ERR_clear_error();
+}
+
+TEST(NodeCrypto, X509UsageVisitorPreservesCallbackErrors) {
+  ncrypto::X509Pointer cert(X509_new());
+  ncrypto::StackOfASN1 usages(sk_ASN1_OBJECT_new_null());
+  ASN1ObjectPointer usage(OBJ_txt2obj("1.3.6.1.5.5.7.3.1", 1));
+  ASSERT_TRUE(cert);
+  ASSERT_TRUE(usages);
+  ASSERT_TRUE(usage);
+  ASSERT_GT(sk_ASN1_OBJECT_push(usages.get(), usage.get()), 0);
+  usage.release();
+
+  X509ExtensionPointer eku(X509V3_EXT_i2d(NID_ext_key_usage, 0, usages.get()));
+  ASSERT_TRUE(eku);
+  ASSERT_EQ(X509_add_ext(cert.get(), eku.get(), -1), 1);
+
+  ERR_clear_error();
+  int calls = 0;
+  EXPECT_TRUE(cert.view().enumUsages([&calls](const char* oid) {
+    calls++;
+    EXPECT_STREQ(oid, "1.3.6.1.5.5.7.3.1");
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+    ERR_raise(ERR_LIB_USER, 1);
+#else
+    ERR_put_error(ERR_LIB_USER, 0, 1, __FILE__, __LINE__);
+#endif
+  }));
+  EXPECT_EQ(calls, 1);
+  EXPECT_NE(ERR_peek_error(), 0UL);
+  ERR_clear_error();
+}
+
+TEST(NodeCrypto, PreservesLongX509NameObjectIdentifiers) {
+  const std::string oid_text = LongObjectIdentifier();
+  X509NamePointer name(X509_NAME_new());
+  ASN1ObjectPointer oid(OBJ_txt2obj(oid_text.c_str(), 1));
+  ASSERT_TRUE(name);
+  ASSERT_TRUE(oid);
+  ASSERT_EQ(X509_NAME_add_entry_by_OBJ(
+                name.get(),
+                oid.get(),
+                V_ASN1_UTF8STRING,
+                reinterpret_cast<const unsigned char*>("value"),
+                -1,
+                -1,
+                0),
+            1);
+
+  ncrypto::X509Name x509_name(name.get());
+  const auto entry = *x509_name.begin();
+  ASSERT_TRUE(entry.has_value());
+  EXPECT_EQ(entry->key, oid_text);
+  ASSERT_TRUE(entry->value.has_value());
+  EXPECT_EQ(*entry->value, "value");
+}
+
+TEST(NodeCrypto, IdentifiesUnconvertibleX509NameValues) {
+  X509NamePointer name(X509_NAME_new());
+  ASN1ObjectPointer oid(OBJ_txt2obj("2.5.4.3", 1));
+  const unsigned char value[] = "value";
+  ASSERT_TRUE(name);
+  ASSERT_TRUE(oid);
+  ASSERT_EQ(X509_NAME_add_entry_by_OBJ(name.get(),
+                                       oid.get(),
+                                       V_ASN1_BIT_STRING,
+                                       value,
+                                       sizeof(value) - 1,
+                                       -1,
+                                       0),
+            1);
+
+  ncrypto::X509Name x509_name(name.get());
+  const auto entry = *x509_name.begin();
+  ASSERT_TRUE(entry.has_value());
+  EXPECT_EQ(entry->key, "CN");
+  EXPECT_FALSE(entry->value.has_value());
+}
+
+TEST(NodeCrypto, RejectsInvalidConvertibleX509NameValues) {
+  X509NamePointer name(X509_NAME_new());
+  ASN1ObjectPointer oid(OBJ_txt2obj("2.5.4.3", 1));
+  const unsigned char valid_value[] = "value";
+  const unsigned char invalid_value[] = {0xff};
+  ASSERT_TRUE(name);
+  ASSERT_TRUE(oid);
+  ASSERT_EQ(X509_NAME_add_entry_by_OBJ(name.get(),
+                                       oid.get(),
+                                       V_ASN1_UTF8STRING,
+                                       valid_value,
+                                       sizeof(valid_value) - 1,
+                                       -1,
+                                       0),
+            1);
+  // Intentionally corrupt the otherwise valid entry after construction.
+  auto* entry =
+      const_cast<X509_NAME_ENTRY*>(X509_NAME_get_entry(name.get(), 0));
+  ASSERT_NE(entry, nullptr);
+  auto* value = const_cast<ASN1_STRING*>(X509_NAME_ENTRY_get_data(entry));
+  ASSERT_NE(value, nullptr);
+  ASSERT_EQ(ASN1_STRING_set(value, invalid_value, sizeof(invalid_value)), 1);
+
+  ncrypto::X509Name x509_name(name.get());
+  EXPECT_FALSE((*x509_name.begin()).has_value());
+}
+
+TEST(NodeCrypto, RejectsEmptyX509SubjectAltNames) {
+  GeneralNamesPointer names(sk_GENERAL_NAME_new_null());
+  ASSERT_TRUE(names);
+
+  X509ExtensionPointer san(
+      X509V3_EXT_i2d(NID_subject_alt_name, 0, names.get()));
+  ASSERT_TRUE(san);
+  ncrypto::BIOPointer output = ncrypto::BIOPointer::NewMem();
+  ASSERT_TRUE(output);
+  EXPECT_FALSE(ncrypto::SafeX509SubjectAltNamePrint(output, san.get()));
+  EXPECT_TRUE(BioContents(output).empty());
+}
+
+TEST(NodeCrypto, RejectsEmptyX509AuthorityInfoAccess) {
+  AccessDescriptionsPointer descriptions(sk_ACCESS_DESCRIPTION_new_null());
+  ASSERT_TRUE(descriptions);
+
+  X509ExtensionPointer info_access(
+      X509V3_EXT_i2d(NID_info_access, 0, descriptions.get()));
+  ASSERT_TRUE(info_access);
+  ncrypto::BIOPointer output = ncrypto::BIOPointer::NewMem();
+  ASSERT_TRUE(output);
+  EXPECT_FALSE(ncrypto::SafeX509InfoAccessPrint(output, info_access.get()));
+  EXPECT_TRUE(BioContents(output).empty());
+}
+
+TEST(NodeCrypto, PrintsLongX509ExtensionObjectIdentifiers) {
+  const std::string oid_text = LongObjectIdentifier();
+  ASSERT_GT(oid_text.size(), 256U);
+
+  GeneralNamesPointer names(sk_GENERAL_NAME_new_null());
+  GeneralNamePointer name(GENERAL_NAME_new());
+  ASN1ObjectPointer oid(OBJ_txt2obj(oid_text.c_str(), 1));
+  ASSERT_TRUE(names);
+  ASSERT_TRUE(name);
+  ASSERT_TRUE(oid);
+  GENERAL_NAME_set0_value(name.get(), GEN_RID, oid.release());
+  ASSERT_GT(sk_GENERAL_NAME_push(names.get(), name.get()), 0);
+  name.release();
+
+  X509ExtensionPointer san(
+      X509V3_EXT_i2d(NID_subject_alt_name, 0, names.get()));
+  ASSERT_TRUE(san);
+  ncrypto::BIOPointer output = ncrypto::BIOPointer::NewMem();
+  ASSERT_TRUE(output);
+  ASSERT_TRUE(ncrypto::SafeX509SubjectAltNamePrint(output, san.get()));
+  EXPECT_EQ(BioContents(output), "Registered ID:" + oid_text);
+
+  AccessDescriptionsPointer descriptions(sk_ACCESS_DESCRIPTION_new_null());
+  AccessDescriptionPointer description(ACCESS_DESCRIPTION_new());
+  ASN1ObjectPointer method(OBJ_txt2obj(oid_text.c_str(), 1));
+  ASN1StringPointer uri(ASN1_IA5STRING_new());
+  ASSERT_TRUE(descriptions);
+  ASSERT_TRUE(description);
+  ASSERT_TRUE(method);
+  ASSERT_TRUE(uri);
+  ASSERT_EQ(ASN1_STRING_set(uri.get(), "https://example.com/", -1), 1);
+  ASN1_OBJECT_free(description->method);
+  description->method = method.release();
+  GENERAL_NAME_set0_value(description->location, GEN_URI, uri.release());
+  ASSERT_GT(sk_ACCESS_DESCRIPTION_push(descriptions.get(), description.get()),
+            0);
+  description.release();
+
+  X509ExtensionPointer info_access(
+      X509V3_EXT_i2d(NID_info_access, 0, descriptions.get()));
+  ASSERT_TRUE(info_access);
+  output = ncrypto::BIOPointer::NewMem();
+  ASSERT_TRUE(output);
+  ASSERT_TRUE(ncrypto::SafeX509InfoAccessPrint(output, info_access.get()));
+  std::string expected = oid_text + " - URI:https://example.com/";
+#if NCRYPTO_USE_LEGACY_OPENSSL || NCRYPTO_USE_BORINGSSL
+  expected += "\n";
+#endif
+  EXPECT_EQ(BioContents(output), expected);
+}
diff --git a/test/fixtures/x509-name-non-string.pem b/test/fixtures/x509-name-non-string.pem
new file mode 100644
index 00000000000..b98b3dea20f
--- /dev/null
+++ b/test/fixtures/x509-name-non-string.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDmzCCAoOgAwIBAgIUfORlbGg63wqnZ55UwzaMxzpE32AwDQYJKoZIhvcNAQEL
+BQAwXTERMA8GA1UEAwMIAEFBQUFBQUExFTATBgNVBAMMDGdvb2QuZXhhbXBsZTER
+MA8GA1UEAwMIAEJCQkJCQkIxDDAKBgNVBAswAwwBQTEQMA4GA1UECgwHR29vZE9y
+ZzAeFw0yNjA4MTgyMzUzMjlaFw0zNjA4MTUyMzUzMjlaMF0xETAPBgNVBAMDCABB
+QUFBQUFBMRUwEwYDVQQDDAxnb29kLmV4YW1wbGUxETAPBgNVBAMDCABCQkJCQkJC
+MQwwCgYDVQQLMAMMAUExEDAOBgNVBAoMB0dvb2RPcmcwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQDUVjIK+yDTgnCT3CxChO0E37q9VuHdrlKeKLeQzUJW
+2yczSfNzX/0zfHpjY+zKWie39z3HCJqWxtiG2wxiOI8c3WqWOvzVmdWADlh6EfkI
+lg+E7VC6JaKDA+zabmhPvnuu3JzogBMnsWl68lCXzuPxdeQAmEwNtqjrh74DtM+U
+d0ulb//Ixjxo1q3rYKu+aaexSramuee6qJta2rjrB4l8B/bU+j1mDf9XQQfSjo9j
+Rnp4hiTFdBl2k+lZzqE2L/rhu6EMjA2IhAq/7xA2MbLo9cObVUin6lfoo5+JKRgT
+9Fp2xEgDOit+2EA/S6oUfPNeLSVUqmXOSWlXlwlb9NxrAgMBAAGjUzBRMB0GA1Ud
+DgQWBBRbCoHeIZEQyF+yO8pC3wbV417ViDAfBgNVHSMEGDAWgBRbCoHeIZEQyF+y
+O8pC3wbV417ViDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCP
+brsAbA40Vmrxt6uVwNxShJlLkqkWUrzXuxjTCi4cQlNXsfR6484Z1wtbITZut+L6
+zht4sxJHLeS0elQAPhht+Z8btJzpYWZ2Y2G+kJFYBhqYTU0F4wB+SG7GsMGbSkpt
+WVCCBLNORHkF5bMds+WC6EJSm4M9sbklhWst1Bdk6Xury0VYU27j8qi+6u7WY80A
+igCpN92H7//gGwYjQBRnycaZUZY/Is/HyOYZqfgoMFjcZLkMOKPtfV5l5QnZPq2l
+56nktK17u2+JVL2n+wc6uyj1L5oPzw1CAmD8Y0N0guDdcT2l6gdc3Og7+1hsqcMf
+hvCKJif2j3L4cuK2J1oN
+-----END CERTIFICATE-----
diff --git a/test/parallel/test-crypto-x509-name-non-string.js b/test/parallel/test-crypto-x509-name-non-string.js
new file mode 100644
index 00000000000..0551163b8e3
--- /dev/null
+++ b/test/parallel/test-crypto-x509-name-non-string.js
@@ -0,0 +1,82 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto) {
+  common.skip('missing crypto');
+}
+
+const assert = require('node:assert');
+const { X509Certificate } = require('node:crypto');
+const fixtures = require('../common/fixtures');
+const tls = require('node:tls');
+
+// The certificate has BIT STRING CN entries before and after its valid CN,
+// and a SEQUENCE OU entry. OpenSSL accepts the certificate but cannot convert
+// those name values to UTF-8.
+const cert = fixtures.readSync('x509-name-non-string.pem');
+const key = fixtures.readKey('agent1-key.pem');
+
+function checkName(name) {
+  assert.strictEqual(name.CN, 'good.example');
+  assert.strictEqual(typeof name.CN, 'string');
+  assert.strictEqual(name.O, 'GoodOrg');
+  assert.strictEqual(Object.hasOwn(name, 'OU'), false);
+  assert.strictEqual(Object.hasOwn(name, ''), false);
+}
+
+const legacy = new X509Certificate(cert).toLegacyObject();
+checkName(legacy.subject);
+checkName(legacy.issuer);
+
+const server = tls.createServer({
+  key: fixtures.readKey('agent2-key.pem'),
+  cert: fixtures.readKey('agent2-cert.pem'),
+  requestCert: true,
+  rejectUnauthorized: false,
+}, common.mustCall((socket) => {
+  // Exercise the legacy TLS conversion, including detailed chain traversal.
+  const detailed = socket.getPeerCertificate(true);
+  checkName(detailed.subject);
+  checkName(detailed.issuer);
+
+  // The X509Certificate path defers name conversion until legacy inspection.
+  const peer = socket.getPeerX509Certificate();
+  assert(peer instanceof X509Certificate);
+  const peerLegacy = peer.toLegacyObject();
+  checkName(peerLegacy.subject);
+  checkName(peerLegacy.issuer);
+  socket.end();
+}));
+
+server.listen(0, common.mustCall(() => {
+  const client = tls.connect({
+    port: server.address().port,
+    cert,
+    key,
+    rejectUnauthorized: false,
+  }, common.mustCall(() => client.end()));
+
+  client.on('close', common.mustCall(() => server.close()));
+}));
+
+// Exercise the implicit getPeerCertificate(true) call made by tls.connect()
+// before checkServerIdentity(). A peer-controlled name value must not escape
+// from the connection callback as an uncaught exception.
+const malformedServer = tls.createServer({ key, cert }, (socket) => {
+  socket.end();
+});
+
+malformedServer.listen(0, common.mustCall(() => {
+  const client = tls.connect({
+    port: malformedServer.address().port,
+    ca: cert,
+    servername: 'good.example',
+  }, common.mustCall(() => {
+    const detailed = client.getPeerCertificate(true);
+    checkName(detailed.subject);
+    checkName(detailed.issuer);
+    client.end();
+  }));
+
+  client.on('close', common.mustCall(() => malformedServer.close()));
+}));
diff --git a/test/parallel/test-crypto-x509.js b/test/parallel/test-crypto-x509.js
index c2603354897..3f58a6cf0cc 100644
--- a/test/parallel/test-crypto-x509.js
+++ b/test/parallel/test-crypto-x509.js
@@ -18,9 +18,11 @@ const {
 } = require('internal/crypto/x509');

 const assert = require('assert');
+const { spawnSync } = require('node:child_process');
 const fixtures = require('../common/fixtures');
 const { hasOpenSSL, isBoringSSL } = require('../common/crypto');
 const { readFileSync } = require('fs');
+const { checkServerIdentity } = require('tls');

 const cert = readFileSync(fixtures.path('keys', 'agent1-cert.pem'));
 const key = readFileSync(fixtures.path('keys', 'agent1-key.pem'));
@@ -122,6 +124,25 @@ const der = Buffer.from(
   '59c19ad1a00732e7adc851b89487cc00799dde068aa64b3b8fd976e8bc113ef2',
   'hex');

+for (const [commonName, property] of [
+  ['06035504030c03636131', 'issuer'],
+  ['06035504030c066167656e7431', 'subject'],
+]) {
+  const malformedNameDer = Buffer.from(der);
+  const commonNameEntry = Buffer.from(commonName, 'hex');
+  const commonNameOffset = malformedNameDer.indexOf(commonNameEntry);
+  assert.notStrictEqual(commonNameOffset, -1);
+
+  // Replace an issuer or subject commonName's UTF8String with a validly
+  // encoded BIT STRING, which ASN1_STRING_to_UTF8() cannot convert.
+  malformedNameDer[commonNameOffset + 5] = 0x03;
+  malformedNameDer[commonNameOffset + 7] = 0x00;
+
+  const malformedNameCert = new X509Certificate(malformedNameDer);
+  const legacy = malformedNameCert.toLegacyObject();
+  assert.strictEqual(Object.hasOwn(legacy[property], 'CN'), false);
+}
+
 {
   const x509 = new X509Certificate(cert);

@@ -197,6 +218,12 @@ const der = Buffer.from(
   assert.throws(() => x509.checkIP('[::]'), {
     code: 'ERR_INVALID_ARG_VALUE'
   });
+  assert.throws(() => x509.checkIP('127.0.0.1\x00.example'), {
+    code: 'ERR_INVALID_ARG_VALUE'
+  });
+  assert.throws(() => x509.checkIP('fe80::1%eth0'), {
+    code: 'ERR_INVALID_ARG_VALUE'
+  });
   assert.throws(() => x509.checkEmail('not\x00hing'), {
     code: 'ERR_INVALID_ARG_VALUE'
   });
@@ -381,6 +408,63 @@ oans248kpal88CGqsN2so/wZKxVnpiXlPHMdiNL7hRSUqlHkUi07FrP2Htg8kjI=
     legacyObjectCheck.serialNumberPattern);
 }

+if (!process.features.openssl_is_boringssl) {
+  // BoringSSL rejects certificates with malformed times during parsing. The
+  // formatter fallback is covered for all backends by the ncrypto cctest.
+  // ASN1_TIME_print() documents "Bad time value" as the representation of an
+  // invalid time. Ensure that detecting output errors does not discard it.
+  const invalidTimeDer = Buffer.from(der);
+  const validFromOffset = invalidTimeDer.indexOf('220903214037Z');
+  assert.notStrictEqual(validFromOffset, -1);
+  invalidTimeDer[validFromOffset + 2] = 0x31;
+  invalidTimeDer[validFromOffset + 3] = 0x33;
+
+  const invalidTimeCert = new X509Certificate(invalidTimeDer);
+  assert.strictEqual(invalidTimeCert.validFrom, 'Bad time value');
+  assert(Number.isNaN(invalidTimeCert.validFromDate.getTime()));
+  assert.strictEqual(
+    invalidTimeCert.toLegacyObject().valid_from,
+    'Bad time value');
+}
+
+{
+  // A malformed, present SAN must not look like an absent SAN in the legacy
+  // certificate object. Otherwise hostname verification can fall back to CN.
+  const sanCert = new X509Certificate(
+    readFileSync(fixtures.path('keys',
+                               'incorrect_san_correct_subject-cert.pem')));
+  const malformedSanDer = Buffer.from(sanCert.raw);
+  const sanSequence = Buffer.from('0603551d11041430', 'hex');
+  const sanOffset = malformedSanDer.indexOf(sanSequence);
+  assert.notStrictEqual(sanOffset, -1);
+  malformedSanDer[sanOffset + sanSequence.length - 1] = 0x31;
+
+  const malformedSanCert = new X509Certificate(malformedSanDer);
+  const expectedError = { code: 'ERR_CRYPTO_OPERATION_FAILED' };
+  assert.throws(() => malformedSanCert.subjectAltName, expectedError);
+
+  const legacyObject = malformedSanCert.toLegacyObject();
+  assert.strictEqual(legacyObject.subjectaltname, 'DNS:');
+  assert.strictEqual(
+    checkServerIdentity(legacyObject.subject.CN, legacyObject).code,
+    'ERR_TLS_CERT_ALTNAME_INVALID');
+
+  // GeneralNames is defined as a nonempty sequence. Some backends accept an
+  // empty sequence and ignore the remaining extension bytes, which must not
+  // make a present SAN look absent and permit commonName fallback.
+  const emptySanDer = Buffer.from(sanCert.raw);
+  emptySanDer[sanOffset + sanSequence.length] = 0;
+  const emptySanCert = new X509Certificate(emptySanDer);
+  assert.throws(() => emptySanCert.subjectAltName, expectedError);
+
+  const emptySanLegacyObject = emptySanCert.toLegacyObject();
+  assert.strictEqual(emptySanLegacyObject.subjectaltname, 'DNS:');
+  assert.strictEqual(
+    checkServerIdentity(
+      emptySanLegacyObject.subject.CN, emptySanLegacyObject).code,
+    'ERR_TLS_CERT_ALTNAME_INVALID');
+}
+
 {
   // This X.509 Certificate can be parsed by OpenSSL because it contains a
   // structurally sound TBSCertificate structure. However, the SPKI field of the
@@ -413,6 +497,29 @@ UcXd/5qu2GhokrKU2cPttU+XAN2Om6a0

   if (!isBoringSSL) {
     const cert = new X509Certificate(certPem);
+
+    // Run the legacy conversion in a child so a native regression is reported
+    // as a signal rather than terminating the test runner.
+    const child = spawnSync(process.execPath, [
+      '-e',
+      `
+        'use strict';
+        const assert = require('node:assert');
+        const { X509Certificate } = require('node:crypto');
+        const cert = new X509Certificate(process.argv[1]);
+        const legacy = cert.toLegacyObject();
+        assert.strictEqual(legacy.modulus, undefined);
+        assert.strictEqual(legacy.exponent, undefined);
+        assert.strictEqual(legacy.pubkey, undefined);
+      `,
+      '--',
+      certPem,
+    ], {
+      encoding: 'utf8',
+    });
+    assert.strictEqual(child.signal, null, child.stderr || child.stdout);
+    assert.strictEqual(child.status, 0, child.stderr || child.stdout);
+
     assert.throws(() => cert.publicKey, {
       message: hasOpenSSL(3) ? /decode error/ : /wrong tag/,
       name: 'Error'
@@ -533,3 +640,28 @@ jEVmV959vS0DtFIIsrGT3moCIBBRZWdLdd0YjFHUQshMZFM9BN2INpjFNUUxqPAC
   assert.ok([undefined, 'brainpoolP256r1'].includes(legacyObject.asn1Curve));
   assert.strictEqual(legacyObject.nistCurve, undefined);
 }
+
+if (!process.features.openssl_is_boringssl) {
+  const certPem = `-----BEGIN CERTIFICATE-----
+MIIBfTCCASSgAwIBAgIUFOenmg/rEU9h3dVqmBzP84ckpbQwCgYIKoZIzj0EAwIw
+FDESMBAGA1UEAwwJYnJhaW5wb29sMB4XDTI2MDYzMDA5MDcwOFoXDTI2MDcwMTA5
+MDcwOFowFDESMBAGA1UEAwwJYnJhaW5wb29sMFowFAYHKoZIzj0CAQYJKyQDAwII
+AQEHA0IABDhDrr79NVJtx/sOq9RiCoGWvuiUucpciBG7gET/H9gDKrlMsftC5KZb
+vlseseOxYj8ZCNXmTaZTlC4Wz9JbEeqjUzBRMB0GA1UdDgQWBBSQRbtKt7fEeMAw
+3jQGNmxiAS/GozAfBgNVHSMEGDAWgBSQRbtKt7fEeMAw3jQGNmxiAS/GozAPBgNV
+HRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIC76KvKG0WuIC69kvqgqwtnJ
+sMTBCid+5zWH7ZLmWxeVAiAnZF9MSUgCGN7Nw4L7uo63vacwZQrNN3vOS/F7eqUY
+wg==
+-----END CERTIFICATE-----`;
+
+  const cert = new X509Certificate(certPem);
+  const legacyObject = cert.toLegacyObject();
+
+  assert.strictEqual(legacyObject.subject.CN, 'brainpool');
+  assert.strictEqual(legacyObject.nistCurve, undefined);
+  // This can either be resolved correctly or not (and not crash)
+  // depending on the OpenSSL version and FIPS state
+  assert.ok(legacyObject.pubkey === undefined || Buffer.isBuffer(legacyObject.pubkey));
+  assert.ok([undefined, 'brainpoolP256r1'].includes(legacyObject.asn1Curve));
+  assert.ok([undefined, 256].includes(legacyObject.bits));
+}
diff --git a/test/parallel/test-tls-cert-ext-encoding.js b/test/parallel/test-tls-cert-ext-encoding.js
index 973e9fad32e..4c6e59a9638 100644
--- a/test/parallel/test-tls-cert-ext-encoding.js
+++ b/test/parallel/test-tls-cert-ext-encoding.js
@@ -3,17 +3,7 @@ const common = require('../common');
 if (!common.hasCrypto)
   common.skip('missing crypto');

-const { hasOpenSSL } = require('../common/crypto');
-
-if (hasOpenSSL(3))
-  // TODO(danbev) This test fails with the following error:
-  // error:0D00008F:asn1 encoding routines::no matching choice type
-  //
-  // I've not been able to figure out the reason for this but there
-  // is a note in https://wiki.openssl.org/index.php/OpenSSL_3.0 which
-  // indicates that this might not work at the moment:
-  // "OCSP, PEM, ASN.1 have some very limited library context support"
-  common.skip('when using OpenSSL 3.x');
+const assert = require('assert');

 // NOTE: This certificate is hand-generated, hence it is not located in
 // `test/fixtures/keys` to avoid confusion.
@@ -82,8 +72,9 @@ server.listen(0, common.mustCall(function() {
     port: this.address().port,
     rejectUnauthorized: false
   }, common.mustCall(() => {
-    // This should not crash process:
-    client.getPeerCertificate();
+    // A malformed, present SAN uses a nonmatching legacy sentinel. This keeps
+    // certificate inspection usable without allowing fallback to commonName.
+    assert.strictEqual(client.getPeerCertificate().subjectaltname, 'DNS:');

     server.close();
     client.end();