Skip to content

Commit

Permalink
quic: add additional quic implementation utilities
Browse files Browse the repository at this point in the history
* add TokenSecret, StatelessResetToken, RetryToken, and RegularToken
* add SessionTicket implementation

PR-URL: #47289
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
  • Loading branch information
jasnell committed Mar 31, 2023
1 parent 863ac8f commit d65ae9f
Show file tree
Hide file tree
Showing 8 changed files with 960 additions and 0 deletions.
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
// 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;
SSL* ssl_;
};

} // namespace quic
} // namespace node

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

0 comments on commit d65ae9f

Please sign in to comment.