From 5c06e67e013db8c5f74eceb1e6e307bdf77442e0 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 30 Aug 2026 09:04:52 +0200 Subject: [PATCH] crypto,tls: clone X.509 chains across isolates X509Certificate cloning drops issuerCertificate and shares its native X509 object with the receiving isolate. Snapshot the leaf and supplied peer chain as DER, then parse separate native certificates in each isolate. Construct the JavaScript chain iteratively for TLS inspection, structuredClone(), MessagePort, and workers. Each supplied position gets its own object, including repeated certificate encodings. Refs: https://github.com/nodejs/node/pull/65602 Assisted-by: Codex Signed-off-by: Filip Skokan --- deps/ncrypto/ncrypto.cc | 2 +- src/crypto/crypto_x509.cc | 272 ++++++++++++++---- src/crypto/crypto_x509.h | 83 ++++-- .../test-crypto-x509-chain-snapshot.js | 95 ++++++ test/parallel/test-crypto-x509-peer-chain.js | 110 +++++++ 5 files changed, 480 insertions(+), 82 deletions(-) create mode 100644 test/parallel/test-crypto-x509-chain-snapshot.js create mode 100644 test/parallel/test-crypto-x509-peer-chain.js diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index d35d601abc70..07aaebc6f716 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -1604,7 +1604,7 @@ bool X509View::isCA() const { } bool X509View::isIssuedBy(const X509View& issuer) const { - ClearErrorOnReturn clearErrorOnReturn; + MarkPopErrorOnReturn mark_pop_error_on_return; if (cert_ == nullptr || issuer.cert_ == nullptr) return false; return X509_check_issued(const_cast(issuer.cert_), const_cast(cert_)) == X509_V_OK; diff --git a/src/crypto/crypto_x509.cc b/src/crypto/crypto_x509.cc index c7dbe956149b..0c99615c95a8 100644 --- a/src/crypto/crypto_x509.cc +++ b/src/crypto/crypto_x509.cc @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -55,17 +56,94 @@ using v8::Value; namespace crypto { -ManagedX509::ManagedX509(X509Pointer&& cert) : cert_(std::move(cert)) {} +namespace { + +std::optional EncodeCertificate( + const X509View& cert) { + MarkPopErrorOnReturn mark_pop_error_on_return; + if (!cert) return std::nullopt; + + const int size = i2d_X509(cert.get(), nullptr); + if (size <= 0) return std::nullopt; + + X509CertificateSnapshot::DER der(static_cast(size)); + unsigned char* cursor = der.data(); + if (i2d_X509(cert.get(), &cursor) != size || + cursor != der.data() + der.size()) { + return std::nullopt; + } + return der; +} + +X509Pointer ParseCertificate(const X509CertificateSnapshot::DER& certificate) { + MarkPopErrorOnReturn mark_pop_error_on_return; + constexpr size_t kMaximumCertificateSize = static_cast( + std::numeric_limits::max()); // NOLINT(runtime/int) + if (certificate.empty() || certificate.size() > kMaximumCertificateSize) { + return {}; + } -ManagedX509::ManagedX509(const ManagedX509& that) { - *this = that; + const unsigned char* cursor = certificate.data(); + const unsigned char* const end = cursor + certificate.size(); + X509Pointer cert( + d2i_X509(nullptr, + &cursor, + static_cast(certificate.size()))); // NOLINT(runtime/int) + if (!cert || cursor != end) return {}; + return cert; } -ManagedX509& ManagedX509::operator=(const ManagedX509& that) { - cert_.reset(that.get()); - if (cert_) [[likely]] - X509_up_ref(cert_.get()); - return *this; +} // namespace + +ManagedX509::ManagedX509(X509Pointer cert) : cert_(std::move(cert)) {} + +std::shared_ptr ManagedX509::Adopt(X509Pointer cert) { + if (!cert) return {}; + return std::shared_ptr(new ManagedX509(std::move(cert))); +} + +X509CertificateSnapshot::X509CertificateSnapshot(std::vector certificates) + : certificates_(std::move(certificates)) {} + +std::shared_ptr X509CertificateSnapshot::Create( + X509View leaf, + const STACK_OF(X509) * issuer_chain, + OPENSSL_SIZE_T issuers_start) { + auto leaf_der = EncodeCertificate(leaf); + if (!leaf_der.has_value()) return {}; + + const OPENSSL_SIZE_T count = + issuer_chain == nullptr ? 0 : sk_X509_num(issuer_chain); + std::vector certificates; + certificates.reserve(static_cast(count) + 1); + certificates.push_back(std::move(leaf_der.value())); + for (OPENSSL_SIZE_T i = issuers_start; i < count; i++) { + const X509View candidate(sk_X509_value(issuer_chain, i)); + auto candidate_der = EncodeCertificate(candidate); + if (!candidate_der.has_value()) return {}; + certificates.push_back(std::move(candidate_der.value())); + } + + return std::shared_ptr( + new X509CertificateSnapshot(std::move(certificates))); +} + +X509Pointer X509CertificateSnapshot::Parse(size_t index) const { + if (index >= certificates_.size()) return {}; + return ParseCertificate(certificates_[index]); +} + +size_t X509CertificateSnapshot::size_bytes() const { + return std::accumulate(certificates_.begin(), + certificates_.end(), + size_t{0}, + [](size_t total, const DER& certificate) { + return total + certificate.size(); + }); +} + +void X509CertificateSnapshot::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("certificates", size_bytes()); } void ManagedX509::MemoryInfo(MemoryTracker* tracker) const { @@ -947,9 +1025,25 @@ void X509Certificate::NewFromHandle(const FunctionCallbackInfo& args) { if (!result.value) [[unlikely]] { return ThrowCryptoError(env, result.error.value_or(0)); } + + auto snapshot = X509CertificateSnapshot::Create(result.value.view()); + if (!snapshot) [[unlikely]] { + THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to snapshot X.509 certificate"); + return; + } + + auto parsed = snapshot->Parse(0); + if (!parsed) [[unlikely]] { + THROW_ERR_CRYPTO_OPERATION_FAILED( + env, "Failed to parse X.509 certificate snapshot"); + return; + } new X509Certificate(env, args.This(), - std::make_shared(std::move(result.value))); + ManagedX509::Adopt(std::move(parsed)), + std::move(snapshot), + 0); return; } @@ -963,7 +1057,12 @@ void X509Certificate::NewFromHandle(const FunctionCallbackInfo& args) { CHECK_NOT_NULL(handle); Local issuer; if (handle->issuer_cert_) issuer = handle->issuer_cert_->object(); - new X509Certificate(env, args.This(), handle->cert_, issuer); + new X509Certificate(env, + args.This(), + handle->cert_, + handle->snapshot_, + handle->snapshot_index_, + issuer); } void X509Certificate::CreateX509CertificateClass( @@ -1002,50 +1101,93 @@ void X509Certificate::IsX509Certificate( MaybeLocal X509Certificate::New(Environment* env, X509Pointer cert, - const STACK_OF(X509) * issuer_chain) { - std::shared_ptr mcert(new ManagedX509(std::move(cert))); - return New(env, std::move(mcert), issuer_chain); -} + const STACK_OF(X509) * issuer_chain, + OPENSSL_SIZE_T issuers_start) { + if (!cert) return {}; + + auto snapshot = + X509CertificateSnapshot::Create(cert.view(), issuer_chain, issuers_start); + if (!snapshot) [[unlikely]] { + THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "Failed to snapshot X.509 certificate"); + return {}; + } -MaybeLocal X509Certificate::New(Environment* env, - std::shared_ptr cert, - const STACK_OF(X509) * issuer_chain) { - return NewWithIssuers(env, std::move(cert), issuer_chain, 0); + // Keep the live certificate independent from the backend object that was + // serialized while creating the immutable snapshot. Some backends populate + // or invalidate lazy caches while encoding malformed-but-parseable input. + auto parsed = snapshot->Parse(0); + if (!parsed) [[unlikely]] { + THROW_ERR_CRYPTO_OPERATION_FAILED( + env, "Failed to parse X.509 certificate snapshot"); + return {}; + } + return New(env, ManagedX509::Adopt(std::move(parsed)), std::move(snapshot)); } -MaybeLocal X509Certificate::NewWithIssuers( +MaybeLocal X509Certificate::New( Environment* env, std::shared_ptr cert, - const STACK_OF(X509) * issuer_chain, - int start) { + std::shared_ptr snapshot, + size_t snapshot_index) { + if (!cert || !snapshot || snapshot_index >= snapshot->size()) return {}; + EscapableHandleScope scope(env->isolate()); + Local context = env->context(); + Local issuer; + + for (size_t i = snapshot->size(); i > snapshot_index + 1; i--) { + const size_t index = i - 1; + auto parsed = snapshot->Parse(index); + if (!parsed) [[unlikely]] { + THROW_ERR_CRYPTO_OPERATION_FAILED( + env, "Failed to parse X.509 certificate snapshot"); + return {}; + } + + Local issuer_object; + if (!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(context) + .ToLocal(&issuer_object)) { + return {}; + } + new X509Certificate(env, + issuer_object, + ManagedX509::Adopt(std::move(parsed)), + snapshot, + index, + issuer); + issuer = issuer_object; + } + Local obj; if (!GetConstructorTemplate(env) ->InstanceTemplate() - ->NewInstance(env->context()) + ->NewInstance(context) .ToLocal(&obj)) { - return MaybeLocal(); + return {}; } + new X509Certificate( + env, obj, std::move(cert), std::move(snapshot), snapshot_index, issuer); + return scope.Escape(obj); +} - Local issuer; - if (issuer_chain != nullptr && - static_cast(start) < sk_X509_num(issuer_chain)) { - X509Pointer issuer_cert = - X509View(sk_X509_value(issuer_chain, start)).clone(); - if (!issuer_cert) [[unlikely]] { - return MaybeLocal(); - } - if (!NewWithIssuers(env, - std::make_shared(std::move(issuer_cert)), - issuer_chain, - start + 1) - .ToLocal(&issuer)) [[unlikely]] { - return MaybeLocal(); - } +MaybeLocal X509Certificate::New( + Environment* env, + std::shared_ptr snapshot, + size_t snapshot_index) { + if (!snapshot || snapshot_index >= snapshot->size()) return {}; + auto cert = snapshot->Parse(snapshot_index); + if (!cert) [[unlikely]] { + THROW_ERR_CRYPTO_OPERATION_FAILED( + env, "Failed to parse X.509 certificate snapshot"); + return {}; } - - new X509Certificate(env, obj, std::move(cert), issuer); - return scope.Escape(obj); + return New(env, + ManagedX509::Adopt(std::move(cert)), + std::move(snapshot), + snapshot_index); } MaybeLocal X509Certificate::GetCert(Environment* env, @@ -1064,26 +1206,25 @@ MaybeLocal X509Certificate::GetPeerCert(Environment* env, // The peer chain is owned by the SSL session and must not be modified. Its // first entry is the peer certificate on the client but not on the server. const STACK_OF(X509)* ssl_certs = SSL_get_peer_cert_chain(ssl.get()); - int issuers_start = 0; + OPENSSL_SIZE_T issuers_start = 0; X509Pointer cert; if ((flag & GetPeerCertificateFlag::SERVER) == GetPeerCertificateFlag::SERVER) { cert = X509Pointer::PeerFrom(ssl); } - if (!cert) { - if (ssl_certs == nullptr || sk_X509_num(ssl_certs) == 0) - return MaybeLocal(); - cert = X509View(sk_X509_value(ssl_certs, 0)).clone(); - if (!cert) [[unlikely]] - return MaybeLocal(); + + if (!cert && (ssl_certs == nullptr || sk_X509_num(ssl_certs) == 0)) + return MaybeLocal(); + + if (!cert) [[unlikely]] { + const X509* leaf = sk_X509_value(ssl_certs, 0); + if (leaf != nullptr) cert = X509View(leaf).clone(); issuers_start = 1; } - return NewWithIssuers(env, - std::make_shared(std::move(cert)), - ssl_certs, - issuers_start); + if (!cert) return {}; + return New(env, std::move(cert), ssl_certs, issuers_start); } v8::MaybeLocal X509Certificate::toObject(Environment* env) { @@ -1097,11 +1238,17 @@ v8::MaybeLocal X509Certificate::toObject(Environment* env, return X509ToObject(env, cert).FromMaybe(Local()); } -X509Certificate::X509Certificate(Environment* env, - Local object, - std::shared_ptr cert, - Local issuer_chain) - : BaseObject(env, object), cert_(std::move(cert)) { +X509Certificate::X509Certificate( + Environment* env, + Local object, + std::shared_ptr cert, + std::shared_ptr snapshot, + size_t snapshot_index, + Local issuer_chain) + : BaseObject(env, object), + cert_(std::move(cert)), + snapshot_(std::move(snapshot)), + snapshot_index_(snapshot_index) { MakeWeak(); if (!issuer_chain.IsEmpty()) { @@ -1111,6 +1258,12 @@ X509Certificate::X509Certificate(Environment* env, void X509Certificate::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("cert", cert_); + tracker->TrackField("snapshot", snapshot_); +} + +void X509Certificate::X509CertificateTransferData::MemoryInfo( + MemoryTracker* tracker) const { + tracker->TrackField("snapshot", snapshot_); } BaseObjectPtr @@ -1124,7 +1277,7 @@ X509Certificate::X509CertificateTransferData::Deserialize( } Local handle; - if (!X509Certificate::New(env, data_).ToLocal(&handle)) + if (!X509Certificate::New(env, snapshot_, snapshot_index_).ToLocal(&handle)) return {}; Local module = @@ -1157,7 +1310,8 @@ BaseObject::TransferMode X509Certificate::GetTransferMode() const { std::unique_ptr X509Certificate::CloneForMessaging() const { - return std::make_unique(cert_); + return std::make_unique(snapshot_, + snapshot_index_); } void X509Certificate::Initialize(Environment* env, Local target) { diff --git a/src/crypto/crypto_x509.h b/src/crypto/crypto_x509.h index a6f64d4f2d4f..44714d1bcca2 100644 --- a/src/crypto/crypto_x509.h +++ b/src/crypto/crypto_x509.h @@ -11,22 +11,24 @@ #include "node_worker.h" #include "v8.h" +#include +#include +#include + namespace node { namespace crypto { -// The ManagedX509 class is essentially a smart pointer for -// X509 objects that allows an X509Certificate instance to -// be cloned at the JS level while pointing at the same -// underlying X509 instance. +// ManagedX509 owns one isolate-local X509. Adopt makes the ownership transfer +// explicit at each call site. class ManagedX509 final : public MemoryRetainer { public: - ManagedX509() = default; - explicit ManagedX509(ncrypto::X509Pointer&& cert); - ManagedX509(const ManagedX509& that); - ManagedX509& operator=(const ManagedX509& that); + static std::shared_ptr Adopt(ncrypto::X509Pointer cert); + + ManagedX509(const ManagedX509&) = delete; + ManagedX509& operator=(const ManagedX509&) = delete; inline operator bool() const { return !!cert_; } - inline X509* get() const { return cert_.get(); } + inline X509* get() { return cert_.get(); } inline ncrypto::X509View view() const { return cert_; } inline operator ncrypto::X509View() const { return cert_; } @@ -35,9 +37,40 @@ class ManagedX509 final : public MemoryRetainer { SET_SELF_SIZE(ManagedX509) private: + explicit ManagedX509(ncrypto::X509Pointer cert); + ncrypto::X509Pointer cert_; }; +// An immutable certificate-encoding snapshot. Certificate zero is the leaf and +// all remaining certificates follow in the supplied order. Sharing this +// snapshot between isolates is safe; the isolate-local X509 objects are always +// reparsed from it. +class X509CertificateSnapshot final : public MemoryRetainer { + public: + using DER = std::vector; + + static std::shared_ptr Create( + ncrypto::X509View leaf, + const STACK_OF(X509) * issuer_chain = nullptr, + OPENSSL_SIZE_T issuers_start = 0); + + ncrypto::X509Pointer Parse(size_t index) const; + + inline size_t size() const { return certificates_.size(); } + inline const DER& at(size_t index) const { return certificates_.at(index); } + size_t size_bytes() const; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(X509CertificateSnapshot) + SET_SELF_SIZE(X509CertificateSnapshot) + + private: + explicit X509CertificateSnapshot(std::vector certificates); + + const std::vector certificates_; +}; + class X509Certificate final : public BaseObject { public: enum class GetPeerCertificateFlag { @@ -60,12 +93,19 @@ class X509Certificate final : public BaseObject { static v8::MaybeLocal New( Environment* env, ncrypto::X509Pointer cert, - const STACK_OF(X509) * issuer_chain = nullptr); + const STACK_OF(X509) * issuer_chain = nullptr, + OPENSSL_SIZE_T issuers_start = 0); static v8::MaybeLocal New( Environment* env, std::shared_ptr cert, - const STACK_OF(X509) * issuer_chain = nullptr); + std::shared_ptr snapshot, + size_t snapshot_index = 0); + + static v8::MaybeLocal New( + Environment* env, + std::shared_ptr snapshot, + size_t snapshot_index = 0); static v8::MaybeLocal GetCert(Environment* env, const ncrypto::SSLPointer& ssl); @@ -96,20 +136,22 @@ class X509Certificate final : public BaseObject { class X509CertificateTransferData : public worker::TransferData { public: explicit X509CertificateTransferData( - const std::shared_ptr& data) - : data_(data) {} + std::shared_ptr snapshot, + size_t snapshot_index) + : snapshot_(std::move(snapshot)), snapshot_index_(snapshot_index) {} BaseObjectPtr Deserialize( Environment* env, v8::Local context, std::unique_ptr self) override; + void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(X509CertificateTransferData) SET_SELF_SIZE(X509CertificateTransferData) - SET_NO_MEMORY_INFO() private: - std::shared_ptr data_; + std::shared_ptr snapshot_; + size_t snapshot_index_; }; BaseObject::TransferMode GetTransferMode() const override; @@ -119,16 +161,13 @@ class X509Certificate final : public BaseObject { X509Certificate(Environment* env, v8::Local object, std::shared_ptr cert, + std::shared_ptr snapshot, + size_t snapshot_index, v8::Local issuer_chain = v8::Local()); - // Like New(), but reads the issuer chain from issuer_chain[start] upward. - static v8::MaybeLocal NewWithIssuers( - Environment* env, - std::shared_ptr cert, - const STACK_OF(X509) * issuer_chain, - int start); - std::shared_ptr cert_; + std::shared_ptr snapshot_; + size_t snapshot_index_ = 0; BaseObjectPtr issuer_cert_; }; diff --git a/test/parallel/test-crypto-x509-chain-snapshot.js b/test/parallel/test-crypto-x509-chain-snapshot.js new file mode 100644 index 000000000000..5a0f696da0f1 --- /dev/null +++ b/test/parallel/test-crypto-x509-chain-snapshot.js @@ -0,0 +1,95 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { X509Certificate } = require('node:crypto'); +const { once } = require('node:events'); +const tls = require('node:tls'); +const { + MessageChannel, + Worker, +} = require('node:worker_threads'); +const fixtures = require('../common/fixtures'); + +const leaf = new X509Certificate(fixtures.readKey('agent6-cert.pem')); +const issuer = new X509Certificate(fixtures.readKey('ca3-cert.pem')); + +function assertChain(actual) { + assert(actual instanceof X509Certificate); + assert.deepStrictEqual(actual.raw, leaf.raw); + assert.strictEqual(actual.issuerCertificate, actual.issuerCertificate); + assert(actual.issuerCertificate instanceof X509Certificate); + assert.deepStrictEqual(actual.issuerCertificate.raw, issuer.raw); + assert.strictEqual(actual.issuerCertificate.issuerCertificate, undefined); +} + +function legacyChainRaw(certificate) { + const result = []; + const seen = new Set(); + while (certificate !== undefined && !seen.has(certificate)) { + seen.add(certificate); + result.push(certificate.raw); + certificate = certificate.issuerCertificate; + } + return result; +} + +async function cloneViaMessagePort(certificate) { + const { port1, port2 } = new MessageChannel(); + port1.postMessage(certificate); + const [clone] = await once(port2, 'message'); + port1.close(); + port2.close(); + return clone; +} + +async function cloneViaWorker(certificate) { + const worker = new Worker(` + 'use strict'; + const { parentPort } = require('node:worker_threads'); + parentPort.once('message', (certificate) => { + parentPort.postMessage(certificate); + }); + `, { eval: true }); + worker.postMessage(certificate); + const [clone] = await once(worker, 'message'); + await worker.terminate(); + return clone; +} + +const server = tls.createServer({ + key: fixtures.readKey('agent6-key.pem'), + cert: fixtures.readKey('agent6-cert.pem'), +}, (socket) => socket.end()); + +(async () => { + server.listen(0); + await once(server, 'listening'); + + const socket = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + }); + await once(socket, 'secureConnect'); + + const first = socket.getPeerX509Certificate(); + assertChain(first); + + const firstLegacy = socket.getPeerCertificate(true); + const second = socket.getPeerX509Certificate(); + const secondLegacy = socket.getPeerCertificate(true); + assertChain(second); + assert.deepStrictEqual(legacyChainRaw(firstLegacy), [leaf.raw, issuer.raw]); + assert.deepStrictEqual(legacyChainRaw(secondLegacy), [leaf.raw, issuer.raw]); + + assertChain(structuredClone(first)); + assertChain(await cloneViaMessagePort(first)); + assertChain(await cloneViaWorker(first)); + + socket.destroy(); + server.close(); + await once(server, 'close'); +})().then(common.mustCall()); diff --git a/test/parallel/test-crypto-x509-peer-chain.js b/test/parallel/test-crypto-x509-peer-chain.js new file mode 100644 index 000000000000..b60f3d00acc0 --- /dev/null +++ b/test/parallel/test-crypto-x509-peer-chain.js @@ -0,0 +1,110 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { X509Certificate } = require('node:crypto'); +const { once } = require('node:events'); +const tls = require('node:tls'); +const { MessageChannel, Worker } = require('node:worker_threads'); +const fixtures = require('../common/fixtures'); + +const leaf = new X509Certificate(fixtures.readKey('agent6-cert.pem')); +const issuer = new X509Certificate(fixtures.readKey('ca3-cert.pem')); +const unrelated = new X509Certificate(fixtures.readKey('agent1-cert.pem')); +const key = fixtures.readKey('agent6-key.pem'); +const chains = [ + [leaf, issuer, unrelated], + [leaf, unrelated, issuer], + [leaf, issuer, issuer], + [leaf, issuer, unrelated, issuer], + [leaf, leaf, issuer], +]; + +function assertChain(actual, expected) { + const seen = new Set(); + for (const certificate of expected) { + assert(actual instanceof X509Certificate); + assert(!seen.has(actual)); + seen.add(actual); + assert.deepStrictEqual(actual.raw, certificate.raw); + assert.strictEqual(actual.issuerCertificate, actual.issuerCertificate); + actual = actual.issuerCertificate; + } + assert.strictEqual(actual, undefined); +} + +async function assertClones(certificate, chain) { + assertChain(structuredClone(certificate), chain); + + const { port1, port2 } = new MessageChannel(); + port1.postMessage(certificate); + const [messageClone] = await once(port2, 'message'); + port1.close(); + port2.close(); + assertChain(messageClone, chain); + + const worker = new Worker(` + const { parentPort } = require('node:worker_threads'); + parentPort.once('message', (certificate) => { + parentPort.postMessage(certificate); + }); + `, { eval: true }); + worker.postMessage(certificate); + const [workerClone] = await once(worker, 'message'); + await worker.terminate(); + assertChain(workerClone, chain); +} + +async function checkPeer(socket, chain) { + const first = socket.getPeerX509Certificate(); + assertChain(first, chain); + for (let i = 0; i < 2; i++) { + assert.deepStrictEqual(socket.getPeerCertificate(true).raw, leaf.raw); + assertChain(socket.getPeerX509Certificate(), chain); + } + await assertClones(first, chain); +} + +async function test(chain) { + // Encode each certificate separately: agent6-cert.pem contains its issuer. + const cert = chain.map((certificate) => certificate.toString()).join(''); + const server = tls.createServer({ + key, + cert, + requestCert: true, + rejectUnauthorized: false, + }); + server.listen(0); + await once(server, 'listening'); + + const peer = once(server, 'secureConnection'); + const client = tls.connect({ + port: server.address().port, + key, + cert, + rejectUnauthorized: false, + }); + await once(client, 'secureConnect'); + const [socket] = await peer; + + // Client and server certificate stacks handle the leaf differently; both + // sockets must expose every certificate sent by the peer exactly once. + await checkPeer(client, chain); + await checkPeer(socket, chain); + + // Clones must remain usable after their TLS session has been destroyed. + const clone = structuredClone(client.getPeerX509Certificate()); + client.destroy(); + socket.destroy(); + const closed = once(server, 'close'); + server.close(); + await closed; + assertChain(clone, chain); +} + +(async () => { + for (const chain of chains) await test(chain); +})().then(common.mustCall());