node/src/quic/tlscontext.cc
James M Snell 062ae6f3cb src, quic: refine more of the quic implementation
Signed-off-by: James M Snell <jasnell@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/56328
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
2025-01-06 10:47:36 -08:00

656 lines
22 KiB
C++

#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
#include "tlscontext.h"
#include <async_wrap-inl.h>
#include <base_object-inl.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_quictls.h>
#include <node_sockaddr-inl.h>
#include <openssl/ssl.h>
#include <v8.h>
#include "bindingdata.h"
#include "defs.h"
#include "session.h"
#include "transportparams.h"
namespace node {
using v8::ArrayBuffer;
using v8::Just;
using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Nothing;
using v8::Object;
using v8::Value;
namespace quic {
// ============================================================================
namespace {
// TODO(@jasnell): One time initialization. ngtcp2 says this is optional but
// highly recommended to deal with some perf regression. Unfortunately doing
// this breaks some existing tests and we need to understand the potential
// impact of calling this.
// auto _ = []() {
// CHECK_EQ(ngtcp2_crypto_quictls_init(), 0);
// return 0;
// }();
void EnableTrace(Environment* env, crypto::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 {
crypto::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 v8::Local<v8::Object>& object,
const v8::Local<v8::String>& name) {
v8::Local<v8::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<v8::Array>();
uint32_t count = values->Length();
for (uint32_t n = 0; n < count; n++) {
v8::Local<v8::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()) {
(options->*member).emplace_back(item.As<v8::ArrayBufferView>());
} else if (item->IsArrayBuffer()) {
(options->*member).emplace_back(item.As<v8::ArrayBuffer>());
} else {
Utf8Value namestr(env->isolate(), name);
THROW_ERR_INVALID_ARG_TYPE(
env, "%s value must be an array buffer", *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()) {
(options->*member).emplace_back(value.As<v8::ArrayBufferView>());
} else if (value->IsArrayBuffer()) {
(options->*member).emplace_back(value.As<v8::ArrayBuffer>());
} else {
Utf8Value namestr(env->isolate(), name);
THROW_ERR_INVALID_ARG_TYPE(
env, "%s value must be an array buffer", *namestr);
return false;
}
}
}
return true;
}
} // namespace
std::shared_ptr<TLSContext> TLSContext::CreateClient(const Options& options) {
return std::make_shared<TLSContext>(Side::CLIENT, options);
}
std::shared_ptr<TLSContext> TLSContext::CreateServer(const Options& options) {
return std::make_shared<TLSContext>(Side::SERVER, options);
}
TLSContext::TLSContext(Side side, const Options& options)
: side_(side), options_(options), ctx_(Initialize()) {}
TLSContext::operator SSL_CTX*() const {
DCHECK(ctx_);
return ctx_.get();
}
int TLSContext::OnSelectAlpn(SSL* ssl,
const unsigned char** out,
unsigned char* outlen,
const unsigned char* in,
unsigned int inlen,
void* arg) {
static constexpr size_t kMaxAlpnLen = 255;
auto& session = TLSSession::From(ssl);
const auto& requested = session.context().options().protocol;
if (requested.length() > kMaxAlpnLen) return SSL_TLSEXT_ERR_NOACK;
// The Session supports exactly one ALPN identifier. If that does not match
// any of the ALPN identifiers provided in the client request, then we fail
// here. Note that this will not fail the TLS handshake, so we have to check
// later if the ALPN matches the expected identifier or not.
//
// We might eventually want to support the ability to negotiate multiple
// possible ALPN's on a single endpoint/session but for now, we only support
// one.
if (SSL_select_next_proto(
const_cast<unsigned char**>(out),
outlen,
reinterpret_cast<const unsigned char*>(requested.data()),
requested.length(),
in,
inlen) == OPENSSL_NPN_NO_OVERLAP) {
Debug(&session.session(), "ALPN negotiation failed");
return SSL_TLSEXT_ERR_NOACK;
}
Debug(&session.session(), "ALPN negotiation succeeded");
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 or the size is greater than our max, let's ignore it
// and continue without emitting the sessionticket event.
if (size > 0 && size <= crypto::SecureContext::kMaxSessionSize) {
auto ticket =
ArrayBuffer::NewBackingStore(session.env()->isolate(), size);
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) {
// TODO(@jasnell): Implement the logic to verify the client certificate
return 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());
return std::make_unique<TLSSession>(
session, shared_from_this(), maybeSessionTicket);
}
crypto::SSLCtxPointer TLSContext::Initialize() {
crypto::SSLCtxPointer ctx;
switch (side_) {
case Side::SERVER: {
static constexpr unsigned char kSidCtx[] = "Node.js QUIC Server";
ctx.reset(SSL_CTX_new(TLS_server_method()));
CHECK_EQ(ngtcp2_crypto_quictls_configure_server_context(ctx.get()), 0);
CHECK_EQ(SSL_CTX_set_max_early_data(ctx.get(), UINT32_MAX), 1);
SSL_CTX_set_options(ctx.get(),
(SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) |
SSL_OP_SINGLE_ECDH_USE |
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);
CHECK_EQ(SSL_CTX_set_session_id_context(
ctx.get(), kSidCtx, sizeof(kSidCtx) - 1),
1);
if (options_.verify_client) {
SSL_CTX_set_verify(ctx.get(),
SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE |
SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
OnVerifyClientCertificate);
}
// TODO(@jasnell): There's a bug int the GenerateCallback flow somewhere.
// Need to update in order to support session tickets.
// CHECK_EQ(SSL_CTX_set_session_ticket_cb(ctx.get(),
// SessionTicket::GenerateCallback,
// SessionTicket::DecryptedCallback,
// nullptr),
// 1);
break;
}
case Side::CLIENT: {
ctx.reset(SSL_CTX_new(TLS_client_method()));
CHECK_EQ(ngtcp2_crypto_quictls_configure_client_context(ctx.get()), 0);
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);
break;
}
}
SSL_CTX_set_default_verify_paths(ctx.get());
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 crypto::SSLCtxPointer();
}
if (SSL_CTX_set1_groups_list(ctx.get(), options_.groups.c_str()) != 1) {
validation_error_ = "Invalid cipher groups";
return crypto::SSLCtxPointer();
}
{
crypto::ClearErrorOnReturn clear_error_on_return;
if (options_.ca.empty()) {
auto store = crypto::GetOrCreateRootCertStore();
X509_STORE_up_ref(store);
SSL_CTX_set_cert_store(ctx.get(), store);
} else {
for (const auto& ca : options_.ca) {
uv_buf_t buf = ca;
if (buf.len == 0) {
auto store = crypto::GetOrCreateRootCertStore();
X509_STORE_up_ref(store);
SSL_CTX_set_cert_store(ctx.get(), store);
} else {
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len);
CHECK(bio);
X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx.get());
while (crypto::X509Pointer x509 = crypto::X509Pointer(
PEM_read_bio_X509_AUX(bio.get(),
nullptr,
crypto::NoPasswordCallback,
nullptr))) {
if (cert_store == crypto::GetOrCreateRootCertStore()) {
cert_store = crypto::NewRootCertStore();
SSL_CTX_set_cert_store(ctx.get(), cert_store);
}
CHECK_EQ(1, X509_STORE_add_cert(cert_store, x509.get()));
CHECK_EQ(1, SSL_CTX_add_client_CA(ctx.get(), x509.get()));
}
}
}
}
}
{
crypto::ClearErrorOnReturn clear_error_on_return;
for (const auto& cert : options_.certs) {
uv_buf_t buf = cert;
if (buf.len > 0) {
crypto::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 crypto::SSLCtxPointer();
}
}
}
}
{
crypto::ClearErrorOnReturn clear_error_on_return;
for (const auto& key : options_.keys) {
if (key.GetKeyType() != crypto::KeyType::kKeyTypePrivate) {
validation_error_ = "Invalid key";
return crypto::SSLCtxPointer();
}
if (!SSL_CTX_use_PrivateKey(ctx.get(), key.GetAsymmetricKey().get())) {
validation_error_ = "Invalid key";
return crypto::SSLCtxPointer();
}
}
}
{
crypto::ClearErrorOnReturn clear_error_on_return;
for (const auto& crl : options_.crl) {
uv_buf_t buf = crl;
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len);
DeleteFnPtr<X509_CRL, X509_CRL_free> crlptr(PEM_read_bio_X509_CRL(
bio.get(), nullptr, crypto::NoPasswordCallback, nullptr));
if (!crlptr) {
validation_error_ = "Invalid CRL";
return crypto::SSLCtxPointer();
}
X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx.get());
if (cert_store == crypto::GetOrCreateRootCertStore()) {
cert_store = crypto::NewRootCertStore();
SSL_CTX_set_cert_store(ctx.get(), cert_store);
}
CHECK_EQ(1, X509_STORE_add_crl(cert_store, crlptr.get()));
CHECK_EQ(
1,
X509_STORE_set_flags(
cert_store, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL));
}
}
{
crypto::ClearErrorOnReturn clear_error_on_return;
if (options_.verify_private_key &&
SSL_CTX_check_private_key(ctx.get()) != 1) {
validation_error_ = "Invalid private key";
return crypto::SSLCtxPointer();
}
}
return ctx;
}
void TLSContext::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("options", options_);
}
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(TLSContext::Options::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(enable_tls_trace) || !SET(protocol) ||
!SET(servername) || !SET(ciphers) || !SET(groups) ||
!SET(verify_private_key) || !SET(keylog) ||
!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 + "protocol: " + protocol;
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 + "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 + "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 = {};
// ============================================================================
const 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),
ssl_(Initialize(maybeSessionTicket)) {
Debug(session_, "Created new TLS session for %s", session->config().dcid);
}
TLSSession::~TLSSession() {
if (ssl_) {
SSL_set_app_data(ssl_.get(), nullptr);
}
}
TLSSession::operator SSL*() const {
CHECK(ssl_);
return ssl_.get();
}
bool TLSSession::early_data_was_accepted() const {
CHECK_NE(ngtcp2_conn_get_handshake_completed(*session_), 0);
return SSL_get_early_data_status(*this) == SSL_EARLY_DATA_ACCEPTED;
}
crypto::SSLPointer TLSSession::Initialize(
const std::optional<SessionTicket>& maybeSessionTicket) {
auto& ctx = context();
auto& options = ctx.options();
crypto::SSLPointer ssl(SSL_new(ctx));
SSL_set_app_data(ssl.get(), &ref_);
ngtcp2_conn_set_tls_native_handle(*session_, ssl.get());
// 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_, *this);
}
switch (ctx.side()) {
case Side::SERVER: {
SSL_set_accept_state(ssl.get());
SSL_set_quic_early_data_enabled(ssl.get(), 1);
break;
}
case Side::CLIENT: {
SSL_set_connect_state(ssl.get());
if (SSL_set_alpn_protos(
ssl.get(),
reinterpret_cast<const unsigned char*>(options.protocol.data()),
options.protocol.size()) != 0) {
validation_error_ = "Invalid ALPN";
return crypto::SSLPointer();
}
if (!options.servername.empty()) {
SSL_set_tlsext_host_name(ssl.get(), options.servername.data());
} else {
SSL_set_tlsext_host_name(ssl.get(), "localhost");
}
if (maybeSessionTicket.has_value()) {
auto sessionTicket = maybeSessionTicket.value();
uv_buf_t buf = sessionTicket.ticket();
crypto::SSLSessionPointer ticket = crypto::GetTLSSession(
reinterpret_cast<unsigned char*>(buf.base), buf.len);
// The early data will just be ignored if it's invalid.
if (crypto::SetTLSSession(ssl, ticket) &&
SSL_SESSION_get_max_early_data(ticket.get()) != 0) {
ngtcp2_vec rtp = sessionTicket.transport_params();
if (ngtcp2_conn_decode_and_set_0rtt_transport_params(
*session_, rtp.base, rtp.len) == 0) {
SSL_set_quic_early_data_enabled(ssl.get(), 1);
session_->SetStreamOpenAllowed();
}
}
}
break;
}
}
TransportParams tp(ngtcp2_conn_get_local_transport_params(*session_));
Store store = tp.Encode(session_->env());
if (store && store.length() > 0) {
ngtcp2_vec vec = store;
SSL_set_quic_transport_params(ssl.get(), vec.base, vec.len);
}
return ssl;
}
std::optional<TLSSession::PeerIdentityValidationError>
TLSSession::VerifyPeerIdentity(Environment* env) {
int err = crypto::VerifyPeerCertificate(ssl_);
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 {
return crypto::X509Certificate::GetCert(env, ssl_);
}
MaybeLocal<Object> TLSSession::peer_cert(Environment* env) const {
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 {
return crypto::GetEphemeralKey(env, ssl_);
}
MaybeLocal<Value> TLSSession::cipher_name(Environment* env) const {
return crypto::GetCurrentCipherName(env, ssl_);
}
MaybeLocal<Value> TLSSession::cipher_version(Environment* env) const {
return crypto::GetCurrentCipherVersion(env, ssl_);
}
const std::string_view TLSSession::servername() const {
const char* servername = crypto::GetServerName(ssl_.get());
return servername != nullptr ? std::string_view(servername)
: std::string_view();
}
const std::string_view TLSSession::protocol() const {
const unsigned char* alpn_buf = nullptr;
unsigned int alpnlen;
SSL_get0_alpn_selected(ssl_.get(), &alpn_buf, &alpnlen);
return alpnlen ? std::string_view(reinterpret_cast<const char*>(alpn_buf),
alpnlen)
: std::string_view();
}
bool TLSSession::InitiateKeyUpdate() {
if (in_key_update_) return false;
auto leave = OnScopeLeave([this] { in_key_update_ = false; });
in_key_update_ = true;
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();
}
} // namespace quic
} // namespace node
#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC