%PDF- %PDF-
| Direktori : /home/vacivi36/vittasync.vacivitta.com.br/vittasync/node/src/quic/ |
| Current File : /home/vacivi36/vittasync.vacivitta.com.br/vittasync/node/src/quic/tlscontext.cc |
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
#include "tlscontext.h"
#include <async_wrap-inl.h>
#include <base_object-inl.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_process-inl.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::BackingStore;
using v8::Just;
using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Nothing;
using v8::Object;
using v8::Value;
namespace quic {
const TLSContext::Options TLSContext::Options::kDefault = {};
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;
// }();
constexpr size_t kMaxAlpnLen = 255;
int AllowEarlyDataCallback(SSL* ssl, void* arg) {
// Currently, we always allow early data. Later we might make
// it configurable.
return 1;
}
int NewSessionCallback(SSL* ssl, SSL_SESSION* session) {
// We use this event to trigger generation of the SessionTicket
// if the user has requested to receive it.
return TLSContext::From(ssl).OnNewSession(session);
}
void KeylogCallback(const SSL* ssl, const char* line) {
TLSContext::From(ssl).Keylog(line);
}
int AlpnSelectionCallback(SSL* ssl,
const unsigned char** out,
unsigned char* outlen,
const unsigned char* in,
unsigned int inlen,
void* arg) {
auto& context = TLSContext::From(ssl);
auto requested = context.options().alpn;
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.c_str()),
requested.length(),
in,
inlen) == OPENSSL_NPN_NO_OVERLAP) {
return SSL_TLSEXT_ERR_NOACK;
}
return SSL_TLSEXT_ERR_OK;
}
BaseObjectPtr<crypto::SecureContext> InitializeSecureContext(
Session* session,
Side side,
Environment* env,
const TLSContext::Options& options) {
auto context = crypto::SecureContext::Create(env);
auto& ctx = context->ctx();
switch (side) {
case Side::SERVER: {
Debug(session, "Initializing secure context for server");
ctx.reset(SSL_CTX_new(TLS_server_method()));
SSL_CTX_set_app_data(ctx.get(), context);
if (ngtcp2_crypto_quictls_configure_server_context(ctx.get()) != 0) {
return BaseObjectPtr<crypto::SecureContext>();
}
SSL_CTX_set_max_early_data(ctx.get(), UINT32_MAX);
SSL_CTX_set_allow_early_data_cb(
ctx.get(), AllowEarlyDataCallback, nullptr);
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(), AlpnSelectionCallback, nullptr);
SSL_CTX_set_session_ticket_cb(ctx.get(),
SessionTicket::GenerateCallback,
SessionTicket::DecryptedCallback,
nullptr);
const unsigned char* sid_ctx = reinterpret_cast<const unsigned char*>(
options.session_id_ctx.c_str());
SSL_CTX_set_session_id_context(
ctx.get(), sid_ctx, options.session_id_ctx.length());
break;
}
case Side::CLIENT: {
Debug(session, "Initializing secure context for client");
ctx.reset(SSL_CTX_new(TLS_client_method()));
SSL_CTX_set_app_data(ctx.get(), context);
if (ngtcp2_crypto_quictls_configure_client_context(ctx.get()) != 0) {
return BaseObjectPtr<crypto::SecureContext>();
}
SSL_CTX_set_session_cache_mode(
ctx.get(), SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL_STORE);
SSL_CTX_sess_set_new_cb(ctx.get(), NewSessionCallback);
break;
}
default:
UNREACHABLE();
}
SSL_CTX_set_default_verify_paths(ctx.get());
if (options.keylog) SSL_CTX_set_keylog_callback(ctx.get(), KeylogCallback);
if (SSL_CTX_set_ciphersuites(ctx.get(), options.ciphers.c_str()) != 1) {
return BaseObjectPtr<crypto::SecureContext>();
}
if (SSL_CTX_set1_groups_list(ctx.get(), options.groups.c_str()) != 1) {
return BaseObjectPtr<crypto::SecureContext>();
}
// Handle CA certificates...
const auto addCACert = [&](uv_buf_t ca) {
crypto::ClearErrorOnReturn clear_error_on_return;
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(ca.base, ca.len);
if (!bio) return false;
context->SetCACert(bio);
return true;
};
const auto addRootCerts = [&] {
crypto::ClearErrorOnReturn clear_error_on_return;
context->SetRootCerts();
};
if (!options.ca.empty()) {
for (auto& ca : options.ca) {
if (!addCACert(ca)) {
return BaseObjectPtr<crypto::SecureContext>();
}
}
} else {
addRootCerts();
}
// Handle Certs
const auto addCert = [&](uv_buf_t cert) {
crypto::ClearErrorOnReturn clear_error_on_return;
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(cert.base, cert.len);
if (!bio) return Just(false);
auto ret = context->AddCert(env, std::move(bio));
return ret;
};
for (auto& cert : options.certs) {
if (!addCert(cert).IsJust()) {
return BaseObjectPtr<crypto::SecureContext>();
}
}
// Handle keys
const auto addKey = [&](auto& key) {
crypto::ClearErrorOnReturn clear_error_on_return;
return context->UseKey(env, key);
// TODO(@jasnell): Maybe SSL_CTX_check_private_key also?
};
for (auto& key : options.keys) {
if (!addKey(key).IsJust()) {
return BaseObjectPtr<crypto::SecureContext>();
}
}
// Handle CRL
const auto addCRL = [&](uv_buf_t crl) {
crypto::ClearErrorOnReturn clear_error_on_return;
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(crl.base, crl.len);
if (!bio) return Just(false);
return context->SetCRL(env, bio);
};
for (auto& crl : options.crl) {
if (!addCRL(crl).IsJust()) {
return BaseObjectPtr<crypto::SecureContext>();
}
}
// TODO(@jasnell): Possibly handle other bits. Such a pfx, client cert engine,
// and session timeout.
return BaseObjectPtr<crypto::SecureContext>(context);
}
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, std::shared_ptr<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());
} 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,
std::shared_ptr<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());
} 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
Side TLSContext::side() const {
return side_;
}
const TLSContext::Options& TLSContext::options() const {
return options_;
}
inline const TLSContext& TLSContext::From(const SSL* ssl) {
auto ref = static_cast<ngtcp2_crypto_conn_ref*>(SSL_get_app_data(ssl));
TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref);
return *context;
}
inline TLSContext& TLSContext::From(SSL* ssl) {
auto ref = static_cast<ngtcp2_crypto_conn_ref*>(SSL_get_app_data(ssl));
TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref);
return *context;
}
TLSContext::TLSContext(Environment* env,
Side side,
Session* session,
const Options& options)
: conn_ref_({getConnection, this}),
side_(side),
env_(env),
session_(session),
options_(options),
secure_context_(InitializeSecureContext(session, side, env, options)) {
CHECK(secure_context_);
ssl_.reset(SSL_new(secure_context_->ctx().get()));
CHECK(ssl_ && SSL_is_quic(ssl_.get()));
SSL_set_app_data(ssl_.get(), &conn_ref_);
SSL_set_verify(ssl_.get(), SSL_VERIFY_NONE, crypto::VerifyCallback);
// Enable tracing if the `--trace-tls` command line flag is used.
if (UNLIKELY(env->options()->trace_tls || options.enable_tls_trace))
EnableTrace(env, &bio_trace_, ssl_.get());
switch (side) {
case Side::CLIENT: {
SSL_set_connect_state(ssl_.get());
CHECK_EQ(0,
SSL_set_alpn_protos(ssl_.get(),
reinterpret_cast<const unsigned char*>(
options_.alpn.c_str()),
options_.alpn.length()));
CHECK_EQ(0,
SSL_set_tlsext_host_name(ssl_.get(), options_.hostname.c_str()));
break;
}
case Side::SERVER: {
SSL_set_accept_state(ssl_.get());
if (options.request_peer_certificate) {
int verify_mode = SSL_VERIFY_PEER;
if (options.reject_unauthorized)
verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
SSL_set_verify(ssl_.get(), verify_mode, crypto::VerifyCallback);
}
break;
}
default:
UNREACHABLE();
}
}
void TLSContext::Start() {
Debug(session_, "Crypto context is starting");
ngtcp2_conn_set_tls_native_handle(*session_, ssl_.get());
TransportParams tp(ngtcp2_conn_get_local_transport_params(*session_));
Store store = tp.Encode(env_);
if (store && store.length() > 0) {
ngtcp2_vec vec = store;
SSL_set_quic_transport_params(ssl_.get(), vec.base, vec.len);
}
}
void TLSContext::Keylog(const char* line) const {
session_->EmitKeylog(line);
}
int TLSContext::OnNewSession(SSL_SESSION* session) {
Debug(session_, "Crypto context received new crypto session");
// Used to generate and emit a SessionTicket for TLS session resumption.
// If there is nothing listening for the session ticket, don't both emitting.
if (!session_->wants_session_ticket()) return 0;
// Pre-fight to see how much space we need to allocate for the session ticket.
size_t size = i2d_SSL_SESSION(session, nullptr);
if (size > 0 && size < crypto::SecureContext::kMaxSessionSize) {
// Generate the actual ticket. If this fails, we'll simply carry on without
// emitting the ticket.
std::shared_ptr<BackingStore> ticket =
ArrayBuffer::NewBackingStore(env_->isolate(), size);
unsigned char* data = reinterpret_cast<unsigned char*>(ticket->Data());
if (i2d_SSL_SESSION(session, &data) <= 0) return 0;
session_->EmitSessionTicket(Store(std::move(ticket), size));
}
// If size == 0, there's no session ticket data to emit. Let's ignore it
// and continue without emitting the sessionticket event.
return 0;
}
bool TLSContext::InitiateKeyUpdate() {
Debug(session_, "Crypto context initiating key update");
if (session_->is_destroyed() || in_key_update_) return false;
auto leave = OnScopeLeave([this] { in_key_update_ = false; });
in_key_update_ = true;
return ngtcp2_conn_initiate_key_update(*session_, uv_hrtime()) == 0;
}
int TLSContext::VerifyPeerIdentity() {
Debug(session_, "Crypto context verifying peer identity");
return crypto::VerifyPeerCertificate(ssl_);
}
void TLSContext::MaybeSetEarlySession(const SessionTicket& sessionTicket) {
Debug(session_, "Crypto context setting early session");
uv_buf_t buf = sessionTicket.ticket();
crypto::SSLSessionPointer ticket = crypto::GetTLSSession(
reinterpret_cast<unsigned char*>(buf.base), buf.len);
// Silently ignore invalid TLS session
if (!ticket || !SSL_SESSION_get_max_early_data(ticket.get())) return;
// The early data will just be ignored if it's invalid.
if (!crypto::SetTLSSession(ssl_, ticket)) return;
ngtcp2_vec rtp = sessionTicket.transport_params();
// Decode and attempt to set the early transport parameters configured
// for the early session. If non-zero is returned, decoding or setting
// failed, in which case we just ignore it.
if (ngtcp2_conn_decode_and_set_0rtt_transport_params(
*session_, rtp.base, rtp.len) != 0)
return;
session_->SetStreamOpenAllowed();
}
void TLSContext::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("options", options_);
tracker->TrackField("secure_context", secure_context_);
}
MaybeLocal<Object> TLSContext::cert(Environment* env) const {
return crypto::X509Certificate::GetCert(env, ssl_);
}
MaybeLocal<Object> TLSContext::peer_cert(Environment* env) const {
crypto::X509Certificate::GetPeerCertificateFlag flag =
side_ == Side::SERVER
? crypto::X509Certificate::GetPeerCertificateFlag::SERVER
: crypto::X509Certificate::GetPeerCertificateFlag::NONE;
return crypto::X509Certificate::GetPeerCert(env, ssl_, flag);
}
MaybeLocal<Value> TLSContext::cipher_name(Environment* env) const {
return crypto::GetCurrentCipherName(env, ssl_);
}
MaybeLocal<Value> TLSContext::cipher_version(Environment* env) const {
return crypto::GetCurrentCipherVersion(env, ssl_);
}
MaybeLocal<Object> TLSContext::ephemeral_key(Environment* env) const {
return crypto::GetEphemeralKey(env, ssl_);
}
const std::string_view TLSContext::servername() const {
const char* servername = crypto::GetServerName(ssl_.get());
return servername != nullptr ? std::string_view(servername)
: std::string_view();
}
const std::string_view TLSContext::alpn() 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 TLSContext::early_data_was_accepted() const {
return (early_data_ &&
SSL_get_early_data_status(ssl_.get()) == SSL_EARLY_DATA_ACCEPTED);
}
void TLSContext::Options::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("keys", keys);
tracker->TrackField("certs", certs);
tracker->TrackField("ca", ca);
tracker->TrackField("crl", crl);
}
ngtcp2_conn* TLSContext::getConnection(ngtcp2_crypto_conn_ref* ref) {
TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref);
return *context->session_;
}
Maybe<TLSContext::Options> TLSContext::Options::From(Environment* env,
Local<Value> value) {
if (value.IsEmpty()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing<Options>();
}
Options options;
auto& state = BindingData::Get(env);
if (value->IsUndefined()) {
// We need at least one key and one cert to complete the tls handshake.
// Why not make this an error? We could but it's not strictly necessary.
env->EmitProcessEnvWarning();
ProcessEmitWarning(
env,
"The default QUIC TLS options are being used. "
"This means there is no key or certificate configured and the "
"TLS handshake will fail. This is likely not what you want.");
return Just<Options>(options);
}
if (!value->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env, "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(keylog) || !SET(reject_unauthorized) || !SET(enable_tls_trace) ||
!SET(request_peer_certificate) || !SET(verify_hostname_identity) ||
!SET(alpn) || !SET(hostname) || !SET(session_id_ctx) || !SET(ciphers) ||
!SET(groups) ||
!SET_VECTOR(std::shared_ptr<crypto::KeyObjectData>, keys) ||
!SET_VECTOR(Store, certs) || !SET_VECTOR(Store, ca) ||
!SET_VECTOR(Store, crl)) {
return Nothing<Options>();
}
// We need at least one key and one cert to complete the tls handshake.
// Why not make this an error? We could but it's not strictly necessary.
if (options.keys.empty() || options.certs.empty()) {
env->EmitProcessEnvWarning();
ProcessEmitWarning(env,
"The QUIC TLS options did not include a key or cert. "
"This means the TLS handshake will fail. This is likely "
"not what you want.");
}
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 + "hostname: " + hostname;
res +=
prefix + "keylog: " + (keylog ? std::string("yes") : std::string("no"));
res += prefix + "reject_unauthorized: " +
(reject_unauthorized ? std::string("yes") : std::string("no"));
res += prefix + "enable_tls_trace: " +
(enable_tls_trace ? std::string("yes") : std::string("no"));
res += prefix + "request_peer_certificate: " +
(request_peer_certificate ? std::string("yes") : std::string("no"));
res += prefix + "verify_hostname_identity: " +
(verify_hostname_identity ? std::string("yes") : std::string("no"));
res += prefix + "session_id_ctx: " + session_id_ctx;
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;
}
} // namespace quic
} // namespace node
#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC