#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include "tlscontext.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "bindingdata.h" #include "defs.h" #include "session.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 ncrypto::X509Pointer; using v8::ArrayBuffer; using v8::Just; using v8::Local; using v8::Maybe; using v8::MaybeLocal; using v8::Nothing; using v8::Object; using v8::Undefined; 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, 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 Opt::*member> bool SetOption(Environment* env, Opt* options, const v8::Local& object, const v8::Local& name) { v8::Local 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(); uint32_t count = values->Length(); for (uint32_t n = 0; n < count; n++) { v8::Local item; if (!values->Get(context, n).ToLocal(&item)) { return false; } if constexpr (std::is_same::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::value) { if (item->IsArrayBufferView()) { (options->*member).emplace_back(item.As()); } else if (item->IsArrayBuffer()) { (options->*member).emplace_back(item.As()); } 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::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::value) { if (value->IsArrayBufferView()) { (options->*member).emplace_back(value.As()); } else if (value->IsArrayBuffer()) { (options->*member).emplace_back(value.As()); } 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::CreateClient(const Options& options) { return std::make_shared(Side::CLIENT, options); } std::shared_ptr TLSContext::CreateServer(const Options& options) { return std::make_shared(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(out), outlen, reinterpret_cast(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(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 TLSContext::NewSession( Session* session, const std::optional& maybeSessionTicket) { // Passing a session ticket only makes sense with a client session. CHECK_IMPLIES(session->is_server(), !maybeSessionTicket.has_value()); return std::make_unique( session, shared_from_this(), maybeSessionTicket); } SSLCtxPointer TLSContext::Initialize() { SSLCtxPointer ctx; switch (side_) { case Side::SERVER: { static constexpr unsigned char kSidCtx[] = "Node.js QUIC Server"; ctx = SSLCtxPointer::NewServer(); 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 = SSLCtxPointer::NewClient(); 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 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()) { 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 { BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len); CHECK(bio); X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx.get()); while ( auto x509 = 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())); } } } } } { 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(); } } } } { ClearErrorOnReturn clear_error_on_return; for (const auto& key : options_.keys) { if (key.GetKeyType() != crypto::KeyType::kKeyTypePrivate) { validation_error_ = "Invalid key"; return SSLCtxPointer(); } if (!SSL_CTX_use_PrivateKey(ctx.get(), key.GetAsymmetricKey().get())) { 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); DeleteFnPtr crlptr(PEM_read_bio_X509_CRL( bio.get(), nullptr, crypto::NoPasswordCallback, nullptr)); if (!crlptr) { validation_error_ = "Invalid CRL"; return 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)); } } { 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; } void TLSContext::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("options", options_); } Maybe TLSContext::Options::From(Environment* env, Local value) { if (value.IsEmpty()) { return Nothing(); } 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(); } auto params = value.As(); #define SET_VECTOR(Type, name) \ SetOption( \ env, &options, params, state.name##_string()) #define SET(name) \ SetOption( \ 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(); } return Just(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(SSL_get_app_data(ssl)); CHECK_NOT_NULL(ref); return *static_cast(ref->user_data); } TLSSession::TLSSession(Session* session, std::shared_ptr context, const std::optional& 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; } SSLPointer TLSSession::Initialize( const std::optional& maybeSessionTicket) { auto& ctx = context(); auto& options = ctx.options(); 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(options.protocol.data()), options.protocol.size()) != 0) { validation_error_ = "Invalid ALPN"; return 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(); SSLSessionPointer ticket = crypto::GetTLSSession( reinterpret_cast(buf.base), buf.len); // The early data will just be ignored if it's invalid. if (ssl.setSession(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::VerifyPeerIdentity(Environment* env) { int err = ssl_.verifyPeerCertificate().value_or(X509_V_ERR_UNSPECIFIED); if (err == X509_V_OK) return std::nullopt; Local reason; Local 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 TLSSession::cert(Environment* env) const { return crypto::X509Certificate::GetCert(env, ssl_); } MaybeLocal 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 TLSSession::ephemeral_key(Environment* env) const { return crypto::GetEphemeralKey(env, ssl_); } MaybeLocal TLSSession::cipher_name(Environment* env) const { auto name = ssl_.getCipherName(); if (!name.has_value()) return Undefined(env->isolate()); return OneByteString(env->isolate(), name.value()); } MaybeLocal TLSSession::cipher_version(Environment* env) const { 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 { return ssl_.getServerName().value_or(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(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(ref->user_data)->session(); } } // namespace quic } // namespace node #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC