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());