#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