Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

quic: add additional utilities for quic (part 2) #47289

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,13 @@
'src/quic/cid.cc',
'src/quic/data.cc',
'src/quic/preferredaddress.cc',
'src/quic/sessionticket.cc',
'src/quic/tokens.cc',
'src/quic/cid.h',
'src/quic/data.h',
'src/quic/preferredaddress.h',
'src/quic/sessionticket.h',
'src/quic/tokens.h',
],
'node_mksnapshot_exec': '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)node_mksnapshot<(EXECUTABLE_SUFFIX)',
'conditions': [
Expand Down Expand Up @@ -1033,6 +1037,7 @@
'test/cctest/test_crypto_clienthello.cc',
'test/cctest/test_node_crypto.cc',
'test/cctest/test_quic_cid.cc',
'test/cctest/test_quic_tokens.cc',
]
}],
['v8_enable_inspector==1', {
Expand Down
9 changes: 9 additions & 0 deletions src/quic/data.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ using v8::BigInt;
using v8::Integer;
using v8::Local;
using v8::MaybeLocal;
using v8::Uint8Array;
using v8::Undefined;
using v8::Value;

Expand Down Expand Up @@ -66,6 +67,14 @@ Store::Store(v8::Local<v8::ArrayBufferView> view, Option option)
}
}

v8::Local<v8::Uint8Array> Store::ToUint8Array(Environment* env) const {
return !store_
? Uint8Array::New(v8::ArrayBuffer::New(env->isolate(), 0), 0, 0)
: Uint8Array::New(v8::ArrayBuffer::New(env->isolate(), store_),
offset_,
length_);
}

Store::operator bool() const {
return store_ != nullptr;
}
Expand Down
3 changes: 3 additions & 0 deletions src/quic/data.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC

#include <env.h>
#include <memory_tracker.h>
#include <nghttp3/nghttp3.h>
#include <ngtcp2/ngtcp2.h>
Expand Down Expand Up @@ -41,6 +42,8 @@ class Store final : public MemoryRetainer {
Store(v8::Local<v8::ArrayBuffer> buffer, Option option = Option::NONE);
Store(v8::Local<v8::ArrayBufferView> view, Option option = Option::NONE);

v8::Local<v8::Uint8Array> ToUint8Array(Environment* env) const;

operator uv_buf_t() const;
operator ngtcp2_vec() const;
operator nghttp3_vec() const;
Expand Down
177 changes: 177 additions & 0 deletions src/quic/sessionticket.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC

#include "sessionticket.h"
#include <env-inl.h>
#include <memory_tracker-inl.h>
#include <ngtcp2/ngtcp2_crypto.h>
#include <node_buffer.h>
#include <node_errors.h>

namespace node {

using v8::ArrayBufferView;
using v8::Just;
using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Nothing;
using v8::Object;
using v8::Value;
using v8::ValueDeserializer;
using v8::ValueSerializer;

namespace quic {

namespace {
SessionTicket::AppData::Source* GetAppDataSource(SSL* ssl) {
ngtcp2_crypto_conn_ref* ref =
static_cast<ngtcp2_crypto_conn_ref*>(SSL_get_app_data(ssl));
if (ref != nullptr && ref->user_data != nullptr) {
return static_cast<SessionTicket::AppData::Source*>(ref->user_data);
}
return nullptr;
}
} // namespace

SessionTicket::SessionTicket(Store&& ticket, Store&& transport_params)
: ticket_(std::move(ticket)),
transport_params_(std::move(transport_params)) {}

Maybe<SessionTicket> SessionTicket::FromV8Value(Environment* env,
v8::Local<v8::Value> value) {
if (!value->IsArrayBufferView()) {
THROW_ERR_INVALID_ARG_TYPE(env, "The ticket must be an ArrayBufferView.");
return Nothing<SessionTicket>();
}

Store content(value.As<ArrayBufferView>());
ngtcp2_vec vec = content;

ValueDeserializer des(env->isolate(), vec.base, vec.len);

if (des.ReadHeader(env->context()).IsNothing()) {
THROW_ERR_INVALID_ARG_VALUE(env, "The ticket format is invalid.");
return Nothing<SessionTicket>();
}

Local<Value> ticket;
Local<Value> transport_params;

errors::TryCatchScope tryCatch(env);

if (!des.ReadValue(env->context()).ToLocal(&ticket) ||
!des.ReadValue(env->context()).ToLocal(&transport_params) ||
!ticket->IsArrayBufferView() || !transport_params->IsArrayBufferView()) {
if (tryCatch.HasCaught()) {
// Any errors thrown we want to catch and supress. The only
// error we want to expose to the user is that the ticket format
// is invalid.
if (!tryCatch.HasTerminated()) {
THROW_ERR_INVALID_ARG_VALUE(env, "The ticket format is invalid.");
tryCatch.ReThrow();
}
return Nothing<SessionTicket>();
}
THROW_ERR_INVALID_ARG_VALUE(env, "The ticket format is invalid.");
return Nothing<SessionTicket>();
}

return Just(SessionTicket(Store(ticket.As<ArrayBufferView>()),
Store(transport_params.As<ArrayBufferView>())));
}

MaybeLocal<Object> SessionTicket::encode(Environment* env) const {
auto context = env->context();
ValueSerializer ser(env->isolate());
ser.WriteHeader();

if (ser.WriteValue(context, ticket_.ToUint8Array(env)).IsNothing() ||
ser.WriteValue(context, transport_params_.ToUint8Array(env))
.IsNothing()) {
return MaybeLocal<Object>();
}

auto result = ser.Release();

return Buffer::New(env, reinterpret_cast<char*>(result.first), result.second);
}

const uv_buf_t SessionTicket::ticket() const {
return ticket_;
}

const ngtcp2_vec SessionTicket::transport_params() const {
return transport_params_;
}

void SessionTicket::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("ticket", ticket_);
tracker->TrackField("transport_params", transport_params_);
}

int SessionTicket::GenerateCallback(SSL* ssl, void* arg) {
SessionTicket::AppData::Collect(ssl);
return 1;
}

SSL_TICKET_RETURN SessionTicket::DecryptedCallback(SSL* ssl,
SSL_SESSION* session,
const unsigned char* keyname,
size_t keyname_len,
SSL_TICKET_STATUS status,
void* arg) {
switch (status) {
default:
return SSL_TICKET_RETURN_IGNORE;
case SSL_TICKET_EMPTY:
[[fallthrough]];
case SSL_TICKET_NO_DECRYPT:
return SSL_TICKET_RETURN_IGNORE_RENEW;
case SSL_TICKET_SUCCESS_RENEW:
[[fallthrough]];
case SSL_TICKET_SUCCESS:
return static_cast<SSL_TICKET_RETURN>(
SessionTicket::AppData::Extract(ssl));
}
}

SessionTicket::AppData::AppData(SSL* ssl) : ssl_(ssl) {}

bool SessionTicket::AppData::Set(const uv_buf_t& data) {
if (set_ || data.base == nullptr || data.len == 0) return false;
set_ = true;
SSL_SESSION_set1_ticket_appdata(SSL_get0_session(ssl_), data.base, data.len);
return set_;
}

std::optional<const uv_buf_t> SessionTicket::AppData::Get() const {
uv_buf_t buf;
int ret =
SSL_SESSION_get0_ticket_appdata(SSL_get0_session(ssl_),
reinterpret_cast<void**>(&buf.base),
reinterpret_cast<size_t*>(&buf.len));
if (ret != 1) return std::nullopt;
return buf;
}

void SessionTicket::AppData::Collect(SSL* ssl) {
auto source = GetAppDataSource(ssl);
if (source != nullptr) {
SessionTicket::AppData app_data(ssl);
source->CollectSessionTicketAppData(&app_data);
}
}

SessionTicket::AppData::Status SessionTicket::AppData::Extract(SSL* ssl) {
auto source = GetAppDataSource(ssl);
if (source != nullptr) {
SessionTicket::AppData app_data(ssl);
return source->ExtractSessionTicketAppData(app_data);
}
return Status::TICKET_IGNORE;
}

} // namespace quic
} // namespace node

#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
112 changes: 112 additions & 0 deletions src/quic/sessionticket.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#pragma once

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC

#include <crypto/crypto_common.h>
#include <env.h>
#include <memory_tracker.h>
#include <uv.h>
#include <v8.h>
#include "data.h"

namespace node {
namespace quic {

// A TLS 1.3 Session resumption ticket. Encapsulates both the TLS
anonrig marked this conversation as resolved.
Show resolved Hide resolved
// ticket and the encoded QUIC transport parameters. The encoded
// structure should be considered to be opaque for end users.
// In JavaScript, the ticket will be represented as a Buffer
// instance with opaque data. To resume a session, the user code
// would pass that Buffer back into to client connection API.
class SessionTicket final : public MemoryRetainer {
public:
static v8::Maybe<SessionTicket> FromV8Value(Environment* env,
v8::Local<v8::Value> value);

SessionTicket() = default;
SessionTicket(Store&& ticket, Store&& transport_params);

const uv_buf_t ticket() const;

const ngtcp2_vec transport_params() const;

v8::MaybeLocal<v8::Object> encode(Environment* env) const;

void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(SessionTicket)
SET_SELF_SIZE(SessionTicket)

class AppData;

// The callback that OpenSSL will call when generating the session ticket
// and it needs to collect additional application specific data.
static int GenerateCallback(SSL* ssl, void* arg);

// The callback that OpenSSL will call when consuming the session ticket
// and it needs to pass embedded application data back into the app.
static SSL_TICKET_RETURN DecryptedCallback(SSL* ssl,
SSL_SESSION* session,
const unsigned char* keyname,
size_t keyname_len,
SSL_TICKET_STATUS status,
void* arg);

private:
Store ticket_;
Store transport_params_;
};

// SessionTicket::AppData is a utility class that is used only during the
// generation or access of TLS stateless sesson tickets. It exists solely to
// provide a easier way for Session::Application instances to set relevant
// metadata in the session ticket when it is created, and the exract and
// subsequently verify that data when a ticket is received and is being
// validated. The app data is completely opaque to anything other than the
// server-side of the Session::Application that sets it.
class SessionTicket::AppData final {
public:
enum class Status {
TICKET_IGNORE = SSL_TICKET_RETURN_IGNORE,
TICKET_IGNORE_RENEW = SSL_TICKET_RETURN_IGNORE_RENEW,
TICKET_USE = SSL_TICKET_RETURN_USE,
TICKET_USE_RENEW = SSL_TICKET_RETURN_USE_RENEW,
};

explicit AppData(SSL* session);
AppData(const AppData&) = delete;
AppData(AppData&&) = delete;
AppData& operator=(const AppData&) = delete;
AppData& operator=(AppData&&) = delete;

bool Set(const uv_buf_t& data);
std::optional<const uv_buf_t> Get() const;

// A source of application data collected during the creation of the
// session ticket. This interface will be implemented by the QUIC
// Session.
class Source {
public:
enum class Flag { STATUS_NONE, STATUS_RENEW };

// Collect application data into the given AppData instance.
virtual void CollectSessionTicketAppData(AppData* app_data) const = 0;

// Extract application data from the given AppData instance.
virtual Status ExtractSessionTicketAppData(
const AppData& app_data, Flag flag = Flag::STATUS_NONE) = 0;
};

static void Collect(SSL* ssl);
static Status Extract(SSL* ssl);

private:
bool set_ = false;
anonrig marked this conversation as resolved.
Show resolved Hide resolved
SSL* ssl_;
};

} // namespace quic
} // namespace node

#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS