current commit
261020.15 · tls: reject chains containing blocked public keys
#include "guard.h"
#ifndef OPENSSL_NO_QUIC
#include <async_wrap-inl.h>
#include <base_object-inl.h>
#include <crypto/crypto_context.h>
#include <crypto/crypto_tls_certificates.h>
#include <crypto/crypto_util.h>
#include <debug_utils-inl.h>
#include <env-inl.h>
#include <memory_tracker-inl.h>
#include <ngtcp2/ngtcp2.h>
#include <ngtcp2/ngtcp2_crypto.h>
#include <ngtcp2/ngtcp2_crypto_ossl.h>
#include <node_sockaddr-inl.h>
#include <openssl/ssl.h>
#ifdef NODE_OPENSSL_HAS_CERT_COMP
#include <openssl/comp.h>
#endif
#include <util-inl.h>
#include <v8.h>
#include "application.h"
#include "bindingdata.h"
#include "defs.h"
#include "session.h"
#include "tlscontext.h"
#include "transportparams.h"

namespace node {

using ncrypto::BIOPointer;
using ncrypto::ClearErrorOnReturn;
using ncrypto::MarkPopErrorOnReturn;
using ncrypto::SSLCtxPointer;
using ncrypto::SSLPointer;
using ncrypto::SSLSessionPointer;
using v8::Array;
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Just;
using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Nothing;
using v8::Object;
using v8::String;
using v8::Undefined;
using v8::Value;

namespace quic {

// ============================================================================

namespace {

// Temporarily wraps an SSL pointer but does not take ownership.
// Used by a few of the TLSSession methods that need access to the SSL*
// pointer held by the OSSLContext but cannot take ownership of it.
class SSLPointerRef final {
 public:
  inline SSLPointerRef(SSL* ssl) : temp_(ssl) { CHECK(temp_); }
  inline ~SSLPointerRef() { release(); }
  DISALLOW_COPY_AND_MOVE(SSLPointerRef)
  inline operator const SSLPointer&() const { return temp_; }
  inline const SSLPointer* operator->() const { return &temp_; }
  inline const SSLPointer& operator*() const { return temp_; }
  inline void release() { temp_.release(); }

 private:
  SSLPointer temp_;
};

void EnableTrace(Environment* env, BIOPointer* bio, SSL* ssl) {
#if HAVE_SSL_TRACE
  static bool warn_trace_tls = true;
  if (warn_trace_tls) {
    warn_trace_tls = false;
    ProcessEmitWarning(env,
                       "Enabling --trace-tls can expose sensitive data in "
                       "the resulting log");
  }
  if (!*bio) {
    bio->reset(BIO_new_fp(stderr, BIO_NOCLOSE | BIO_FP_TEXT));
    SSL_set_msg_callback(
        ssl,
        [](int write_p,
           int version,
           int content_type,
           const void* buf,
           size_t len,
           SSL* ssl,
           void* arg) -> void {
          MarkPopErrorOnReturn mark_pop_error_on_return;
          SSL_trace(write_p, version, content_type, buf, len, ssl, arg);
        });
    SSL_set_msg_callback_arg(ssl, bio->get());
  }
#endif
}

template <typename T, typename Opt, std::vector<T> Opt::*member>
bool SetOption(Environment* env,
               Opt* options,
               const Local<Object>& object,
               const Local<String>& name) {
  Local<Value> value;
  if (!object->Get(env->context(), name).ToLocal(&value)) return false;

  if (value->IsUndefined()) return true;

  // The value can be either a single item or an array of items.

  if (value->IsArray()) {
    auto context = env->context();
    auto values = value.As<Array>();
    uint32_t count = values->Length();
    for (uint32_t n = 0; n < count; n++) {
      Local<Value> item;
      if (!values->Get(context, n).ToLocal(&item)) {
        return false;
      }
      if constexpr (std::is_same<T, crypto::KeyObjectData>::value) {
        if (crypto::KeyObjectHandle::HasInstance(env, item)) {
          crypto::KeyObjectHandle* handle;
          ASSIGN_OR_RETURN_UNWRAP(&handle, item, false);
          (options->*member).push_back(handle->Data().addRef());
        } else {
          Utf8Value namestr(env->isolate(), name);
          THROW_ERR_INVALID_ARG_TYPE(
              env, "%s value must be a key object", namestr);
          return false;
        }
      } else if constexpr (std::is_same<T, Store>::value) {
        if (item->IsArrayBufferView()) {
          Store store = Store::CopyFrom(item.As<ArrayBufferView>());
          (options->*member).push_back(std::move(store));
        } else if (item->IsArrayBuffer()) {
          Store store = Store::CopyFrom(item.As<ArrayBuffer>());
          (options->*member).push_back(std::move(store));
        } else {
          Utf8Value namestr(env->isolate(), name);
          THROW_ERR_INVALID_ARG_TYPE(
              env,
              "%s value must be an array buffer or array buffer view",
              *namestr);
          return false;
        }
      }
    }
  } else {
    if constexpr (std::is_same<T, crypto::KeyObjectData>::value) {
      if (crypto::KeyObjectHandle::HasInstance(env, value)) {
        crypto::KeyObjectHandle* handle;
        ASSIGN_OR_RETURN_UNWRAP(&handle, value, false);
        (options->*member).push_back(handle->Data().addRef());
      } else {
        Utf8Value namestr(env->isolate(), name);
        THROW_ERR_INVALID_ARG_TYPE(
            env, "%s value must be a key object", namestr);
        return false;
      }
    } else if constexpr (std::is_same<T, Store>::value) {
      if (value->IsArrayBufferView()) {
        Store store = Store::CopyFrom(value.As<ArrayBufferView>());
        (options->*member).push_back(std::move(store));
      } else if (value->IsArrayBuffer()) {
        Store store = Store::CopyFrom(value.As<ArrayBuffer>());
        (options->*member).push_back(std::move(store));
      } else {
        Utf8Value namestr(env->isolate(), name);
        THROW_ERR_INVALID_ARG_TYPE(
            env,
            "%s value must be an array buffer or array buffer view",
            *namestr);
        return false;
      }
    }
  }
  return true;
}
}  // namespace

OSSLContext::OSSLContext() {
  CHECK_EQ(ngtcp2_crypto_ossl_ctx_new(&ctx_, nullptr), 0);
}

OSSLContext::~OSSLContext() {
  reset();
}

void OSSLContext::reset() {
  if (ctx_) {
    // The SSL object inside the ngtcp2 ctx may not have been set if
    // SSL creation failed. Guard against null before clearing app data.
    if (SSL* ssl = *this; ssl != nullptr) {
      SSL_set_app_data(ssl, nullptr);
    }
    // connection_ is set during Initialize(). If Initialize() was
    // never called (e.g. SSL creation failed), it's still nullptr.
    if (connection_ != nullptr) {
      ngtcp2_conn_set_tls_native_handle(connection_, nullptr);
    }
    ngtcp2_crypto_ossl_ctx_del(ctx_);
    ctx_ = nullptr;
    connection_ = nullptr;
  }
}

OSSLContext::operator SSL*() const {
  return ngtcp2_crypto_ossl_ctx_get_ssl(ctx_);
}

OSSLContext::operator ngtcp2_crypto_ossl_ctx*() const {
  return ctx_;
}

void OSSLContext::Initialize(SSL* ssl,
                             ngtcp2_crypto_conn_ref* ref,
                             ngtcp2_conn* connection,
                             SSL_CTX* ssl_ctx) {
  CHECK(ssl);
  ngtcp2_crypto_ossl_ctx_set_ssl(ctx_, ssl);
  SSL_set_app_data(*this, ref);
  // TODO(@jasnell): Later when BoringSSL is also supported, the native
  // handle will be different. The ngtcp2_crypto_ossl.h impl requires
  // that the native handle be set to the ngtcp2_crypto_ossl_ctx. So
  // this will need to be updated to support both cases.
  ngtcp2_conn_set_tls_native_handle(connection, ctx_);
  connection_ = connection;
}

std::string_view OSSLContext::get_cipher_name() const {
  return SSL_get_cipher_name(*this);
}

std::string OSSLContext::get_selected_alpn() const {
  const unsigned char* alpn = nullptr;
  unsigned int len;
  SSL_get0_alpn_selected(*this, &alpn, &len);
  if (alpn == nullptr) return {};
  return std::string(reinterpret_cast<const char*>(alpn), len);
}

std::string_view OSSLContext::get_negotiated_group() const {
  auto name = SSL_get0_group_name(*this);
  if (name == nullptr) return "";
  return name;
}

bool OSSLContext::set_alpn_protocols(std::string_view protocols) const {
  return SSL_set_alpn_protos(
             *this,
             reinterpret_cast<const unsigned char*>(protocols.data()),
             protocols.size()) == 0;
}

bool OSSLContext::set_hostname(std::string_view hostname) const {
  // SSL_set_tlsext_host_name is a macro that casts to void* internally.
  // The std::string constructed here guarantees null-termination for
  // the underlying SSL_ctrl C API.
  std::string name(hostname.empty() ? "localhost" : hostname);
  return SSL_ctrl(*this,
                  SSL_CTRL_SET_TLSEXT_HOSTNAME,
                  TLSEXT_NAMETYPE_host_name,
                  const_cast<char*>(name.c_str())) == 1;
}

bool OSSLContext::set_verify_hostname(std::string_view hostname) const {
  // SSL_set1_host tells OpenSSL to verify the peer certificate's
  // subject name (SAN/CN) matches this hostname. This is separate
  // from SSL_set_tlsext_host_name which only sets the SNI extension.
  static const char* kDefaultHostname = "localhost";
  if (hostname.empty()) {
    return SSL_set1_host(*this, kDefaultHostname) == 1;
  } else {
    return SSL_set1_host(*this, hostname.data()) == 1;
  }
}

bool OSSLContext::set_early_data_enabled() const {
  return SSL_set_quic_tls_early_data_enabled(*this, 1) == 1;
}

bool OSSLContext::set_transport_params(const ngtcp2_vec& tp) const {
  return SSL_set_quic_tls_transport_params(*this, tp.base, tp.len) == 1;
}

bool OSSLContext::get_early_data_accepted() const {
  return SSL_get_early_data_status(*this) == SSL_EARLY_DATA_ACCEPTED;
}

bool OSSLContext::get_early_data_rejected() const {
  return SSL_get_early_data_status(*this) == SSL_EARLY_DATA_REJECTED;
}

bool OSSLContext::get_early_data_attempted() const {
  return SSL_get_early_data_status(*this) != SSL_EARLY_DATA_NOT_SENT;
}

bool OSSLContext::set_session_ticket(const ncrypto::SSLSessionPointer& ticket) {
  if (!ticket) return false;
  if (SSL_set_session(*this, ticket.get()) != 1) return false;
  return SSL_SESSION_get_max_early_data(ticket.get()) != 0;
}

bool OSSLContext::ConfigureServer() const {
  if (ngtcp2_crypto_ossl_configure_server_session(*this) != 0) return false;
  SSL_set_accept_state(*this);
  return set_early_data_enabled();
}

bool OSSLContext::ConfigureClient() const {
  if (ngtcp2_crypto_ossl_configure_client_session(*this) != 0) return false;
  SSL_set_connect_state(*this);
  return true;
}

// ============================================================================

std::shared_ptr<TLSContext> TLSContext::CreateClient(Environment* env,
                                                     const Options& options) {
  return std::make_shared<TLSContext>(env, Side::CLIENT, options);
}

std::shared_ptr<TLSContext> TLSContext::CreateServer(Environment* env,
                                                     const Options& options) {
  return std::make_shared<TLSContext>(env, Side::SERVER, options);
}

TLSContext::TLSContext(Environment* env, Side side, const Options& options)
    : side_(side), options_(options), ctx_(Initialize(env)) {}

TLSContext::operator SSL_CTX*() const {
  DCHECK(ctx_);
  return ctx_.get();
}

crypto::ClientHelloResult TLSContext::OnClientHello(
    const crypto::ClientHelloContext& hello) {
  auto& tls_session = TLSSession::From(hello.ssl());
  auto& session = tls_session.session();

  using EarlySelection = TLSSession::EarlySelection;
  switch (tls_session.early_selection()) {
    case EarlySelection::kPending:
      break;
    case EarlySelection::kSelected:
      // Selected already, but the Session has not been surfaced yet, so
      // the handshake must stay where it is.
      return crypto::ClientHelloResult::kRetry;
    case EarlySelection::kComplete: {
      // Don't let HelloRetryRequests change SNI:
      auto name = hello.servername();
      if (!name.has_value() || *name != tls_session.servername()) {
        Debug(&session, "ClientHello changed the requested servername");
        hello.set_alert(SSL_AD_ILLEGAL_PARAMETER);
        return crypto::ClientHelloResult::kFail;
      }
      // Everything else the TLS library needs from here on is cached on the
      // TLSSession, so just let the handshake run.
      return crypto::ClientHelloResult::kContinue;
    }
  }

  auto requested = hello.servername();
  if (!requested.has_value()) {
    Debug(&session, "Unusable servername in ClientHello");
    hello.set_alert(SSL_AD_DECODE_ERROR);
    return crypto::ClientHelloResult::kFail;
  }
  const std::string_view servername = *requested;

  auto* selected = tls_session.context().SelectSNIContext(servername);
  if (selected == nullptr) {
    Debug(&session, "No TLS context for servername %s", servername);
    hello.set_alert(SSL_AD_UNRECOGNIZED_NAME);
    return crypto::ClientHelloResult::kFail;
  }
  if (selected != &tls_session.context()) {
    SSL_set_SSL_CTX(hello.ssl(), *selected);
  }
  tls_session.set_servername(servername);

  // The client's QUIC transport parameters travel in the ClientHello, but
  // the TLS stack does not hand them to ngtcp2 until it parses extensions,
  // which is after this callback.
  auto params = hello.extension(TLSEXT_TYPE_quic_transport_parameters);
  if (params.has_value()) session.SetEarlyRemoteTransportParams(*params);

  const auto& supported = selected->options().alpn;
  auto negotiated = crypto::SelectNextProtocol(
      {reinterpret_cast<const uint8_t*>(supported.data()), supported.size()},
      hello.alpn_protocols());
  if (!negotiated.has_value()) {
    Debug(&session, "ALPN negotiation failed");
    hello.set_alert(SSL_AD_NO_APPLICATION_PROTOCOL);
    return crypto::ClientHelloResult::kFail;
  }
  Debug(&session, "ALPN negotiation succeeded: %s", *negotiated);
  tls_session.set_alpn(*negotiated);

  // Install the Application while the handshake is still stopped, so that
  // it is in place before a session ticket can be accepted and early data
  // can start arriving.
  session.InstallApplicationForAlpn(*negotiated);
  session.set_hello_processed();

  // Stop here. Session::AfterNgtcp2Read surfaces the server session to
  // JavaScript and then resumes the handshake, at which point this callback
  // runs again and takes the kComplete path.
  tls_session.set_early_selection(EarlySelection::kSelected);
  return crypto::ClientHelloResult::kRetry;
}

int TLSContext::OnSelectAlpn(SSL* ssl,
                             const unsigned char** out,
                             unsigned char* outlen,
                             const unsigned char* in,
                             unsigned int inlen,
                             void* arg) {
  // The protocol was already chosen from the ClientHello. OpenSSL still
  // requires it to be handed back here for the ALPN extension to be sent.
  const auto& negotiated = TLSSession::From(ssl).alpn();
  if (negotiated.empty()) return SSL_TLSEXT_ERR_NOACK;
  // The list offered here should be the one the choice was made from, but
  // a peer can send a second ClientHello after HelloRetryRequest; never
  // answer with a protocol this one does not offer.
  if (!crypto::AlpnListContains({in, inlen}, negotiated)) {
    return SSL_TLSEXT_ERR_ALERT_FATAL;
  }
  crypto::SetSelectedProtocol(out, outlen, negotiated);
  return SSL_TLSEXT_ERR_OK;
}

int TLSContext::OnNewSession(SSL* ssl, SSL_SESSION* sess) {
  auto& session = TLSSession::From(ssl).session();

  // If there is nothing listening for the session ticket, do not bother.
  if (session.wants_session_ticket()) {
    Debug(&session, "Preparing TLS session resumption ticket");

    // Pre-fight to see how much space we need to allocate for the session
    // ticket.
    size_t size = i2d_SSL_SESSION(sess, nullptr);

    // If size is 0, the size is greater than our max, or there is not
    // enough memory to allocate the backing store, then we ignore it
    // and continue without emitting the sessionticket event.
    if (size > 0 && size <= crypto::SecureContext::kMaxSessionSize) {
      JS_TRY_ALLOCATE_BACKING_OR_RETURN(session.env(), ticket, size, 0);
      auto data = reinterpret_cast<unsigned char*>(ticket->Data());
      if (i2d_SSL_SESSION(sess, &data) > 0) {
        session.EmitSessionTicket(Store(std::move(ticket), size));
      }
    }
  }

  return 0;
}

void TLSContext::OnKeylog(const SSL* ssl, const char* line) {
  TLSSession::From(ssl).session().EmitKeylog(line);
}

int TLSContext::OnVerifyClientCertificate(int preverify_ok,
                                          X509_STORE_CTX* ctx) {
  // This callback is invoked by OpenSSL for each certificate in the
  // client's chain during the TLS handshake. The preverify_ok
  // parameter reflects OpenSSL's own chain validation result for
  // the current certificate. Failures include:
  //   - Expired or not-yet-valid certificates
  //   - Self-signed certificates not in the trusted CA list
  //   - Broken chain (signature verification failure)
  //   - Untrusted CA (chain does not terminate at a configured CA)
  //   - Revoked certificates (if CRL is configured)
  //   - Invalid basic constraints or key usage
  //
  // If preverify_ok is 1, validation passed for this cert and we
  // always continue. If it is 0, the behavior depends on the
  // reject_unauthorized option:
  //   - true (default): return 0 to abort the handshake immediately,
  //     avoiding wasted work on an untrusted client.
  //   - false: return 1 to let the handshake complete. The validation
  //     error is still recorded by OpenSSL and will be reported to JS
  //     via VerifyPeerIdentity() in the handshake callback, allowing
  //     the application to make its own decision.
  //
  // Note that even when preverify_ok is 1 (chain validation passed),
  // the application may need to perform additional verification after
  // the handshake — for example, checking the certificate's common
  // name or subject alternative names against an allowlist, verifying
  // application-specific fields or extensions, or enforcing certificate
  // pinning. Chain validation only confirms cryptographic integrity
  // and trust anchor; it does not confirm authorization.
  if (preverify_ok) return 1;

  SSL* ssl = static_cast<SSL*>(
      X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()));
  auto& tls_session = TLSSession::From(ssl);
  return tls_session.context().options().reject_unauthorized ? 0 : 1;
}

std::unique_ptr<TLSSession> TLSContext::NewSession(
    Session* session, const std::optional<SessionTicket>& maybeSessionTicket) {
  // Passing a session ticket only makes sense with a client session.
  CHECK_IMPLIES(session->is_server(), !maybeSessionTicket.has_value());
  auto tls_session = std::make_unique<TLSSession>(
      session, shared_from_this(), maybeSessionTicket);
  if (!tls_session->is_valid()) return nullptr;
  return tls_session;
}

SSLCtxPointer TLSContext::Initialize(Environment* env) {
  SSLCtxPointer ctx;
  switch (side_) {
    case Side::SERVER: {
      static constexpr unsigned char kSidCtx[] = "Node.js QUIC Server";
      ctx = SSLCtxPointer::NewServer();
      if (!ctx) [[unlikely]] {
        validation_error_ = "Failed to create SSL_CTX for server";
        return {};
      }

      if (SSL_CTX_set_max_early_data(
              ctx.get(), options_.enable_early_data ? UINT32_MAX : 0) != 1) {
        validation_error_ = "Failed to set max early data";
        return {};
      }
      // ngtcp2 handles replay protection at the QUIC layer,
      // so we disable OpenSSL's built-in anti-replay.
      SSL_CTX_set_options(ctx.get(),
                          (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) |
                              SSL_OP_CIPHER_SERVER_PREFERENCE |
                              SSL_OP_NO_ANTI_REPLAY);

      SSL_CTX_set_mode(ctx.get(), SSL_MODE_RELEASE_BUFFERS);
      SSL_CTX_set_alpn_select_cb(ctx.get(), OnSelectAlpn, this);
      crypto::SetClientHelloCallback<OnClientHello>(ctx.get());

      if (SSL_CTX_set_session_id_context(
              ctx.get(), kSidCtx, sizeof(kSidCtx) - 1) != 1) {
        validation_error_ = "Failed to set session ID context";
        return {};
      }

      if (options_.verify_client) [[unlikely]] {
        SSL_CTX_set_verify(ctx.get(),
                           SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE |
                               SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
                           OnVerifyClientCertificate);
      }

      CHECK_EQ(SSL_CTX_set_session_ticket_cb(ctx.get(),
                                             SessionTicket::GenerateCallback,
                                             SessionTicket::DecryptedCallback,
                                             nullptr),
               1);

      // The SNI context is selected from the ClientHello; this callback
      // exists only so that OpenSSL acknowledges the extension.
      SSL_CTX_set_tlsext_servername_callback(ctx.get(), OnSNI);
      break;
    }
    case Side::CLIENT: {
      ctx = SSLCtxPointer::NewClient();
      SSL_CTX_set_mode(ctx.get(), SSL_MODE_RELEASE_BUFFERS);
      SSL_CTX_set_session_cache_mode(
          ctx.get(), SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL);
      SSL_CTX_sess_set_new_cb(ctx.get(), OnNewSession);

      // In strict mode, set SSL_VERIFY_PEER so OpenSSL aborts the
      // handshake if the server's certificate fails validation. In
      // non-strict modes, verification still occurs but the handshake
      // completes regardless — the result is surfaced to JS.
      if (options_.verify_peer_strict) {
        SSL_CTX_set_verify(ctx.get(), SSL_VERIFY_PEER, nullptr);
      }
      break;
    }
  }

  // QUIC builds its SSL_CTX here instead of through SecureContext, so the
  // chain verification hook shared with TLS has to be installed explicitly.
  crypto::InstallCertVerifyCallback(ctx.get());

  if (options_.keylog) {
    SSL_CTX_set_keylog_callback(ctx.get(), OnKeylog);
  }

  if (SSL_CTX_set_ciphersuites(ctx.get(), options_.ciphers.c_str()) != 1) {
    validation_error_ = "Invalid cipher suite";
    return SSLCtxPointer();
  }

  if (SSL_CTX_set1_groups_list(ctx.get(), options_.groups.c_str()) != 1) {
    validation_error_ = "Invalid cipher groups";
    return SSLCtxPointer();
  }

  {
    ClearErrorOnReturn clear_error_on_return;
    if (options_.ca.empty()) {
      crypto::UseDefaultRootCertStore(env, ctx.get());
    } else {
      for (const auto& ca : options_.ca) {
        uv_buf_t buf = ca;
        if (buf.len == 0) {
          crypto::UseDefaultRootCertStore(env, ctx.get());
        } else {
          BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len);
          CHECK(bio);
          crypto::AddCACertificates(env, ctx.get(), bio);
        }
      }
    }
  }

  {
    ClearErrorOnReturn clear_error_on_return;
    for (const auto& cert : options_.certs) {
      uv_buf_t buf = cert;
      if (buf.len > 0) {
        BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len);
        CHECK(bio);
        cert_.reset();
        issuer_.reset();
        if (crypto::SSL_CTX_use_certificate_chain(
                ctx.get(), std::move(bio), &cert_, &issuer_) == 0) {
          validation_error_ = "Invalid certificate";
          return SSLCtxPointer();
        }
      }
    }
  }

  // TLS certificate compression (RFC 8879). OpenSSL enables all available
  // algorithms by default once compression libraries are linked, so we
  // always clear the preference first to keep certificate compression
  // opt-in (matching the behavior of node:tls). When the
  // certificateCompression option is set, apply the requested algorithms
  // and pre-compress the certificate(s) loaded above. QUIC always uses
  // TLS 1.3, which is the minimum required for certificate compression.
#ifdef NODE_OPENSSL_HAS_CERT_COMP
  {
    ClearErrorOnReturn clear_error_on_return;
    SSL_CTX_set1_cert_comp_preference(ctx.get(), nullptr, 0);

    // The JS layer packs (length | alg0<<8 | alg1<<16 | alg2<<24) into a
    // single uint32. IDs match TLSEXT_comp_cert_zlib (1), _brotli (2),
    // _zstd (3).
    const uint32_t packed = options_.certificate_compression;
    const size_t len = packed & 0xff;
    if (len > 0) {
      // TLSEXT_comp_cert_limit bounds the zero-terminated algs array; the
      // number of usable algorithms is one fewer.
      constexpr size_t kMaxCompAlgs = TLSEXT_comp_cert_limit - 1;
      if (len > kMaxCompAlgs) {
        validation_error_ = "Invalid certificate compression preference";
        return SSLCtxPointer();
      }
      int algs[kMaxCompAlgs];
      for (size_t i = 0; i < len; i++) {
        algs[i] = (packed >> (8 * (i + 1))) & 0xff;
      }
      if (!SSL_CTX_set1_cert_comp_preference(ctx.get(), algs, len)) {
        validation_error_ = "Failed to set certificate compression preference";
        return SSLCtxPointer();
      }
      // Pre-compress the loaded certificate(s) for the preferred algorithms.
      // Returns 0 when no certificate is loaded (e.g. a client context) or
      // when compression did not reduce the size; both are non-fatal.
      constexpr int kCompressAllAlgs = 0;
      SSL_CTX_compress_certs(ctx.get(), kCompressAllAlgs);
    }
  }
#endif  // NODE_OPENSSL_HAS_CERT_COMP

  {
    ClearErrorOnReturn clear_error_on_return;
    for (const auto& key : options_.keys) {
      if (!crypto::UsePrivateKey(ctx.get(), key)) {
        validation_error_ = "Invalid key";
        return SSLCtxPointer();
      }
    }
  }

  {
    ClearErrorOnReturn clear_error_on_return;
    for (const auto& crl : options_.crl) {
      uv_buf_t buf = crl;
      BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len);
      if (!crypto::AddCRL(env, ctx.get(), bio)) {
        validation_error_ = "Invalid CRL";
        return SSLCtxPointer();
      }
    }
  }

  {
    ClearErrorOnReturn clear_error_on_return;
    if (options_.verify_private_key &&
        SSL_CTX_check_private_key(ctx.get()) != 1) {
      validation_error_ = "Invalid private key";
      return SSLCtxPointer();
    }
  }

  return ctx;
}

TLSContext* TLSContext::SelectSNIContext(std::string_view servername) {
  DCHECK_EQ(side_, Side::SERVER);
  if (!servername.empty()) {
    auto it = sni_contexts_.find(servername);
    if (it != sni_contexts_.end()) return it->second.get();
  }
  // No matching hostname. If this context has a certificate (from the
  // sni['*'] wildcard identity), fall back to it. Otherwise there is no
  // identity that can serve this connection.
  if (SSL_CTX_get0_certificate(ctx_.get()) == nullptr) return nullptr;
  return this;
}

int TLSContext::OnSNI(SSL* ssl, int* ad, void* arg) {
  // The context for this host name was already selected and applied from
  // the ClientHello. OpenSSL only needs the extension acknowledged here.
  return SSL_TLSEXT_ERR_OK;
}

bool TLSContext::AddSNIContext(Environment* env,
                               const std::string& hostname,
                               const Options& options) {
  DCHECK_EQ(side_, Side::SERVER);
  auto ctx = std::make_shared<TLSContext>(env, Side::SERVER, options);
  if (!*ctx) return false;
  sni_contexts_[hostname] = std::move(ctx);
  return true;
}

bool TLSContext::SetSNIContexts(
    Environment* env, const std::unordered_map<std::string, Options>& entries) {
  DCHECK_EQ(side_, Side::SERVER);
  decltype(sni_contexts_) new_contexts;
  for (const auto& [hostname, options] : entries) {
    auto ctx = std::make_shared<TLSContext>(env, Side::SERVER, options);
    if (!*ctx) return false;
    new_contexts[hostname] = std::move(ctx);
  }
  sni_contexts_ = std::move(new_contexts);
  return true;
}

void TLSContext::MemoryInfo(MemoryTracker* tracker) const {
  tracker->TrackField("options", options_);
  tracker->TrackFieldWithSize(
      "sni_contexts",
      sni_contexts_.size() * sizeof(std::shared_ptr<TLSContext>));
}

Maybe<TLSContext::Options> TLSContext::Options::From(Environment* env,
                                                     Local<Value> value) {
  if (value.IsEmpty()) {
    return Nothing<Options>();
  }

  Options options;
  auto& state = BindingData::Get(env);

  if (value->IsUndefined()) {
    return Just(kDefault);
  }

  if (!value->IsObject()) {
    THROW_ERR_INVALID_ARG_TYPE(env, "tls options must be an object");
    return Nothing<Options>();
  }

  auto params = value.As<Object>();

#define SET_VECTOR(Type, name)                                                 \
  SetOption<Type, TLSContext::Options, &TLSContext::Options::name>(            \
      env, &options, params, state.name##_string())

#define SET(name)                                                              \
  SetOption<TLSContext::Options, &TLSContext::Options::name>(                  \
      env, &options, params, state.name##_string())

  if (!SET(verify_client) || !SET(reject_unauthorized) ||
      !SET(verify_hostname) || !SET(verify_peer_strict) ||
      !SET(enable_early_data) || !SET(enable_tls_trace) || !SET(alpn) ||
      !SET(servername) || !SET(ciphers) || !SET(groups) ||
      !SET(verify_private_key) || !SET(keylog) || !SET(port) ||
      !SET(certificate_compression) || !SET(authoritative) ||
      !SET_VECTOR(crypto::KeyObjectData, keys) || !SET_VECTOR(Store, certs) ||
      !SET_VECTOR(Store, ca) || !SET_VECTOR(Store, crl)) {
    return Nothing<Options>();
  }

  return Just<Options>(options);
}

std::string TLSContext::Options::ToString() const {
  DebugIndentScope indent;
  auto prefix = indent.Prefix();
  std::string res("{");
  res += prefix + "alpn: " + alpn;
  res += prefix + "servername: " + servername;
  res +=
      prefix + "keylog: " + (keylog ? std::string("yes") : std::string("no"));
  res += prefix + "verify client: " +
         (verify_client ? std::string("yes") : std::string("no"));
  res += prefix + "reject unauthorized: " +
         (reject_unauthorized ? std::string("yes") : std::string("no"));
  res += prefix + "verify peer strict: " +
         (verify_peer_strict ? std::string("yes") : std::string("no"));
  res += prefix + "enable early data: " +
         (enable_early_data ? std::string("yes") : std::string("no"));
  res += prefix + "enable_tls_trace: " +
         (enable_tls_trace ? std::string("yes") : std::string("no"));
  res += prefix + "verify private key: " +
         (verify_private_key ? std::string("yes") : std::string("no"));
  res += prefix + "ciphers: " + ciphers;
  res += prefix + "groups: " + groups;
  res += prefix +
         "certificate compression: " + std::to_string(certificate_compression);
  res += prefix + "keys: " + std::to_string(keys.size());
  res += prefix + "certs: " + std::to_string(certs.size());
  res += prefix + "ca: " + std::to_string(ca.size());
  res += prefix + "crl: " + std::to_string(crl.size());
  res += indent.Close();
  return res;
}

void TLSContext::Options::MemoryInfo(MemoryTracker* tracker) const {
  tracker->TrackField("keys", keys);
  tracker->TrackField("certs", certs);
  tracker->TrackField("ca", ca);
  tracker->TrackField("crl", crl);
}

const TLSContext::Options TLSContext::Options::kDefault = {};

// ============================================================================

TLSSession& TLSSession::From(const SSL* ssl) {
  auto ref = static_cast<ngtcp2_crypto_conn_ref*>(SSL_get_app_data(ssl));
  CHECK_NOT_NULL(ref);
  return *static_cast<TLSSession*>(ref->user_data);
}

TLSSession::TLSSession(Session* session,
                       std::shared_ptr<TLSContext> context,
                       const std::optional<SessionTicket>& maybeSessionTicket)
    : ref_({connection, this}),
      context_(std::move(context)),
      session_(session) {
  Debug(session_, "Created new TLS session for %s", session->config().dcid);
  Initialize(maybeSessionTicket);
  if (!ossl_context_) [[unlikely]] {
    Debug(session_,
          "Failed to initialize TLS session: %s",
          validation_error_.empty() ? "unknown error" : validation_error_);
  }
}

TLSSession::operator SSL*() const {
  return ossl_context_;
}

bool TLSSession::early_data_was_accepted() const {
  CHECK_NE(ngtcp2_conn_get_handshake_completed(*session_), 0);
  return ossl_context_.get_early_data_accepted();
}

bool TLSSession::early_data_was_rejected() const {
  CHECK_NE(ngtcp2_conn_get_handshake_completed(*session_), 0);
  return ossl_context_.get_early_data_rejected();
}

bool TLSSession::early_data_was_attempted() const {
  CHECK_NE(ngtcp2_conn_get_handshake_completed(*session_), 0);
  return ossl_context_.get_early_data_attempted();
}

void TLSSession::Initialize(
    const std::optional<SessionTicket>& maybeSessionTicket) {
  auto& ctx = context();
  auto& options = ctx.options();
  auto ssl = SSLPointer::New(ctx);
  if (!ssl) [[unlikely]] {
    validation_error_ = "Failed to create SSL session";
    ossl_context_.reset();
    return;
  }

  // Enable tracing if the `--trace-tls` command line flag is used.
  if (session_->env()->options()->trace_tls || options.enable_tls_trace)
      [[unlikely]] {
    EnableTrace(session_->env(), &bio_trace_, ssl);
  }

  ossl_context_.Initialize(ssl.release(), &ref_, session(), ctx);

  switch (ctx.side()) {
    case Side::SERVER: {
      if (!ossl_context_.ConfigureServer()) [[unlikely]] {
        validation_error_ = "Failed to configure server session";
        ossl_context_.reset();
        return;
      }
      break;
    }
    case Side::CLIENT: {
      if (!ossl_context_.ConfigureClient()) [[unlikely]] {
        validation_error_ = "Failed to configure client session";
        ossl_context_.reset();
        return;
      };

      if (!ossl_context_.set_alpn_protocols(options.alpn)) {
        validation_error_ = "Failed to set ALPN protocols";
        ossl_context_.reset();
        return;
      }

      if (!ossl_context_.set_hostname(options.servername)) {
        validation_error_ = "Failed to set server name";
        ossl_context_.reset();
        return;
      }

      if (options.verify_hostname) {
        if (!ossl_context_.set_verify_hostname(options.servername)) {
          validation_error_ = "Failed to set verify hostname";
          ossl_context_.reset();
          return;
        }
      }

      if (maybeSessionTicket.has_value()) {
        const auto& sessionTicket = *maybeSessionTicket;
        uv_buf_t buf = sessionTicket.ticket();
        SSLSessionPointer ticket = crypto::GetTLSSession(
            reinterpret_cast<unsigned char*>(buf.base), buf.len);

        // The early data will just be ignored if it's invalid.
        if (ossl_context_.set_session_ticket(ticket)) {
          // Only enable 0-RTT if the option allows it. The session
          // ticket is still used for TLS resumption (1-RTT) either way.
          if (options.enable_early_data) {
            ngtcp2_vec rtp = sessionTicket.transport_params();
            if (ngtcp2_conn_decode_and_set_0rtt_transport_params(
                    *session_, rtp.base, rtp.len) == 0) {
              if (!ossl_context_.set_early_data_enabled()) {
                validation_error_ = "Failed to enable early data";
                ossl_context_.reset();
                return;
              }
              session_->SetStreamOpenAllowed();
              // Populate the state buffer from the 0-RTT transport
              // params so that maxDatagramSize and other values are
              // available before the handshake completes.
              session_->PopulateEarlyTransportParamsState();
            }
          }
        }
      }

      break;
    }
  }

  // Encode transport parameters directly into a stack buffer to avoid
  // a heap allocation. Transport parameters are typically < 256 bytes.
  {
    TransportParams tp(ngtcp2_conn_get_local_transport_params(*session_));
    // Preflight to get the encoded size.
    ssize_t size = tp.EncodedSize();
    if (size > 0) {
      MaybeStackBuffer<uint8_t, 512> buf(size);
      ssize_t written = tp.EncodeInto(buf.out(), size);
      if (written > 0) {
        ngtcp2_vec vec = {buf.out(), static_cast<size_t>(written)};
        if (!ossl_context_.set_transport_params(vec)) {
          validation_error_ = "Failed to set transport parameters";
          ossl_context_.reset();
          return;
        }
      }
    }
  }
}

std::optional<TLSSession::PeerIdentityValidationError>
TLSSession::VerifyPeerIdentity(Environment* env) {
  // We are just temporarily wrapping the ssl, not taking ownership.
  SSLPointerRef ssl(ossl_context_);
  int err = ssl->verifyPeerCertificate().value_or(X509_V_ERR_UNSPECIFIED);
  if (err == X509_V_OK) return std::nullopt;
  Local<Value> reason;
  Local<Value> code;
  if (!crypto::GetValidationErrorReason(env, err).ToLocal(&reason) ||
      !crypto::GetValidationErrorCode(env, err).ToLocal(&code)) {
    // Getting the validation error details failed. We'll return a value but
    // the fields will be empty.
    return PeerIdentityValidationError{};
  }
  return PeerIdentityValidationError{reason, code};
}

MaybeLocal<Object> TLSSession::cert(Environment* env) const {
  SSLPointerRef ssl(ossl_context_);
  return crypto::X509Certificate::GetCert(env, ssl);
}

MaybeLocal<Object> TLSSession::peer_cert(Environment* env) const {
  // We are just temporarily wrapping the ssl, not taking ownership.
  SSLPointerRef ssl(ossl_context_);
  crypto::X509Certificate::GetPeerCertificateFlag flag =
      context_->side() == Side::SERVER
          ? crypto::X509Certificate::GetPeerCertificateFlag::SERVER
          : crypto::X509Certificate::GetPeerCertificateFlag::NONE;
  return crypto::X509Certificate::GetPeerCert(env, ssl, flag);
}

MaybeLocal<Object> TLSSession::ephemeral_key(Environment* env) const {
  // We are just temporarily wrapping the ssl, not taking ownership.
  SSLPointerRef ssl(ossl_context_);
  return crypto::GetEphemeralKey(env, ssl);
}

MaybeLocal<Value> TLSSession::cipher_name(Environment* env) const {
  CHECK(ossl_context_);
  auto name = ossl_context_.get_cipher_name();
  return OneByteString(env->isolate(), name);
}

MaybeLocal<Value> TLSSession::cipher_version(Environment* env) const {
  SSLPointerRef ssl(ossl_context_);
  auto version = ssl->getCipherVersion();
  if (!version.has_value()) return Undefined(env->isolate());
  return OneByteString(env->isolate(), version.value());
}

const std::string_view TLSSession::servername() const {
  // A server caches the name from the ClientHello, which is available
  // earlier than SSL_get_servername() and stays available while the
  // handshake is paused. A client just reports what it asked for.
  if (context_->side() == Side::SERVER) return servername_;
  SSLPointerRef ssl(ossl_context_);
  return ssl->getServerName().value_or(std::string_view());
}

const std::string TLSSession::protocol() const {
  CHECK(ossl_context_);
  // As with servername(), a server has the answer cached from the
  // ClientHello; a client learns it from the ServerHello.
  if (context_->side() == Side::SERVER) return alpn_;
  return ossl_context_.get_selected_alpn();
}

bool TLSSession::InitiateKeyUpdate() {
  // ngtcp2 internally tracks key update state and will return an error
  // if a key update is already in progress.
  Debug(session_, "Initiating key update");
  return ngtcp2_conn_initiate_key_update(*session_, uv_hrtime()) == 0;
}

ngtcp2_conn* TLSSession::connection(ngtcp2_crypto_conn_ref* ref) {
  CHECK_NOT_NULL(ref->user_data);
  return static_cast<TLSSession*>(ref->user_data)->session();
}

void TLSSession::MemoryInfo(MemoryTracker* tracker) const {
  tracker->TrackField("context", context_);
}

}  // namespace quic
}  // namespace node
#endif  // OPENSSL_NO_QUIC