Commit e86ce8b9758 for nodejs

commit e86ce8b97584e2a3516cf1fec7dcc74c38f1d17d
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Wed Nov 4 10:20:53 2026 +0100

    crypto: add X509Certificate inspection getters

    Expose version, subjectName, issuerName, extensions, tbs, and signature
    without requiring callers to parse display strings or certificate DER.

    Names include complete DER and grouped, ordered RDN attributes.
    Extensions retain their order, duplicate and unknown OIDs, criticality,
    and raw values. Expose the exact signed TBSCertificate bytes and the
    signature octets, algorithm OID, and DER parameters.

    Return fresh objects, arrays, and Buffers from the new getters, leaving
    existing getter caching unchanged. Report inspection failures as
    ERR_CRYPTO_INVALID_X509_CERTIFICATE.

    Fixes: https://github.com/nodejs/node/issues/48730
    Assisted-by: Codex
    Signed-off-by: Filip Skokan <panva.ip@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/67082
    Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
    Reviewed-By: Tim Perry <pimterry@gmail.com>

diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc
index 025857fd541..3991235fb29 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -46,6 +46,15 @@ using BignumCtxPointer = DeleteFnPtr<BN_CTX, BN_CTX_free>;
 using BignumGenCallbackPointer = DeleteFnPtr<BN_GENCB, BN_GENCB_free>;
 using NetscapeSPKIPointer = DeleteFnPtr<NETSCAPE_SPKI, NETSCAPE_SPKI_free>;

+struct ASN1SequenceDeleter final {
+  void operator()(ASN1_SEQUENCE_ANY* sequence) const {
+    sk_ASN1_TYPE_pop_free(sequence, ASN1_TYPE_free);
+  }
+};
+
+using ASN1SequencePointer =
+    std::unique_ptr<ASN1_SEQUENCE_ANY, ASN1SequenceDeleter>;
+
 #if NCRYPTO_USE_OPENSSL_PROVIDER
 using X509PubKeyPointer = DeleteFnPtr<X509_PUBKEY, X509_PUBKEY_free>;
 // OSSL_STORE_close() returns int, so it needs a void-returning adapter to be
@@ -1606,6 +1615,164 @@ bool X509View::isCA() const {
   return X509_check_ca(const_cast<X509*>(cert_)) == 1;
 }

+namespace {
+bool CopyStringBytes(const ASN1_STRING* value,
+                     std::vector<unsigned char>* output) {
+  if (value == nullptr) return false;
+  const int length = ASN1_STRING_length(value);
+  const unsigned char* data = ASN1_STRING_get0_data(value);
+  if (length < 0 || (length > 0 && data == nullptr)) return false;
+  if (length == 0) {
+    output->clear();
+  } else {
+    output->assign(data, data + static_cast<size_t>(length));
+  }
+  return true;
+}
+
+bool CanConvertNameValueToUtf8(const ASN1_STRING* value) {
+  if (value == nullptr) return false;
+  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:
+      return true;
+    default:
+      return false;
+  }
+}
+
+std::optional<size_t> GetEncodedRdnCount(const X509_NAME* name) {
+  const unsigned char* der = nullptr;
+  size_t der_length = 0;
+  if (X509_NAME_get0_der(name, &der, &der_length) != 1 || der == nullptr ||
+      der_length > static_cast<size_t>(LONG_MAX)) {
+    return std::nullopt;
+  }
+
+  const unsigned char* cursor = der;
+  ASN1SequencePointer rdns(d2i_ASN1_SEQUENCE_ANY(
+      nullptr, &cursor, static_cast<long>(der_length)));  // NOLINT(runtime/int)
+  if (!rdns || cursor != der + der_length) return std::nullopt;
+
+  const OPENSSL_SIZE_T count = sk_ASN1_TYPE_num(rdns.get());
+  for (OPENSSL_SIZE_T i = 0; i < count; i++) {
+    const ASN1_TYPE* rdn = sk_ASN1_TYPE_value(rdns.get(), i);
+    if (rdn == nullptr || ASN1_TYPE_get(rdn) != V_ASN1_SET) {
+      return std::nullopt;
+    }
+  }
+  return static_cast<size_t>(count);
+}
+
+std::optional<X509NameInspection> InspectName(const X509_NAME* name) {
+  if (name == nullptr) return std::nullopt;
+  const int count = X509_NAME_entry_count(name);
+  if (count < 0) return std::nullopt;
+  const auto encoded_rdn_count = GetEncodedRdnCount(name);
+  if (!encoded_rdn_count.has_value()) return std::nullopt;
+
+  X509NameInspection output;
+  output.reserve(static_cast<size_t>(count));
+  int previous_set = -1;
+  for (int i = 0; i < count; i++) {
+    const X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, i);
+    if (entry == nullptr) return std::nullopt;
+
+    const ASN1_OBJECT* object = X509_NAME_ENTRY_get_object(entry);
+    const ASN1_STRING* value = X509_NAME_ENTRY_get_data(entry);
+    auto oid = ObjectToText(object, true);
+    const int set = X509_NAME_ENTRY_set(entry);
+    if (!oid.has_value() || value == nullptr || set < 0 ||
+        (set != previous_set &&
+         (previous_set == INT_MAX || set != previous_set + 1))) {
+      return std::nullopt;
+    }
+    previous_set = set;
+
+    X509NameAttribute attribute{
+        .set = set,
+        .oid = std::move(oid.value()),
+    };
+    if (CanConvertNameValueToUtf8(value)) {
+      unsigned char* data = nullptr;
+      const int length = ASN1_STRING_to_UTF8(&data, value);
+      DataPointer owned(data, length > 0 ? static_cast<size_t>(length) : 0);
+      if (length < 0 || (length > 0 && !owned)) return std::nullopt;
+      attribute.value =
+          length == 0 ? std::string()
+                      : std::string(reinterpret_cast<const char*>(owned.get()),
+                                    static_cast<size_t>(length));
+    }
+    output.push_back(std::move(attribute));
+  }
+  const size_t inspected_rdn_count =
+      previous_set < 0 ? 0 : static_cast<size_t>(previous_set) + 1;
+  if (inspected_rdn_count != encoded_rdn_count.value()) return std::nullopt;
+  return output;
+}
+
+std::optional<std::vector<X509RawExtension>> InspectExtensions(
+    const X509* certificate) {
+  if (certificate == nullptr) return std::nullopt;
+  const int count = X509_get_ext_count(certificate);
+  if (count < 0) return std::nullopt;
+
+  std::vector<X509RawExtension> output;
+  output.reserve(static_cast<size_t>(count));
+  for (int i = 0; i < count; i++) {
+    const X509_EXTENSION* extension = X509_get_ext(certificate, i);
+    if (extension == nullptr) return std::nullopt;
+
+    auto* mutable_extension = const_cast<X509_EXTENSION*>(extension);
+    auto oid = ObjectToText(X509_EXTENSION_get_object(mutable_extension), true);
+    const ASN1_OCTET_STRING* value = X509_EXTENSION_get_data(mutable_extension);
+    if (!oid.has_value() || value == nullptr) return std::nullopt;
+
+    X509RawExtension inspected{
+        .oid = std::move(oid.value()),
+        .critical = X509_EXTENSION_get_critical(mutable_extension) != 0,
+    };
+    if (!CopyStringBytes(value, &inspected.value)) return std::nullopt;
+    output.push_back(std::move(inspected));
+  }
+  return output;
+}
+}  // namespace
+
+std::optional<int64_t> X509View::getVersion() const {
+  MarkPopErrorOnReturn mark_pop_error_on_return;
+  if (cert_ == nullptr) return std::nullopt;
+  const long version = X509_get_version(cert_);  // NOLINT(runtime/int)
+  if (version < 0 || version > 2) return std::nullopt;
+  return static_cast<int64_t>(version) + 1;
+}
+
+std::optional<X509NameInspection> X509View::inspectSubjectName() const {
+  MarkPopErrorOnReturn mark_pop_error_on_return;
+  if (cert_ == nullptr) return std::nullopt;
+  return InspectName(X509_get_subject_name(cert_));
+}
+
+std::optional<X509NameInspection> X509View::inspectIssuerName() const {
+  MarkPopErrorOnReturn mark_pop_error_on_return;
+  if (cert_ == nullptr) return std::nullopt;
+  return InspectName(X509_get_issuer_name(cert_));
+}
+
+std::optional<std::vector<X509RawExtension>> X509View::inspectExtensions()
+    const {
+  MarkPopErrorOnReturn mark_pop_error_on_return;
+  return InspectExtensions(cert_);
+}
+
 bool X509View::isIssuedBy(const X509View& issuer) const {
   MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr || issuer.cert_ == nullptr) return false;
diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h
index afb780da7cc..a9468655aa6 100644
--- a/deps/ncrypto/ncrypto.h
+++ b/deps/ncrypto/ncrypto.h
@@ -61,6 +61,15 @@

 #define NCRYPTO_USE_LEGACY_KEY_TYPES NCRYPTO_USE_BORINGSSL

+// OpenSSL 4 makes ASN1_STRING opaque and provides a checked BIT STRING length
+// accessor. Earlier OpenSSL releases and pinned BoringSSL expose the decoded
+// unused-bit count through ASN1_STRING::flags.
+#if !NCRYPTO_USE_BORINGSSL && OPENSSL_VERSION_PREREQ(4, 0)
+#define NCRYPTO_HAS_ASN1_BIT_STRING_GET_LENGTH 1
+#else
+#define NCRYPTO_HAS_ASN1_BIT_STRING_GET_LENGTH 0
+#endif
+
 #if NCRYPTO_USE_OPENSSL_PROVIDER
 #include <openssl/core_names.h>
 #include <openssl/encoder.h>
@@ -1648,6 +1657,20 @@ class X509Name final {
   int total_;
 };

+struct X509NameAttribute final {
+  int set = 0;
+  std::string oid;
+  std::optional<std::string> value;
+};
+
+using X509NameInspection = std::vector<X509NameAttribute>;
+
+struct X509RawExtension final {
+  std::string oid;
+  bool critical = false;
+  std::vector<unsigned char> value;
+};
+
 class X509View final {
  public:
   static X509View From(const SSLPointer& ssl);
@@ -1685,6 +1708,11 @@ class X509View final {
   Result<EVPKeyPointer, int> getPublicKey() const;
   StackOfASN1 getKeyUsage() const;

+  std::optional<int64_t> getVersion() const;
+  std::optional<X509NameInspection> inspectSubjectName() const;
+  std::optional<X509NameInspection> inspectIssuerName() const;
+  std::optional<std::vector<X509RawExtension>> inspectExtensions() const;
+
   bool isCA() const;
   bool isIssuedBy(const X509View& other) const;
   bool checkPrivateKey(const EVPKeyPointer& pkey) const;
diff --git a/doc/api/crypto.md b/doc/api/crypto.md
index 2485f1935f8..b14c4876884 100644
--- a/doc/api/crypto.md
+++ b/doc/api/crypto.md
@@ -3152,6 +3152,47 @@ added: v15.6.0
 Checks whether the public key for this certificate is consistent with
 the given private key.

+### `x509.extensions`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* Type: {Object\[]}
+  * `oid` {string} The canonical dotted-decimal extension OID.
+  * `critical` {boolean} Whether the extension is critical.
+  * `value` {Buffer} The bytes contained in the extension's `extnValue` OCTET
+    STRING. The OCTET STRING tag and length are not included.
+
+One entry is returned for every extension, in the order in which it appears in
+the certificate. Unknown extensions and repeated OIDs are included. The
+contents of `value` are not decoded according to `oid`. A certificate with no
+extensions produces an empty array.
+
+Unlike [`x509.subjectAltName`][], [`x509.infoAccess`][], and
+[`x509.keyUsage`][] (Extended Key Usage), which decode or format selected
+extension types, this property exposes raw extension contents. `value` contains
+the exact extension value bytes, not a complete encoded `Extension` record.
+The extension-specific syntax is not validated.
+
+Each access returns a new array containing new objects and `Buffer`s. Modifying
+them does not affect the certificate or later accesses.
+
+For example, a Basic Constraints extension allowing a certificate to act as a
+CA can appear as:
+
+```js
+console.log(x509.extensions[0]);
+// {
+//   oid: '2.5.29.19',
+//   critical: true,
+//   value: <Buffer 30 03 01 01 ff>
+// }
+```
+
+Accessing this property throws `ERR_CRYPTO_INVALID_X509_CERTIFICATE` if the
+extensions cannot be inspected.
+
 ### `x509.fingerprint`

 <!-- YAML
@@ -3243,6 +3284,46 @@ added: v15.9.0
 The issuer certificate or `undefined` if the issuer certificate is not
 available.

+### `x509.issuerName`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* Type: {Object}
+  * `raw` {Buffer} The complete DER-encoded `Name`.
+  * `rdns` {Array\<Object\[]>} The relative distinguished name sequence.
+    * `oid` {string} The canonical dotted-decimal object identifier.
+    * `value` {string} A UTF-8 representation of the attribute value. This
+      property is omitted when the ASN.1 value type is not supported for text
+      conversion.
+
+The issuer distinguished name, also exposed as a formatted string by
+[`x509.issuer`][]. The outer `rdns` array contains the relative distinguished
+names in encoded order. Each inner array represents one relative distinguished
+name and contains its attributes in encoded order. A valid empty name has an
+empty `rdns` array.
+
+`rdns` is a decoded view and does not include ASN.1 value types. Use `raw` for a
+lossless representation of the complete name, including attribute values whose
+`value` property is omitted from `rdns`.
+
+Each access returns newly allocated objects, arrays, and a `Buffer`.
+Modifying them does not affect the certificate or later accesses.
+
+For example:
+
+```js
+console.log(x509.issuerName.rdns);
+// [
+//   [ { oid: '2.5.4.6', value: 'US' } ],
+//   [ { oid: '2.5.4.10', value: 'Example CA' } ]
+// ]
+```
+
+Accessing this property throws `ERR_CRYPTO_INVALID_X509_CERTIFICATE` if the
+issuer name cannot be inspected.
+
 ### `x509.keyUsage`

 <!-- YAML
@@ -3329,6 +3410,70 @@ For backward compatibility, Node.js only uses JSON string literals within this
 property when necessary to avoid ambiguity. Third-party code should be prepared
 to handle both possible entry formats.

+### `x509.subjectName`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* Type: {Object}
+  * `raw` {Buffer} The complete DER-encoded `Name`.
+  * `rdns` {Array\<Object\[]>} The relative distinguished name sequence.
+    * `oid` {string} The canonical dotted-decimal object identifier.
+    * `value` {string} A UTF-8 representation of the attribute value. This
+      property is omitted when the ASN.1 value type is not supported for text
+      conversion.
+
+The subject distinguished name, also exposed as a formatted string by
+[`x509.subject`][]. The outer `rdns` array contains the relative distinguished
+names in encoded order. Each inner array represents one relative distinguished
+name and contains its attributes in encoded order. A valid empty name has an
+empty `rdns` array.
+
+`rdns` is a decoded view and does not include ASN.1 value types. Use `raw` for a
+lossless representation of the complete name, including attribute values whose
+`value` property is omitted from `rdns`.
+
+Each access returns newly allocated objects, arrays, and a `Buffer`.
+Modifying them does not affect the certificate or later accesses.
+
+For example, attributes in the same inner array belong to one multi-valued
+relative distinguished name:
+
+```js
+console.log(x509.subjectName.rdns);
+// [
+//   [ { oid: '2.5.4.6', value: 'US' } ],
+//   [
+//     { oid: '2.5.4.11', value: 'Security' },
+//     { oid: '2.5.4.10', value: 'Example Corp' }
+//   ],
+//   [ { oid: '2.5.4.3', value: 'example.com' } ]
+// ]
+```
+
+Accessing this property throws `ERR_CRYPTO_INVALID_X509_CERTIFICATE` if the
+subject name cannot be inspected.
+
+### `x509.tbs`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* Type: {Buffer}
+
+Unlike [`x509.raw`][], which contains the whole certificate, this property
+contains only the complete encoded `TBSCertificate`: the exact bytes covered
+by the certificate's signature. They are copied from the certificate encoding,
+not reconstructed from decoded fields.
+
+Each access returns a new `Buffer`. Modifying it does not affect the certificate
+or later accesses.
+
+Accessing this property throws `ERR_CRYPTO_INVALID_X509_CERTIFICATE` if the
+`TBSCertificate` cannot be inspected.
+
 ### `x509.toJSON()`

 <!-- YAML
@@ -3406,6 +3551,50 @@ added:

 The date/time until which this certificate is valid, encapsulated in a `Date` object.

+### `x509.signature`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* Type: {Object}
+  * `algorithm` {Object} The certificate-level `signatureAlgorithm` paired with
+    the `signatureValue`.
+    * `oid` {string} The canonical dotted-decimal algorithm OID.
+    * `parameters` {Buffer} The complete DER-encoded ASN.1 parameters. This
+      property is omitted when the parameters are absent.
+  * `value` {Buffer} The signature octets contained in the certificate's
+    `signatureValue` BIT STRING.
+
+`algorithm.oid` identifies the same certificate-level algorithm as
+[`x509.signatureAlgorithmOid`][]. In addition to the algorithm identification
+provided by that property and [`x509.signatureAlgorithm`][], this property
+exposes the algorithm parameters and signature bytes.
+
+The parameters are DER-encoded independently; the complete encoded
+`AlgorithmIdentifier` and BIT STRING wrappers are not returned. Accessing this
+property does not verify the signature.
+
+Each access returns new outer and `algorithm` objects, with new `Buffer`s for
+`value` and `parameters` when present. Modifying them does not affect the
+certificate or later accesses.
+
+For example, a certificate signed using RSA with SHA-256 can produce:
+
+```js
+console.log(x509.signature);
+// {
+//   algorithm: {
+//     oid: '1.2.840.113549.1.1.11',
+//     parameters: <Buffer 05 00>
+//   },
+//   value: <Buffer ...>
+// }
+```
+
+Accessing this property throws `ERR_CRYPTO_INVALID_X509_CERTIFICATE` if the
+signature cannot be inspected.
+
 ### `x509.signatureAlgorithm`

 <!-- YAML
@@ -3426,6 +3615,17 @@ added: v24.9.0

 The OID of the algorithm used to sign the certificate.

+### `x509.version`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* Type: {number}
+
+The certificate version: `1` for X.509 v1, `2` for X.509 v2, or `3` for X.509
+v3. A certificate with no encoded version field has version `1`.
+
 ### `x509.verify(publicKey)`

 <!-- YAML
@@ -7637,6 +7837,14 @@ See the [list of SSL OP Flags][] for details.
 [`verify.update()`]: #verifyupdatedata-inputencoding
 [`verify.verify()`]: #verifyverifykey-signature-signatureencoding
 [`x509.fingerprint256`]: #x509fingerprint256
+[`x509.infoAccess`]: #x509infoaccess
+[`x509.issuer`]: #x509issuer
+[`x509.keyUsage`]: #x509keyusage
+[`x509.raw`]: #x509raw
+[`x509.signatureAlgorithmOid`]: #x509signaturealgorithmoid
+[`x509.signatureAlgorithm`]: #x509signaturealgorithm
+[`x509.subjectAltName`]: #x509subjectaltname
+[`x509.subject`]: #x509subject
 [`x509.verify(publicKey)`]: #x509verifypublickey
 [argon2]: https://www.rfc-editor.org/rfc/rfc9106.html
 [asymmetric key types]: #asymmetric-key-types
diff --git a/doc/api/errors.md b/doc/api/errors.md
index 2a1e423d10d..8be520b7e52 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1120,6 +1120,16 @@ added: v15.0.0

 An invalid authentication tag length was provided.

+<a id="ERR_CRYPTO_INVALID_X509_CERTIFICATE"></a>
+
+### `ERR_CRYPTO_INVALID_X509_CERTIFICATE`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+An X.509 certificate field could not be inspected.
+
 <a id="ERR_CRYPTO_JOB_INIT_FAILED"></a>

 ### `ERR_CRYPTO_JOB_INIT_FAILED`
diff --git a/lib/internal/crypto/x509.js b/lib/internal/crypto/x509.js
index 0a0da0296a8..30681da0b12 100644
--- a/lib/internal/crypto/x509.js
+++ b/lib/internal/crypto/x509.js
@@ -48,6 +48,7 @@ const {

 const {
   codes: {
+    ERR_CRYPTO_INVALID_X509_CERTIFICATE,
     ERR_INVALID_ARG_TYPE,
     ERR_INVALID_ARG_VALUE,
     ERR_INVALID_THIS,
@@ -135,22 +136,28 @@ const {
     fingerprint512: nativeFingerprint512,
     getIssuerCert: nativeGetIssuerCert,
     infoAccess: nativeInfoAccess,
+    issuerName: nativeIssuerName,
     issuer: nativeIssuer,
+    extensions: nativeExtensions,
     keyUsage: nativeKeyUsage,
     pem: nativePem,
     publicKey: nativePublicKey,
     raw: nativeRaw,
     serialNumber: nativeSerialNumber,
+    signature: nativeSignature,
     signatureAlgorithm: nativeSignatureAlgorithm,
     signatureAlgorithmOid: nativeSignatureAlgorithmOid,
     subject: nativeSubject,
+    subjectName: nativeSubjectName,
     subjectAltName: nativeSubjectAltName,
     toLegacy: nativeToLegacy,
     validFrom: nativeValidFrom,
     validFromDate: nativeValidFromDate,
     validTo: nativeValidTo,
     validToDate: nativeValidToDate,
+    version: nativeVersion,
     verify: nativeVerify,
+    tbs: nativeTbs,
   } = NativeX509Certificate.prototype;

   let getState;
@@ -165,6 +172,16 @@ const {
     return state[index];
   }

+  function getInspectionValue(certificate, getter) {
+    getState(certificate);
+    const value = FunctionPrototypeCall(getter, certificate);
+    if (value === undefined) {
+      throw new ERR_CRYPTO_INVALID_X509_CERTIFICATE(
+        'the certificate cannot be inspected');
+    }
+    return value;
+  }
+
   class X509Certificate {
     constructor(buffer) {
       if (typeof buffer === 'string')
@@ -296,6 +313,30 @@ const {
         this, kSlotSignatureAlgorithmOid, nativeSignatureAlgorithmOid);
     }

+    get version() {
+      return getInspectionValue(this, nativeVersion);
+    }
+
+    get subjectName() {
+      return getInspectionValue(this, nativeSubjectName);
+    }
+
+    get issuerName() {
+      return getInspectionValue(this, nativeIssuerName);
+    }
+
+    get extensions() {
+      return getInspectionValue(this, nativeExtensions);
+    }
+
+    get tbs() {
+      return getInspectionValue(this, nativeTbs);
+    }
+
+    get signature() {
+      return getInspectionValue(this, nativeSignature);
+    }
+
     get raw() {
       return getCachedValue(this, kSlotRaw, nativeRaw);
     }
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 88c4102eee4..e2ee2cdd340 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1194,6 +1194,8 @@ E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE',
   'Invalid key object type %s, expected %s.', TypeError);
 E('ERR_CRYPTO_INVALID_MAC', 'Invalid MAC: %s', TypeError);
 E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error);
+E('ERR_CRYPTO_INVALID_X509_CERTIFICATE',
+  'Invalid X.509 certificate: %s', Error);
 E('ERR_CRYPTO_KEM_NOT_SUPPORTED', 'KEM is not supported', Error);
 E('ERR_CRYPTO_MAC_FINALIZED', 'MAC already finalized', Error);
 E('ERR_CRYPTO_MAC_NOT_SUPPORTED', 'MAC is not supported', Error);
diff --git a/src/crypto/crypto_x509.cc b/src/crypto/crypto_x509.cc
index 3a9c296970a..37a190a2166 100644
--- a/src/crypto/crypto_x509.cc
+++ b/src/crypto/crypto_x509.cc
@@ -28,6 +28,7 @@ using ncrypto::Digest;
 using ncrypto::MarkPopErrorOnReturn;
 using ncrypto::SSLPointer;
 using ncrypto::X509Name;
+using ncrypto::X509NameInspection;
 using ncrypto::X509Pointer;
 using ncrypto::X509View;
 using v8::Array;
@@ -392,6 +393,380 @@ MaybeLocal<Value> GetKeyUsage(Environment* env, const X509View& cert) {
   return Array::New(env->isolate(), vec.data(), vec.size());
 }

+bool SetValue(Local<Context> context,
+              Local<Object> object,
+              Local<String> name,
+              Local<Value> value) {
+  return object->CreateDataProperty(context, name, value).FromMaybe(false);
+}
+
+bool SetArrayValue(Local<Context> context,
+                   Local<Array> array,
+                   uint32_t index,
+                   Local<Value> value) {
+  return array->CreateDataProperty(context, index, value).FromMaybe(false);
+}
+
+MaybeLocal<Value> CopyBuffer(Environment* env,
+                             ncrypto::Buffer<const unsigned char> value) {
+  const char* data =
+      value.len == 0 ? "" : reinterpret_cast<const char*>(value.data);
+  return Buffer::Copy(env, data, value.len);
+}
+
+MaybeLocal<Value> CopyBuffer(Environment* env,
+                             const std::vector<unsigned char>& value) {
+  return CopyBuffer(env, {.data = value.data(), .len = value.size()});
+}
+
+std::optional<std::string> OidToText(const ASN1_OBJECT* object) {
+  if (object == nullptr) return std::nullopt;
+  const int length = OBJ_obj2txt(nullptr, 0, object, 1);
+  if (length <= 0 || length == std::numeric_limits<int>::max()) {
+    return std::nullopt;
+  }
+  std::string output(static_cast<size_t>(length) + 1, '\0');
+  if (OBJ_obj2txt(output.data(), length + 1, object, 1) != length) {
+    return std::nullopt;
+  }
+  output.resize(static_cast<size_t>(length));
+  return output;
+}
+
+struct SequenceSpan final {
+  ncrypto::Buffer<const unsigned char> encoded;
+  ncrypto::Buffer<const unsigned char> content;
+};
+
+bool ReadSequence(ncrypto::Buffer<const unsigned char> input,
+                  SequenceSpan* out) {
+  if (out == nullptr || input.data == nullptr || input.len == 0 ||
+      input.len > static_cast<size_t>(LONG_MAX)) {
+    return false;
+  }
+
+  const unsigned char* const start = input.data;
+  const unsigned char* content = start;
+  long content_length = 0;  // NOLINT(runtime/int)
+  int tag = 0;
+  int tag_class = 0;
+  const int flags =
+      ASN1_get_object(&content,
+                      &content_length,
+                      &tag,
+                      &tag_class,
+                      static_cast<long>(input.len));  // NOLINT(runtime/int)
+  constexpr int kIndefiniteLength = 0x01;
+  constexpr int kDecodeError = 0x80;
+  if ((flags & (kIndefiniteLength | kDecodeError)) != 0 ||
+      (flags & V_ASN1_CONSTRUCTED) == 0 || tag != V_ASN1_SEQUENCE ||
+      tag_class != V_ASN1_UNIVERSAL || content_length < 0) {
+    return false;
+  }
+
+  const size_t header_length = static_cast<size_t>(content - start);
+  const size_t length = static_cast<size_t>(content_length);
+  if (header_length > input.len || length > input.len - header_length) {
+    return false;
+  }
+  *out = {
+      .encoded = {.data = start, .len = header_length + length},
+      .content = {.data = content, .len = length},
+  };
+  return true;
+}
+
+std::optional<ncrypto::Buffer<const unsigned char>> GetExactTbs(
+    const X509CertificateSnapshot::DER& certificate) {
+  SequenceSpan outer;
+  const ncrypto::Buffer<const unsigned char> encoded{
+      .data = certificate.data(),
+      .len = certificate.size(),
+  };
+  if (!ReadSequence(encoded, &outer) || outer.encoded.len != encoded.len) {
+    return std::nullopt;
+  }
+
+  SequenceSpan tbs;
+  if (!ReadSequence(outer.content, &tbs)) return std::nullopt;
+  return tbs.encoded;
+}
+
+bool GetNameDer(const X509_NAME* name,
+                ncrypto::Buffer<const unsigned char>* encoded) {
+  if (name == nullptr || encoded == nullptr) return false;
+  const unsigned char* data = nullptr;
+  size_t length = 0;
+  if (X509_NAME_get0_der(name, &data, &length) != 1 ||
+      (data == nullptr && length != 0)) {
+    return false;
+  }
+  *encoded = {.data = data, .len = length};
+  return true;
+}
+
+MaybeLocal<Value> CreateName(Environment* env,
+                             const X509NameInspection& native,
+                             const X509_NAME* name) {
+  Local<Context> context = env->context();
+  size_t rdn_count = 0;
+  for (const auto& attribute : native) {
+    if (attribute.set < 0) return {};
+    const size_t set = static_cast<size_t>(attribute.set);
+    if (set == rdn_count) {
+      rdn_count++;
+    } else if (rdn_count == 0 || set != rdn_count - 1) {
+      return {};
+    }
+  }
+
+  if (rdn_count > static_cast<size_t>(INT_MAX)) return {};
+  Local<Array> rdns = Array::New(env->isolate(), static_cast<int>(rdn_count));
+  LocalVector<Array> groups(env->isolate());
+  groups.reserve(rdn_count);
+  std::vector<uint32_t> group_lengths(rdn_count, 0);
+  for (size_t i = 0; i < rdn_count; i++) {
+    Local<Array> group = Array::New(env->isolate());
+    if (!SetArrayValue(context, rdns, i, group)) return {};
+    groups.push_back(group);
+  }
+
+  for (const auto& attribute : native) {
+    Local<Object> output = Object::New(env->isolate());
+    if (!SetValue(context,
+                  output,
+                  env->oid_string(),
+                  OneByteString(env->isolate(), attribute.oid))) {
+      return {};
+    }
+    if (attribute.value.has_value()) {
+      Local<Value> value;
+      if (!ToV8Value(env, attribute.value.value()).ToLocal(&value) ||
+          !SetValue(context, output, env->value_string(), value)) {
+        return {};
+      }
+    }
+    const size_t rdn_index = static_cast<size_t>(attribute.set);
+    if (!SetArrayValue(
+            context, groups[rdn_index], group_lengths[rdn_index]++, output)) {
+      return {};
+    }
+  }
+
+  ncrypto::Buffer<const unsigned char> encoded;
+  Local<Value> raw;
+  Local<Object> output = Object::New(env->isolate());
+  if (!GetNameDer(name, &encoded) || !CopyBuffer(env, encoded).ToLocal(&raw) ||
+      !SetValue(context, output, env->raw_string(), raw) ||
+      !SetValue(context, output, env->rdns_string(), rdns)) {
+    return {};
+  }
+  return output;
+}
+
+MaybeLocal<Value> CreateRawExtensions(
+    Environment* env,
+    const std::vector<ncrypto::X509RawExtension>& extensions) {
+  Local<Context> context = env->context();
+  if (extensions.size() > static_cast<size_t>(INT_MAX)) return {};
+  Local<Array> output =
+      Array::New(env->isolate(), static_cast<int>(extensions.size()));
+  for (size_t i = 0; i < extensions.size(); i++) {
+    const auto& extension = extensions[i];
+    Local<Object> value = Object::New(env->isolate());
+    Local<Value> bytes;
+    if (!CopyBuffer(env, extension.value).ToLocal(&bytes) ||
+        !SetValue(context,
+                  value,
+                  env->oid_string(),
+                  OneByteString(env->isolate(), extension.oid)) ||
+        !SetValue(context,
+                  value,
+                  env->critical_string(),
+                  Boolean::New(env->isolate(), extension.critical)) ||
+        !SetValue(context, value, env->value_string(), bytes) ||
+        !SetArrayValue(context, output, i, value)) {
+      return {};
+    }
+  }
+  return output;
+}
+
+MaybeLocal<Value> CreateAlgorithmIdentifier(Environment* env,
+                                            const X509_ALGOR* native) {
+  if (native == nullptr) return {};
+  const ASN1_OBJECT* native_oid = nullptr;
+  int parameter_type = V_ASN1_UNDEF;
+  const void* parameter_value = nullptr;
+  X509_ALGOR_get0(&native_oid, &parameter_type, &parameter_value, native);
+  auto oid = OidToText(native_oid);
+  if (!oid.has_value()) return {};
+
+  Local<Context> context = env->context();
+  Local<Object> output = Object::New(env->isolate());
+  if (!SetValue(context,
+                output,
+                env->oid_string(),
+                OneByteString(env->isolate(), oid.value()))) {
+    return {};
+  }
+  if (parameter_type != V_ASN1_UNDEF) {
+    ncrypto::DeleteFnPtr<ASN1_TYPE, ASN1_TYPE_free> native_parameters(
+        ASN1_TYPE_new());
+    if (!native_parameters ||
+        ASN1_TYPE_set1(
+            native_parameters.get(), parameter_type, parameter_value) != 1) {
+      return {};
+    }
+    const int length = i2d_ASN1_TYPE(native_parameters.get(), nullptr);
+    if (length <= 0) return {};
+    std::vector<unsigned char> encoded(static_cast<size_t>(length));
+    unsigned char* cursor = encoded.data();
+    if (i2d_ASN1_TYPE(native_parameters.get(), &cursor) != length ||
+        cursor != encoded.data() + encoded.size()) {
+      return {};
+    }
+
+    Local<Value> parameters;
+    if (!CopyBuffer(env, encoded).ToLocal(&parameters) ||
+        !SetValue(context, output, env->parameters_string(), parameters)) {
+      return {};
+    }
+  }
+  return output;
+}
+
+MaybeLocal<Value> CreateSignature(Environment* env,
+                                  const X509View& certificate) {
+  const ASN1_BIT_STRING* native_signature = nullptr;
+  const X509_ALGOR* native_algorithm = nullptr;
+  X509_get0_signature(&native_signature, &native_algorithm, certificate.get());
+  if (native_signature == nullptr || native_algorithm == nullptr ||
+      ASN1_STRING_type(native_signature) != V_ASN1_BIT_STRING) {
+    return {};
+  }
+
+  const int native_length = ASN1_STRING_length(native_signature);
+  const unsigned char* native_data = ASN1_STRING_get0_data(native_signature);
+  if (native_length < 0 || (native_length > 0 && native_data == nullptr)) {
+    return {};
+  }
+#if NCRYPTO_HAS_ASN1_BIT_STRING_GET_LENGTH
+  size_t byte_length = 0;
+  int unused_bits = 0;
+  if (ASN1_BIT_STRING_get_length(
+          native_signature, &byte_length, &unused_bits) != 1 ||
+      unused_bits != 0 || byte_length != static_cast<size_t>(native_length)) {
+    return {};
+  }
+#elif NCRYPTO_USE_OPENSSL_PROVIDER || NCRYPTO_USE_BORINGSSL
+  if ((native_signature->flags & ASN1_STRING_FLAG_BITS_LEFT) != 0 &&
+      (native_signature->flags & 0x07) != 0) {
+    return {};
+  }
+#else
+#error "Unsupported crypto backend"
+#endif
+
+  const ncrypto::Buffer<const unsigned char> signature{
+      .data = native_data,
+      .len = static_cast<size_t>(native_length),
+  };
+
+  Local<Value> algorithm;
+  Local<Value> value;
+  if (!CreateAlgorithmIdentifier(env, native_algorithm).ToLocal(&algorithm) ||
+      !CopyBuffer(env, signature).ToLocal(&value)) {
+    return {};
+  }
+
+  Local<Context> context = env->context();
+  Local<Object> output = Object::New(env->isolate());
+  if (!SetValue(context, output, env->algorithm_string(), algorithm) ||
+      !SetValue(context, output, env->value_string(), value)) {
+    return {};
+  }
+  return output;
+}
+
+void Version(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args);
+  X509Certificate* certificate;
+  ASSIGN_OR_RETURN_UNWRAP(&certificate, args.This());
+  const auto version = certificate->view().getVersion();
+  if (!version.has_value()) return;
+  args.GetReturnValue().Set(
+      Integer::New(env->isolate(), static_cast<int32_t>(version.value())));
+}
+
+void StructuredName(const FunctionCallbackInfo<Value>& args, bool subject) {
+  Environment* env = Environment::GetCurrent(args);
+  X509Certificate* certificate;
+  ASSIGN_OR_RETURN_UNWRAP(&certificate, args.This());
+
+  const auto native = subject ? certificate->view().inspectSubjectName()
+                              : certificate->view().inspectIssuerName();
+  if (!native.has_value()) return;
+
+  MarkPopErrorOnReturn mark_pop_error_on_return;
+  const X509_NAME* name = subject ? X509_get_subject_name(certificate->get())
+                                  : X509_get_issuer_name(certificate->get());
+  Local<Value> output;
+  if (!CreateName(env, native.value(), name).ToLocal(&output)) {
+    return;
+  }
+  args.GetReturnValue().Set(output);
+}
+
+void SubjectName(const FunctionCallbackInfo<Value>& args) {
+  StructuredName(args, true);
+}
+
+void IssuerName(const FunctionCallbackInfo<Value>& args) {
+  StructuredName(args, false);
+}
+
+void Extensions(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args);
+  X509Certificate* certificate;
+  ASSIGN_OR_RETURN_UNWRAP(&certificate, args.This());
+
+  const auto extensions = certificate->view().inspectExtensions();
+  Local<Value> output;
+  if (!extensions.has_value() ||
+      !CreateRawExtensions(env, extensions.value()).ToLocal(&output)) {
+    return;
+  }
+  args.GetReturnValue().Set(output);
+}
+
+void Tbs(const FunctionCallbackInfo<Value>& args) {
+  MarkPopErrorOnReturn mark_pop_error_on_return;
+  Environment* env = Environment::GetCurrent(args);
+  X509Certificate* certificate;
+  ASSIGN_OR_RETURN_UNWRAP(&certificate, args.This());
+
+  const auto tbs = GetExactTbs(certificate->snapshot_der());
+  Local<Value> output;
+  if (!tbs.has_value() || !CopyBuffer(env, tbs.value()).ToLocal(&output)) {
+    return;
+  }
+  args.GetReturnValue().Set(output);
+}
+
+void Signature(const FunctionCallbackInfo<Value>& args) {
+  MarkPopErrorOnReturn mark_pop_error_on_return;
+  Environment* env = Environment::GetCurrent(args);
+  X509Certificate* certificate;
+  ASSIGN_OR_RETURN_UNWRAP(&certificate, args.This());
+
+  Local<Value> output;
+  if (!CreateSignature(env, certificate->view()).ToLocal(&output)) {
+    return;
+  }
+  args.GetReturnValue().Set(output);
+}
+
 void Pem(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   X509Certificate* cert;
@@ -1011,6 +1386,12 @@ Local<FunctionTemplate> X509Certificate::GetConstructorTemplate(
     SetProtoMethodNoSideEffect(isolate, tmpl, "serialNumber", SerialNumber);
     SetProtoMethodNoSideEffect(isolate, tmpl, "pem", Pem);
     SetProtoMethodNoSideEffect(isolate, tmpl, "raw", Der);
+    SetProtoMethodNoSideEffect(isolate, tmpl, "version", Version);
+    SetProtoMethodNoSideEffect(isolate, tmpl, "subjectName", SubjectName);
+    SetProtoMethodNoSideEffect(isolate, tmpl, "issuerName", IssuerName);
+    SetProtoMethodNoSideEffect(isolate, tmpl, "extensions", Extensions);
+    SetProtoMethodNoSideEffect(isolate, tmpl, "tbs", Tbs);
+    SetProtoMethodNoSideEffect(isolate, tmpl, "signature", Signature);
     SetProtoMethodNoSideEffect(isolate, tmpl, "publicKey", PublicKey);
     SetProtoMethodNoSideEffect(isolate, tmpl, "checkCA", CheckCA);
     SetProtoMethodNoSideEffect(isolate, tmpl, "checkHost", CheckHost);
@@ -1448,6 +1829,12 @@ void X509Certificate::RegisterExternalReferences(
   registry->Register(SerialNumber);
   registry->Register(Pem);
   registry->Register(Der);
+  registry->Register(Version);
+  registry->Register(SubjectName);
+  registry->Register(IssuerName);
+  registry->Register(Extensions);
+  registry->Register(Tbs);
+  registry->Register(Signature);
   registry->Register(PublicKey);
   registry->Register(CheckCA);
   registry->Register(CheckHost);
diff --git a/src/crypto/crypto_x509.h b/src/crypto/crypto_x509.h
index e618edd8e5a..73f3799fed8 100644
--- a/src/crypto/crypto_x509.h
+++ b/src/crypto/crypto_x509.h
@@ -128,6 +128,9 @@ class X509Certificate final : public BaseObject {

   inline ncrypto::X509View view() const { return *cert_; }
   inline X509* get() { return cert_->get(); }
+  inline const X509CertificateSnapshot::DER& snapshot_der() const {
+    return snapshot_->at(snapshot_index_);
+  }

   v8::MaybeLocal<v8::Value> toObject(Environment* env);
   static v8::MaybeLocal<v8::Value> toObject(Environment* env,
diff --git a/src/env_properties.h b/src/env_properties.h
index bb1d8394935..80b41b7a826 100644
--- a/src/env_properties.h
+++ b/src/env_properties.h
@@ -112,6 +112,7 @@
   V(code_string, "code")                                                       \
   V(config_string, "config")                                                   \
   V(constants_string, "constants")                                             \
+  V(critical_string, "critical")                                               \
   V(cwd_string, "cwd")                                                         \
   V(data_string, "data")                                                       \
   V(database_string, "database")                                               \
@@ -244,6 +245,7 @@
   V(node_string, "node")                                                       \
   V(object_string, "Object")                                                   \
   V(ocsp_request_string, "OCSPRequest")                                        \
+  V(oid_string, "oid")                                                         \
   V(ok_string, "ok")                                                           \
   V(oncertcb_string, "oncertcb")                                               \
   V(onchange_string, "onchange")                                               \
@@ -276,6 +278,7 @@
   V(p256_string, "P-256")                                                      \
   V(p384_string, "P-384")                                                      \
   V(p521_string, "P-521")                                                      \
+  V(parameters_string, "parameters")                                           \
   V(parse_error_string, "Parse Error")                                         \
   V(password_string, "password")                                               \
   V(path_string, "path")                                                       \
@@ -301,6 +304,8 @@
   V(psk_string, "psk")                                                         \
   V(public_exponent_string, "publicExponent")                                  \
   V(rate_string, "rate")                                                       \
+  V(raw_string, "raw")                                                         \
+  V(rdns_string, "rdns")                                                       \
   V(read_host_object_string, "_readHostObject")                                \
   V(readable_string, "readable")                                               \
   V(read_bigints_string, "readBigInts")                                        \
diff --git a/test/cctest/test_node_crypto.cc b/test/cctest/test_node_crypto.cc
index 55c619fb09a..447f4f93896 100644
--- a/test/cctest/test_node_crypto.cc
+++ b/test/cctest/test_node_crypto.cc
@@ -1026,6 +1026,14 @@ TEST(NodeCrypto, X509FormattingPreservesUnrelatedErrors) {
   const ncrypto::BIOPointer subject = cert.view().getSubject();
   ASSERT_TRUE(subject);
   EXPECT_EQ(ERR_peek_error(), expected);
+  EXPECT_TRUE(cert.view().getVersion().has_value());
+  EXPECT_EQ(ERR_peek_error(), expected);
+  EXPECT_TRUE(cert.view().inspectSubjectName().has_value());
+  EXPECT_EQ(ERR_peek_error(), expected);
+  EXPECT_TRUE(cert.view().inspectIssuerName().has_value());
+  EXPECT_EQ(ERR_peek_error(), expected);
+  EXPECT_TRUE(cert.view().inspectExtensions().has_value());
+  EXPECT_EQ(ERR_peek_error(), expected);
   ERR_clear_error();
 }

diff --git a/test/parallel/test-crypto-x509-chain-snapshot.js b/test/parallel/test-crypto-x509-chain-snapshot.js
index 433abd9fcca..7b091c84679 100644
--- a/test/parallel/test-crypto-x509-chain-snapshot.js
+++ b/test/parallel/test-crypto-x509-chain-snapshot.js
@@ -21,12 +21,46 @@ const issuer = new X509Certificate(fixtures.readKey('ca3-cert.pem'));
 function assertChain(actual) {
   assert(actual instanceof X509Certificate);
   assert.deepStrictEqual(actual.raw, leaf.raw);
+  assertInspection(actual, leaf);
   assert.strictEqual(actual.issuerCertificate, actual.issuerCertificate);
   assert(actual.issuerCertificate instanceof X509Certificate);
   assert.deepStrictEqual(actual.issuerCertificate.raw, issuer.raw);
+  assertInspection(actual.issuerCertificate, issuer);
   assert.strictEqual(actual.issuerCertificate.issuerCertificate, undefined);
 }

+function assertInspection(actual, expected) {
+  assert.strictEqual(actual.version, expected.version);
+
+  const subjectName = actual.subjectName;
+  const issuerName = actual.issuerName;
+  const extensions = actual.extensions;
+  const tbs = actual.tbs;
+  const signature = actual.signature;
+
+  assert.deepStrictEqual(subjectName, expected.subjectName);
+  assert.deepStrictEqual(issuerName, expected.issuerName);
+  assert.deepStrictEqual(extensions, expected.extensions);
+  assert.deepStrictEqual(tbs, expected.tbs);
+  assert.deepStrictEqual(signature, expected.signature);
+
+  subjectName.raw.fill(0);
+  issuerName.raw.fill(0);
+  for (const extension of extensions) extension.value.fill(0);
+  subjectName.rdns.length = 0;
+  issuerName.rdns.length = 0;
+  extensions.length = 0;
+  tbs.fill(0);
+  signature.value.fill(0);
+  signature.algorithm.parameters?.fill(0);
+
+  assert.deepStrictEqual(actual.subjectName, expected.subjectName);
+  assert.deepStrictEqual(actual.issuerName, expected.issuerName);
+  assert.deepStrictEqual(actual.extensions, expected.extensions);
+  assert.deepStrictEqual(actual.tbs, expected.tbs);
+  assert.deepStrictEqual(actual.signature, expected.signature);
+}
+
 function legacyChainRaw(certificate) {
   const result = [];
   const seen = new Set();
diff --git a/test/parallel/test-crypto-x509-inspection.js b/test/parallel/test-crypto-x509-inspection.js
new file mode 100644
index 00000000000..54ee7dae359
--- /dev/null
+++ b/test/parallel/test-crypto-x509-inspection.js
@@ -0,0 +1,281 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const { X509Certificate } = require('crypto');
+const { isBoringSSL } = require('../common/crypto');
+const fixtures = require('../common/fixtures');
+
+function load(name) {
+  return new X509Certificate(fixtures.readKey(name));
+}
+
+function readDer(input, offset = 0) {
+  const start = offset;
+  const tag = input[offset++];
+  let length = input[offset++];
+  if ((length & 0x80) !== 0) {
+    const octets = length & 0x7f;
+    length = 0;
+    for (let i = 0; i < octets; i++)
+      length = (length * 256) + input[offset++];
+  }
+  return {
+    tag,
+    start,
+    contentStart: offset,
+    end: offset + length,
+  };
+}
+
+function readChildren(input, parent) {
+  const children = [];
+  for (let offset = parent.contentStart; offset < parent.end;) {
+    const child = readDer(input, offset);
+    children.push(child);
+    offset = child.end;
+  }
+  return children;
+}
+
+function encodeDer(tag, content) {
+  let length;
+  if (content.length < 128) {
+    length = Buffer.of(content.length);
+  } else {
+    const octets = [];
+    for (let value = content.length; value !== 0; value >>>= 8)
+      octets.unshift(value & 0xff);
+    length = Buffer.of(0x80 | octets.length, ...octets);
+  }
+  return Buffer.concat([Buffer.of(tag), length, content]);
+}
+
+function rawElement(input, element) {
+  return input.subarray(element.start, element.end);
+}
+
+function appendExtensions(raw, additions) {
+  const certificate = readDer(raw);
+  const certificateFields = readChildren(raw, certificate);
+  const tbs = certificateFields[0];
+  const tbsFields = readChildren(raw, tbs);
+  const extensionIndex = tbsFields.findIndex(({ tag }) => tag === 0xa3);
+  assert.notStrictEqual(extensionIndex, -1);
+
+  const wrapper = tbsFields[extensionIndex];
+  const extensionSequence = readChildren(raw, wrapper)[0];
+  const extensions = readChildren(raw, extensionSequence)
+    .map((element) => rawElement(raw, element));
+  extensions.push(...additions);
+
+  const newWrapper = encodeDer(0xa3, encodeDer(
+    0x30, Buffer.concat(extensions)));
+  const newTbsFields = tbsFields.map((element, index) =>
+    (index === extensionIndex ? newWrapper : rawElement(raw, element)));
+  const newTbs = encodeDer(0x30, Buffer.concat(newTbsFields));
+  const newCertificateFields = certificateFields.map((element, index) =>
+    (index === 0 ? newTbs : rawElement(raw, element)));
+  return encodeDer(0x30, Buffer.concat(newCertificateFields));
+}
+
+function replaceVersion(raw, value) {
+  const certificate = readDer(raw);
+  const certificateFields = readChildren(raw, certificate);
+  const tbs = certificateFields[0];
+  const tbsFields = readChildren(raw, tbs);
+  assert.strictEqual(tbsFields[0].tag, 0xa0);
+  const version = encodeDer(0xa0, encodeDer(0x02, value));
+  const newTbs = encodeDer(0x30, Buffer.concat(tbsFields.map(
+    (element, index) => (index === 0 ? version : rawElement(raw, element)))));
+  return encodeDer(0x30, Buffer.concat(certificateFields.map(
+    (element, index) => (index === 0 ? newTbs : rawElement(raw, element)))));
+}
+
+function replaceSubject(raw, value) {
+  const certificate = readDer(raw);
+  const certificateFields = readChildren(raw, certificate);
+  const tbs = certificateFields[0];
+  const tbsFields = readChildren(raw, tbs);
+  const fieldOffset = tbsFields[0].tag === 0xa0 ? 1 : 0;
+  const subjectIndex = fieldOffset + 4;
+  const newTbs = encodeDer(0x30, Buffer.concat(tbsFields.map(
+    (element, index) =>
+      (index === subjectIndex ? value : rawElement(raw, element)))));
+  return encodeDer(0x30, Buffer.concat(certificateFields.map(
+    (element, index) => (index === 0 ? newTbs : rawElement(raw, element)))));
+}
+
+const keyUsageExtension = Buffer.from(
+  '300b0603551d0f0404030205a0', 'hex');
+const unknownExtension = Buffer.from(
+  '300906032a030404020500', 'hex');
+const criticalUnknownExtension = Buffer.from(
+  '300c06032a03040101ff04020500', 'hex');
+
+{
+  const cert = load('agent1-cert.pem');
+  const raw = cert.raw;
+  const certificate = readDer(raw);
+  const [tbs] = readChildren(raw, certificate);
+  const tbsFields = readChildren(raw, tbs);
+  const fieldOffset = tbsFields[0].tag === 0xa0 ? 1 : 0;
+  const issuer = tbsFields[fieldOffset + 2];
+  const subject = tbsFields[fieldOffset + 4];
+
+  assert.strictEqual(cert.version, 3);
+  assert.strictEqual(cert.subjectName.rdns.at(-1)[0].oid,
+                     '1.2.840.113549.1.9.1');
+  assert.strictEqual(cert.subjectName.rdns.at(-1)[0].value,
+                     'ry@tinyclouds.org');
+  assert.deepStrictEqual(cert.subjectName.raw, rawElement(raw, subject));
+  assert.strictEqual(cert.issuerName.rdns.at(-2)[0].value, 'ca1');
+  assert.deepStrictEqual(cert.issuerName.raw, rawElement(raw, issuer));
+
+  assert.deepStrictEqual(cert.extensions.map(({ oid, critical }) => ({
+    oid,
+    critical,
+  })), [{
+    oid: '1.3.6.1.5.5.7.1.1',
+    critical: false,
+  }]);
+  assert(cert.extensions[0].value.length > 0);
+
+  assert.deepStrictEqual(cert.tbs, rawElement(raw, tbs));
+
+  assert.deepStrictEqual(cert.signature.algorithm, {
+    oid: '1.2.840.113549.1.1.11',
+    parameters: Buffer.from('0500', 'hex'),
+  });
+  assert(cert.signature.value.length > 0);
+}
+
+// Raw inspection preserves extension order, duplicates, criticality, and the
+// exact bytes inside extnValue without interpreting the extension syntax.
+{
+  const source = load('agent1-cert.pem');
+  const cert = new X509Certificate(appendExtensions(source.raw, [
+    keyUsageExtension,
+    unknownExtension,
+    criticalUnknownExtension,
+  ]));
+
+  assert.deepStrictEqual(cert.extensions.slice(-3), [
+    {
+      oid: '2.5.29.15',
+      critical: false,
+      value: Buffer.from('030205a0', 'hex'),
+    },
+    {
+      oid: '1.2.3.4',
+      critical: false,
+      value: Buffer.from('0500', 'hex'),
+    },
+    {
+      oid: '1.2.3.4',
+      critical: true,
+      value: Buffer.from('0500', 'hex'),
+    },
+  ]);
+}
+
+// An empty Name is valid, but an RDN is a nonempty SET OF attributes. OpenSSL
+// does not expose empty RDNs through X509_NAME entries, so inspection must not
+// silently collapse this malformed Name into a valid empty one.
+{
+  const source = load('agent1-cert.pem');
+  const emptyRdnName = encodeDer(0x30, encodeDer(0x31, Buffer.alloc(0)));
+  const malformed = replaceSubject(source.raw, emptyRdnName);
+  let cert;
+  try {
+    cert = new X509Certificate(malformed);
+  } catch (err) {
+    if (!isBoringSSL || err.code !== 'ERR_OSSL_PEM_NO_START_LINE') {
+      throw err;
+    }
+  }
+  if (cert !== undefined) {
+    assert.throws(() => cert.subjectName, {
+      code: 'ERR_CRYPTO_INVALID_X509_CERTIFICATE',
+    });
+  }
+}
+
+// Reject unsupported versions at whichever native parsing layer observes them
+// first. Backends that accept the encoding must reject it before converting
+// OpenSSL's long result to the public one-based version number.
+{
+  const source = load('agent1-cert.pem');
+  let cert;
+  try {
+    cert = new X509Certificate(replaceVersion(
+      source.raw, Buffer.from('7fffffffffffffff', 'hex')));
+  } catch (err) {
+    assert.strictEqual(err.code, isBoringSSL ?
+      'ERR_OSSL_PEM_NO_START_LINE' : 'ERR_OSSL_X509_INVALID_VERSION');
+  }
+  if (cert !== undefined) {
+    assert.throws(() => cert.version, {
+      code: 'ERR_CRYPTO_INVALID_X509_CERTIFICATE',
+    });
+  }
+}
+
+// Inspection uses the immutable native snapshot, not the mutable Buffer cached
+// by the legacy raw getter.
+{
+  const cert = load('agent1-cert.pem');
+  cert.raw.fill(0);
+  assert.strictEqual(cert.version, 3);
+  assert.strictEqual(cert.subjectName.rdns.at(-1)[0].value,
+                     'ry@tinyclouds.org');
+}
+
+// New structured getters return fresh defensive values. Existing legacy
+// getters retain their current identity behavior in this backportable change.
+{
+  const cert = load('leaf-from-intermediate-cert.pem');
+  const subjectName = cert.subjectName;
+  const subjectRaw = subjectName.raw;
+  const extensions = cert.extensions;
+  const tbs = cert.tbs;
+  const signature = cert.signature;
+  const parameters = signature.algorithm.parameters;
+
+  subjectName.rdns.length = 0;
+  subjectRaw.fill(0);
+  extensions.length = 0;
+  tbs.fill(0);
+  signature.value.fill(0);
+  parameters.fill(0);
+
+  assert(cert.subjectName.rdns.length > 0);
+  assert.notDeepStrictEqual(cert.subjectName.raw, subjectRaw);
+  assert(cert.extensions.length > 0);
+  assert.notStrictEqual(cert.tbs[0], 0);
+  assert.notStrictEqual(cert.signature.value[0], 0);
+  assert.notStrictEqual(cert.signature.algorithm.parameters[0], 0);
+  assert.strictEqual(cert.raw, cert.raw);
+}
+
+for (const name of [
+  'version',
+  'subjectName',
+  'issuerName',
+  'extensions',
+  'tbs',
+  'signature',
+]) {
+  const descriptor = Object.getOwnPropertyDescriptor(
+    X509Certificate.prototype, name);
+  assert.strictEqual(typeof descriptor.get, 'function');
+  assert.strictEqual(descriptor.set, undefined);
+  assert.strictEqual(descriptor.enumerable, false);
+  assert.strictEqual(descriptor.configurable, true);
+  assert.throws(() => descriptor.get.call({}), {
+    code: 'ERR_INVALID_THIS',
+  });
+}
diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts
index cc0d8856ab8..1ff5a8b1ed4 100644
--- a/typings/internalBinding/crypto.d.ts
+++ b/typings/internalBinding/crypto.d.ts
@@ -724,6 +724,32 @@ declare namespace InternalCryptoBinding {
     getIssuer(): Buffer | null;
   }

+  interface X509NameAttribute {
+    oid: string;
+    value?: string;
+  }
+
+  interface X509Name {
+    raw: Buffer;
+    rdns: X509NameAttribute[][];
+  }
+
+  interface X509RawExtension {
+    oid: string;
+    critical: boolean;
+    value: Buffer;
+  }
+
+  interface X509AlgorithmIdentifier {
+    oid: string;
+    parameters?: Buffer;
+  }
+
+  interface X509Signature {
+    algorithm: X509AlgorithmIdentifier;
+    value: Buffer;
+  }
+
   interface X509CertificateHandle {
     subject(): string;
     subjectAltName(): string | undefined;
@@ -741,6 +767,12 @@ declare namespace InternalCryptoBinding {
     serialNumber(): string;
     signatureAlgorithm(): string | undefined;
     signatureAlgorithmOid(): string | undefined;
+    version(): number | undefined;
+    subjectName(): X509Name | undefined;
+    issuerName(): X509Name | undefined;
+    extensions(): X509RawExtension[] | undefined;
+    tbs(): Buffer | undefined;
+    signature(): X509Signature | undefined;
     raw(): Buffer;
     publicKey(): KeyObjectHandle;
     pem(): string;