quic: add additional quic implementation utilities
* add TokenSecret, StatelessResetToken, RetryToken, and RegularToken * add SessionTicket implementation PR-URL: https://github.com/nodejs/node/pull/47289 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
This commit is contained in:
parent
863ac8fa37
commit
d65ae9f678
5
node.gyp
5
node.gyp
@ -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': [
|
||||
@ -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', {
|
||||
|
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
@ -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;
|
||||
|
177
src/quic/sessionticket.cc
Normal file
177
src/quic/sessionticket.cc
Normal file
@ -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
src/quic/sessionticket.h
Normal file
112
src/quic/sessionticket.h
Normal file
@ -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
|
255
src/quic/tokens.cc
Normal file
255
src/quic/tokens.cc
Normal file
@ -0,0 +1,255 @@
|
||||
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
|
||||
#include "tokens.h"
|
||||
#include <crypto/crypto_util.h>
|
||||
#include <ngtcp2/ngtcp2_crypto.h>
|
||||
#include <node_sockaddr-inl.h>
|
||||
#include <string_bytes.h>
|
||||
#include <algorithm>
|
||||
#include "util.h"
|
||||
|
||||
namespace node {
|
||||
namespace quic {
|
||||
|
||||
// ============================================================================
|
||||
// TokenSecret
|
||||
|
||||
TokenSecret::TokenSecret() : buf_() {
|
||||
Reset();
|
||||
}
|
||||
|
||||
TokenSecret::TokenSecret(const uint8_t* secret) : buf_() {
|
||||
*this = secret;
|
||||
}
|
||||
|
||||
TokenSecret& TokenSecret::operator=(const uint8_t* other) {
|
||||
CHECK_NOT_NULL(other);
|
||||
memcpy(buf_, other, QUIC_TOKENSECRET_LEN);
|
||||
return *this;
|
||||
}
|
||||
|
||||
TokenSecret::operator const uint8_t*() const {
|
||||
return buf_;
|
||||
}
|
||||
|
||||
void TokenSecret::Reset() {
|
||||
// As a performance optimization later, we could consider creating an entropy
|
||||
// cache here similar to what we use for random CIDs so that we do not have
|
||||
// to engage CSPRNG on every call. That, however, is suboptimal for secrets.
|
||||
// If someone manages to get visibility into that cache then they would know
|
||||
// the secrets for a larger number of tokens, which could be bad. For now,
|
||||
// generating on each call is safer, even if less performant.
|
||||
CHECK(crypto::CSPRNG(buf_, QUIC_TOKENSECRET_LEN).is_ok());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// StatelessResetToken
|
||||
|
||||
StatelessResetToken::StatelessResetToken() : ptr_(nullptr), buf_() {}
|
||||
|
||||
StatelessResetToken::StatelessResetToken(const uint8_t* token) : ptr_(token) {}
|
||||
|
||||
StatelessResetToken::StatelessResetToken(const TokenSecret& secret,
|
||||
const CID& cid)
|
||||
: ptr_(buf_) {
|
||||
CHECK_EQ(ngtcp2_crypto_generate_stateless_reset_token(
|
||||
buf_, secret, kStatelessTokenLen, cid),
|
||||
0);
|
||||
}
|
||||
|
||||
StatelessResetToken::StatelessResetToken(uint8_t* token,
|
||||
const TokenSecret& secret,
|
||||
const CID& cid)
|
||||
: ptr_(token) {
|
||||
CHECK_EQ(ngtcp2_crypto_generate_stateless_reset_token(
|
||||
token, secret, kStatelessTokenLen, cid),
|
||||
0);
|
||||
}
|
||||
|
||||
StatelessResetToken::StatelessResetToken(const StatelessResetToken& other)
|
||||
: ptr_(buf_) {
|
||||
if (other) {
|
||||
memcpy(buf_, other.ptr_, kStatelessTokenLen);
|
||||
} else {
|
||||
ptr_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
StatelessResetToken::operator const uint8_t*() const {
|
||||
return ptr_ != nullptr ? ptr_ : buf_;
|
||||
}
|
||||
|
||||
StatelessResetToken::operator const char*() const {
|
||||
return reinterpret_cast<const char*>(ptr_ != nullptr ? ptr_ : buf_);
|
||||
}
|
||||
|
||||
StatelessResetToken::operator bool() const {
|
||||
return ptr_ != nullptr;
|
||||
}
|
||||
|
||||
bool StatelessResetToken::operator==(const StatelessResetToken& other) const {
|
||||
if (ptr_ == other.ptr_) return true;
|
||||
if ((ptr_ == nullptr && other.ptr_ != nullptr) ||
|
||||
(ptr_ != nullptr && other.ptr_ == nullptr)) {
|
||||
return false;
|
||||
}
|
||||
return memcmp(ptr_, other.ptr_, kStatelessTokenLen) == 0;
|
||||
}
|
||||
|
||||
bool StatelessResetToken::operator!=(const StatelessResetToken& other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
std::string StatelessResetToken::ToString() const {
|
||||
if (ptr_ == nullptr) return std::string();
|
||||
char dest[kStatelessTokenLen * 2];
|
||||
size_t written =
|
||||
StringBytes::hex_encode(*this, kStatelessTokenLen, dest, arraysize(dest));
|
||||
DCHECK_EQ(written, arraysize(dest));
|
||||
return std::string(dest, written);
|
||||
}
|
||||
|
||||
size_t StatelessResetToken::Hash::operator()(
|
||||
const StatelessResetToken& token) const {
|
||||
size_t hash = 0;
|
||||
if (token.ptr_ == nullptr) return hash;
|
||||
for (size_t n = 0; n < kStatelessTokenLen; n++)
|
||||
hash ^= std::hash<uint8_t>{}(token.ptr_[n]) + 0x9e3779b9 + (hash << 6) +
|
||||
(hash >> 2);
|
||||
return hash;
|
||||
}
|
||||
|
||||
StatelessResetToken StatelessResetToken::kInvalid;
|
||||
|
||||
// ============================================================================
|
||||
// RetryToken and RegularToken
|
||||
namespace {
|
||||
ngtcp2_vec GenerateRetryToken(uint8_t* buffer,
|
||||
uint32_t version,
|
||||
const SocketAddress& address,
|
||||
const CID& retry_cid,
|
||||
const CID& odcid,
|
||||
const TokenSecret& token_secret) {
|
||||
ssize_t ret =
|
||||
ngtcp2_crypto_generate_retry_token(buffer,
|
||||
token_secret,
|
||||
TokenSecret::QUIC_TOKENSECRET_LEN,
|
||||
version,
|
||||
address.data(),
|
||||
address.length(),
|
||||
retry_cid,
|
||||
odcid,
|
||||
uv_hrtime());
|
||||
DCHECK_GE(ret, 0);
|
||||
DCHECK_LE(ret, RetryToken::kRetryTokenLen);
|
||||
DCHECK_EQ(buffer[0], RetryToken::kTokenMagic);
|
||||
// This shouldn't be possible but we handle it anyway just to be safe.
|
||||
if (ret == 0) return {nullptr, 0};
|
||||
return {buffer, static_cast<size_t>(ret)};
|
||||
}
|
||||
|
||||
ngtcp2_vec GenerateRegularToken(uint8_t* buffer,
|
||||
uint32_t version,
|
||||
const SocketAddress& address,
|
||||
const TokenSecret& token_secret) {
|
||||
ssize_t ret =
|
||||
ngtcp2_crypto_generate_regular_token(buffer,
|
||||
token_secret,
|
||||
TokenSecret::QUIC_TOKENSECRET_LEN,
|
||||
address.data(),
|
||||
address.length(),
|
||||
uv_hrtime());
|
||||
DCHECK_GE(ret, 0);
|
||||
DCHECK_LE(ret, RegularToken::kRegularTokenLen);
|
||||
DCHECK_EQ(buffer[0], RegularToken::kTokenMagic);
|
||||
// This shouldn't be possible but we handle it anyway just to be safe.
|
||||
if (ret == 0) return {nullptr, 0};
|
||||
return {buffer, static_cast<size_t>(ret)};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
RetryToken::RetryToken(uint32_t version,
|
||||
const SocketAddress& address,
|
||||
const CID& retry_cid,
|
||||
const CID& odcid,
|
||||
const TokenSecret& token_secret)
|
||||
: buf_(),
|
||||
ptr_(GenerateRetryToken(
|
||||
buf_, version, address, retry_cid, odcid, token_secret)) {}
|
||||
|
||||
RetryToken::RetryToken(const uint8_t* token, size_t size)
|
||||
: ptr_(ngtcp2_vec{const_cast<uint8_t*>(token), size}) {
|
||||
DCHECK_LE(size, RetryToken::kRetryTokenLen);
|
||||
DCHECK_IMPLIES(token == nullptr, size = 0);
|
||||
}
|
||||
|
||||
std::optional<CID> RetryToken::Validate(uint32_t version,
|
||||
const SocketAddress& addr,
|
||||
const CID& dcid,
|
||||
const TokenSecret& token_secret,
|
||||
uint64_t verification_expiration) {
|
||||
if (ptr_.base == nullptr || ptr_.len == 0) return std::nullopt;
|
||||
ngtcp2_cid ocid;
|
||||
int ret = ngtcp2_crypto_verify_retry_token(
|
||||
&ocid,
|
||||
ptr_.base,
|
||||
ptr_.len,
|
||||
token_secret,
|
||||
TokenSecret::QUIC_TOKENSECRET_LEN,
|
||||
version,
|
||||
addr.data(),
|
||||
addr.length(),
|
||||
dcid,
|
||||
std::min(verification_expiration, QUIC_MIN_RETRYTOKEN_EXPIRATION),
|
||||
uv_hrtime());
|
||||
if (ret != 0) return std::nullopt;
|
||||
return std::optional<CID>(ocid);
|
||||
}
|
||||
|
||||
RetryToken::operator const ngtcp2_vec&() const {
|
||||
return ptr_;
|
||||
}
|
||||
RetryToken::operator const ngtcp2_vec*() const {
|
||||
return &ptr_;
|
||||
}
|
||||
|
||||
RegularToken::RegularToken(uint32_t version,
|
||||
const SocketAddress& address,
|
||||
const TokenSecret& token_secret)
|
||||
: buf_(),
|
||||
ptr_(GenerateRegularToken(buf_, version, address, token_secret)) {}
|
||||
|
||||
RegularToken::RegularToken(const uint8_t* token, size_t size)
|
||||
: ptr_(ngtcp2_vec{const_cast<uint8_t*>(token), size}) {
|
||||
DCHECK_LE(size, RegularToken::kRegularTokenLen);
|
||||
DCHECK_IMPLIES(token == nullptr, size = 0);
|
||||
}
|
||||
|
||||
bool RegularToken::Validate(uint32_t version,
|
||||
const SocketAddress& addr,
|
||||
const TokenSecret& token_secret,
|
||||
uint64_t verification_expiration) {
|
||||
if (ptr_.base == nullptr || ptr_.len == 0) return false;
|
||||
return ngtcp2_crypto_verify_regular_token(
|
||||
ptr_.base,
|
||||
ptr_.len,
|
||||
token_secret,
|
||||
TokenSecret::QUIC_TOKENSECRET_LEN,
|
||||
addr.data(),
|
||||
addr.length(),
|
||||
std::min(verification_expiration,
|
||||
QUIC_MIN_REGULARTOKEN_EXPIRATION),
|
||||
uv_hrtime()) == 0;
|
||||
}
|
||||
|
||||
RegularToken::operator const ngtcp2_vec&() const {
|
||||
return ptr_;
|
||||
}
|
||||
RegularToken::operator const ngtcp2_vec*() const {
|
||||
return &ptr_;
|
||||
}
|
||||
|
||||
} // namespace quic
|
||||
} // namespace node
|
||||
|
||||
#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
245
src/quic/tokens.h
Normal file
245
src/quic/tokens.h
Normal file
@ -0,0 +1,245 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
|
||||
#include <memory_tracker.h>
|
||||
#include <ngtcp2/ngtcp2_crypto.h>
|
||||
#include <node_internals.h>
|
||||
#include <node_sockaddr.h>
|
||||
#include "cid.h"
|
||||
|
||||
namespace node {
|
||||
namespace quic {
|
||||
|
||||
// TokenSecrets are used to generate things like stateless reset tokens,
|
||||
// retry tokens, and token packets. They are always QUIC_TOKENSECRET_LEN
|
||||
// bytes in length.
|
||||
//
|
||||
// In the default case, token secrets will always be generated randomly.
|
||||
// User code will be given the option to provide a secret directly
|
||||
// however.
|
||||
class TokenSecret final : public MemoryRetainer {
|
||||
public:
|
||||
static constexpr int QUIC_TOKENSECRET_LEN = 16;
|
||||
|
||||
// Generate a random secret.
|
||||
TokenSecret();
|
||||
|
||||
// Copy the given secret. The uint8_t* is assumed
|
||||
// to be QUIC_TOKENSECRET_LEN in length. Note that
|
||||
// the length is not verified so care must be taken
|
||||
// when this constructor is used.
|
||||
explicit TokenSecret(const uint8_t* secret);
|
||||
|
||||
TokenSecret(const TokenSecret& other) = default;
|
||||
TokenSecret& operator=(const TokenSecret& other) = default;
|
||||
TokenSecret& operator=(const uint8_t* other);
|
||||
|
||||
TokenSecret& operator=(TokenSecret&& other) = delete;
|
||||
|
||||
operator const uint8_t*() const;
|
||||
|
||||
// Resets the secret to a random value.
|
||||
void Reset();
|
||||
|
||||
SET_NO_MEMORY_INFO()
|
||||
SET_MEMORY_INFO_NAME(TokenSecret)
|
||||
SET_SELF_SIZE(TokenSecret)
|
||||
|
||||
private:
|
||||
uint8_t buf_[QUIC_TOKENSECRET_LEN];
|
||||
};
|
||||
|
||||
// A stateless reset token is used when a QUIC endpoint receives a QUIC packet
|
||||
// with a short header but the associated connection ID cannot be matched to any
|
||||
// known Session. In such cases, the receiver may choose to send a subtle opaque
|
||||
// indication to the sending peer that state for the Session has apparently been
|
||||
// lost. For any on- or off- path attacker, a stateless reset packet resembles
|
||||
// any other QUIC packet with a short header. In order to be successfully
|
||||
// handled as a stateless reset, the peer must have already seen a reset token
|
||||
// issued to it associated with the given CID. The token itself is opaque to the
|
||||
// peer that receives is but must be possible to statelessly recreate by the
|
||||
// peer that originally created it. The actual implementation is Node.js
|
||||
// specific but we currently defer to a utility function provided by ngtcp2.
|
||||
//
|
||||
// QUIC leaves the generation of stateless session tokens up to the
|
||||
// implementation to figure out. The idea, however, is that it ought to be
|
||||
// possible to generate a stateless reset token reliably even when all state
|
||||
// for a connection has been lost. We use the cid as it is the only reliably
|
||||
// consistent bit of data we have when a session is destroyed.
|
||||
//
|
||||
// StatlessResetTokens are always kStatelessTokenLen bytes,
|
||||
// as are the secrets used to generate the token.
|
||||
class StatelessResetToken final : public MemoryRetainer {
|
||||
public:
|
||||
static constexpr int kStatelessTokenLen = NGTCP2_STATELESS_RESET_TOKENLEN;
|
||||
|
||||
// Generates a stateless reset token using HKDF with the cid and token secret
|
||||
// as input. The token secret is either provided by user code when an Endpoint
|
||||
// is created or is generated randomly.
|
||||
StatelessResetToken(const TokenSecret& secret, const CID& cid);
|
||||
|
||||
// Generates a stateless reset token using the given token storage.
|
||||
// The StatelessResetToken wraps the token and does not take ownership.
|
||||
// The token storage must be at least kStatelessTokenLen bytes in length.
|
||||
// The length is not verified so care must be taken when using this
|
||||
// constructor.
|
||||
StatelessResetToken(uint8_t* token,
|
||||
const TokenSecret& secret,
|
||||
const CID& cid);
|
||||
|
||||
// Wraps the given token. Does not take over ownership of the token storage.
|
||||
// The token must be at least kStatelessTokenLen bytes in length.
|
||||
// The length is not verified so care must be taken when using this
|
||||
// constructor.
|
||||
explicit StatelessResetToken(const uint8_t* token);
|
||||
|
||||
StatelessResetToken(const StatelessResetToken& other);
|
||||
StatelessResetToken(StatelessResetToken&&) = delete;
|
||||
|
||||
std::string ToString() const;
|
||||
|
||||
operator const uint8_t*() const;
|
||||
operator bool() const;
|
||||
|
||||
bool operator==(const StatelessResetToken& other) const;
|
||||
bool operator!=(const StatelessResetToken& other) const;
|
||||
|
||||
struct Hash {
|
||||
size_t operator()(const StatelessResetToken& token) const;
|
||||
};
|
||||
|
||||
SET_NO_MEMORY_INFO()
|
||||
SET_MEMORY_INFO_NAME(StatelessResetToken)
|
||||
SET_SELF_SIZE(StatelessResetToken)
|
||||
|
||||
template <typename T>
|
||||
using Map =
|
||||
std::unordered_map<StatelessResetToken, T, StatelessResetToken::Hash>;
|
||||
|
||||
static StatelessResetToken kInvalid;
|
||||
|
||||
private:
|
||||
StatelessResetToken();
|
||||
operator const char*() const;
|
||||
|
||||
const uint8_t* ptr_;
|
||||
uint8_t buf_[NGTCP2_STATELESS_RESET_TOKENLEN];
|
||||
};
|
||||
|
||||
// A RETRY packet communicates a retry token to the client. Retry tokens are
|
||||
// generated only by QUIC servers for the purpose of validating the network path
|
||||
// between a client and server. The content payload of the RETRY packet is
|
||||
// opaque to the clientand must not be guessable by on- or off-path attackers.
|
||||
//
|
||||
// A QUIC server sends a RETRY token as a way of initiating explicit path
|
||||
// validation in response to an initial QUIC packet. The client, upon receiving
|
||||
// a RETRY, must abandon the initial connection attempt and try again with the
|
||||
// received retry token included with the new initial packet sent to the server.
|
||||
// If the server is performing explicit validation, it will look for the
|
||||
// presence of the retry token and attempt to validate it if found. The internal
|
||||
// structure of the retry token must be meaningful to the server, and the server
|
||||
// must be able to validate that the token is correct without relying on any
|
||||
// state left over from the previous connection attempt. We use an
|
||||
// implementation that is provided by ngtcp2.
|
||||
//
|
||||
// The token secret must be kept private on the QUIC server that generated the
|
||||
// retry. When multiple QUIC servers are used in a cluster, it cannot be
|
||||
// guaranteed that the same QUIC server instance will receive the subsequent new
|
||||
// Initial packet. Therefore, all QUIC servers in the cluster should either
|
||||
// share or be aware of the same token secret or a mechanism needs to be
|
||||
// implemented to ensure that subsequent packets are routed to the same QUIC
|
||||
// server instance.
|
||||
class RetryToken final : public MemoryRetainer {
|
||||
public:
|
||||
// The token prefix that is used to differentiate between a retry token
|
||||
// and a regular token.
|
||||
static constexpr uint8_t kTokenMagic = NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY;
|
||||
static constexpr int kRetryTokenLen = NGTCP2_CRYPTO_MAX_RETRY_TOKENLEN;
|
||||
|
||||
static constexpr uint64_t QUIC_DEFAULT_RETRYTOKEN_EXPIRATION =
|
||||
10 * NGTCP2_SECONDS;
|
||||
static constexpr uint64_t QUIC_MIN_RETRYTOKEN_EXPIRATION = 1 * NGTCP2_SECONDS;
|
||||
|
||||
// Generates a new retry token.
|
||||
RetryToken(uint32_t version,
|
||||
const SocketAddress& address,
|
||||
const CID& retry_cid,
|
||||
const CID& odcid,
|
||||
const TokenSecret& token_secret);
|
||||
|
||||
// Wraps the given retry token
|
||||
RetryToken(const uint8_t* token, size_t length);
|
||||
|
||||
// Validates the retry token given the input. If the token is valid,
|
||||
// the embedded original CID will be extracted from the token an
|
||||
// returned. If the token is invalid, std::nullopt will be returned.
|
||||
std::optional<CID> Validate(
|
||||
uint32_t version,
|
||||
const SocketAddress& address,
|
||||
const CID& cid,
|
||||
const TokenSecret& token_secret,
|
||||
uint64_t verification_expiration = QUIC_DEFAULT_RETRYTOKEN_EXPIRATION);
|
||||
|
||||
operator const ngtcp2_vec&() const;
|
||||
operator const ngtcp2_vec*() const;
|
||||
|
||||
SET_NO_MEMORY_INFO()
|
||||
SET_MEMORY_INFO_NAME(RetryToken)
|
||||
SET_SELF_SIZE(RetryToken)
|
||||
|
||||
private:
|
||||
uint8_t buf_[kRetryTokenLen];
|
||||
const ngtcp2_vec ptr_;
|
||||
};
|
||||
|
||||
// A NEW_TOKEN packet communicates a regular token to a client that the server
|
||||
// would like the client to send in the header of an initial packet for a
|
||||
// future connection. It is similar to RETRY and used for the same purpose,
|
||||
// except a NEW_TOKEN is used in advance of the client establishing a new
|
||||
// connection and a RETRY is sent in response to the client trying to open
|
||||
// a new connection.
|
||||
class RegularToken final : public MemoryRetainer {
|
||||
public:
|
||||
// The token prefix that is used to differentiate between a retry token
|
||||
// and a regular token.
|
||||
static constexpr uint8_t kTokenMagic = NGTCP2_CRYPTO_TOKEN_MAGIC_REGULAR;
|
||||
static constexpr int kRegularTokenLen = NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN;
|
||||
static constexpr uint64_t QUIC_DEFAULT_REGULARTOKEN_EXPIRATION =
|
||||
10 * NGTCP2_SECONDS;
|
||||
static constexpr uint64_t QUIC_MIN_REGULARTOKEN_EXPIRATION =
|
||||
1 * NGTCP2_SECONDS;
|
||||
|
||||
// Generates a new retry token.
|
||||
RegularToken(uint32_t version,
|
||||
const SocketAddress& address,
|
||||
const TokenSecret& token_secret);
|
||||
|
||||
// Wraps the given retry token
|
||||
RegularToken(const uint8_t* token, size_t length);
|
||||
|
||||
// Validates the retry token given the input.
|
||||
bool Validate(
|
||||
uint32_t version,
|
||||
const SocketAddress& address,
|
||||
const TokenSecret& token_secret,
|
||||
uint64_t verification_expiration = QUIC_DEFAULT_REGULARTOKEN_EXPIRATION);
|
||||
|
||||
operator const ngtcp2_vec&() const;
|
||||
operator const ngtcp2_vec*() const;
|
||||
|
||||
SET_NO_MEMORY_INFO()
|
||||
SET_MEMORY_INFO_NAME(RetryToken)
|
||||
SET_SELF_SIZE(RetryToken)
|
||||
|
||||
private:
|
||||
uint8_t buf_[kRegularTokenLen];
|
||||
const ngtcp2_vec ptr_;
|
||||
};
|
||||
|
||||
} // namespace quic
|
||||
} // namespace node
|
||||
|
||||
#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
154
test/cctest/test_quic_tokens.cc
Normal file
154
test/cctest/test_quic_tokens.cc
Normal file
@ -0,0 +1,154 @@
|
||||
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
#include <gtest/gtest.h>
|
||||
#include <ngtcp2/ngtcp2.h>
|
||||
#include <node_sockaddr-inl.h>
|
||||
#include <quic/cid.h>
|
||||
#include <quic/tokens.h>
|
||||
#include <util-inl.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
using node::quic::CID;
|
||||
using node::quic::RegularToken;
|
||||
using node::quic::RetryToken;
|
||||
using node::quic::StatelessResetToken;
|
||||
using node::quic::TokenSecret;
|
||||
|
||||
TEST(StatelessResetToken, Basic) {
|
||||
ngtcp2_cid cid_;
|
||||
uint8_t secret[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6};
|
||||
uint8_t nothing[StatelessResetToken::kStatelessTokenLen]{};
|
||||
uint8_t cid_data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
|
||||
ngtcp2_cid_init(&cid_, cid_data, 10);
|
||||
|
||||
TokenSecret fixed_secret(secret);
|
||||
|
||||
CID cid(cid_);
|
||||
|
||||
CHECK(!StatelessResetToken::kInvalid);
|
||||
const uint8_t* zeroed = StatelessResetToken::kInvalid;
|
||||
CHECK_EQ(memcmp(zeroed, nothing, StatelessResetToken::kStatelessTokenLen), 0);
|
||||
CHECK_EQ(StatelessResetToken::kInvalid.ToString(), "");
|
||||
|
||||
StatelessResetToken token(fixed_secret, cid);
|
||||
CHECK(token);
|
||||
CHECK_EQ(token.ToString(), "e21ea22bb78cae0ab8c7daa422240857");
|
||||
|
||||
// Token generation should be deterministic
|
||||
StatelessResetToken token2(fixed_secret, cid);
|
||||
|
||||
CHECK_EQ(token, token2);
|
||||
|
||||
// Let's pretend out secret is also a token just for the sake
|
||||
// of the test. That's ok because they're the same length.
|
||||
StatelessResetToken token3(secret);
|
||||
|
||||
CHECK_NE(token, token3);
|
||||
|
||||
// Copy constructor works.
|
||||
StatelessResetToken token4 = token3;
|
||||
CHECK_EQ(token3, token4);
|
||||
|
||||
uint8_t wrapped[StatelessResetToken::kStatelessTokenLen];
|
||||
StatelessResetToken token5(wrapped, fixed_secret, cid);
|
||||
CHECK_EQ(token5, token);
|
||||
|
||||
// StatelessResetTokens will be used as keys in a map...
|
||||
StatelessResetToken::Map<std::string> map;
|
||||
map[token] = "abc";
|
||||
map[token3] = "xyz";
|
||||
CHECK_EQ(map[token], "abc");
|
||||
CHECK_EQ(map[token4], "xyz");
|
||||
|
||||
// And as values in a CID::Map...
|
||||
CID::Map<StatelessResetToken> tokens;
|
||||
tokens.emplace(cid, token);
|
||||
auto found = tokens.find(cid);
|
||||
CHECK_NE(found, tokens.end());
|
||||
CHECK_EQ(found->second, token);
|
||||
}
|
||||
|
||||
TEST(RetryToken, Basic) {
|
||||
auto& random = CID::Factory::random();
|
||||
TokenSecret secret;
|
||||
node::SocketAddress address;
|
||||
CHECK(node::SocketAddress::New(AF_INET, "123.123.123.123", 1234, &address));
|
||||
auto retry_cid = random.Generate();
|
||||
auto odcid = random.Generate();
|
||||
RetryToken token(NGTCP2_PROTO_VER_MAX, address, retry_cid, odcid, secret);
|
||||
auto result = token.Validate(NGTCP2_PROTO_VER_MAX,
|
||||
address,
|
||||
retry_cid,
|
||||
secret,
|
||||
// Set a large expiration just to be safe
|
||||
10000000000);
|
||||
CHECK_NE(result, std::nullopt);
|
||||
CHECK_EQ(result.value(), odcid);
|
||||
|
||||
// We can pass the data into a new instance...
|
||||
ngtcp2_vec token_data = token;
|
||||
RetryToken token2(token_data.base, token_data.len);
|
||||
auto result2 = token.Validate(NGTCP2_PROTO_VER_MAX,
|
||||
address,
|
||||
retry_cid,
|
||||
secret,
|
||||
// Set a large expiration just to be safe
|
||||
10000000000);
|
||||
CHECK_NE(result2, std::nullopt);
|
||||
CHECK_EQ(result2.value(), odcid);
|
||||
|
||||
auto noresult = token.Validate(NGTCP2_PROTO_VER_MAX,
|
||||
address,
|
||||
retry_cid,
|
||||
secret,
|
||||
// Use a very small expiration that is
|
||||
// guaranteed to fail
|
||||
0);
|
||||
CHECK_EQ(noresult, std::nullopt);
|
||||
|
||||
// Fails if we change the retry_cid...
|
||||
auto noresult2 = token.Validate(
|
||||
NGTCP2_PROTO_VER_MAX, address, random.Generate(), secret, 10000000000);
|
||||
CHECK_EQ(noresult2, std::nullopt);
|
||||
|
||||
// Also fails if we change the address....
|
||||
CHECK(node::SocketAddress::New(AF_INET, "123.123.123.124", 1234, &address));
|
||||
|
||||
auto noresult3 = token.Validate(
|
||||
NGTCP2_PROTO_VER_MAX, address, retry_cid, secret, 10000000000);
|
||||
CHECK_EQ(noresult3, std::nullopt);
|
||||
}
|
||||
|
||||
TEST(RegularToken, Basic) {
|
||||
TokenSecret secret;
|
||||
node::SocketAddress address;
|
||||
CHECK(node::SocketAddress::New(AF_INET, "123.123.123.123", 1234, &address));
|
||||
RegularToken token(NGTCP2_PROTO_VER_MAX, address, secret);
|
||||
CHECK(token.Validate(NGTCP2_PROTO_VER_MAX,
|
||||
address,
|
||||
secret,
|
||||
// Set a large expiration just to be safe
|
||||
10000000000));
|
||||
|
||||
// We can pass the data into a new instance...
|
||||
ngtcp2_vec token_data = token;
|
||||
RegularToken token2(token_data.base, token_data.len);
|
||||
CHECK(token.Validate(NGTCP2_PROTO_VER_MAX,
|
||||
address,
|
||||
secret,
|
||||
// Set a large expiration just to be safe
|
||||
10000000000));
|
||||
|
||||
CHECK(!token.Validate(NGTCP2_PROTO_VER_MAX,
|
||||
address,
|
||||
secret,
|
||||
// Use a very small expiration that is
|
||||
// guaranteed to fail
|
||||
0));
|
||||
|
||||
// Also fails if we change the address....
|
||||
CHECK(node::SocketAddress::New(AF_INET, "123.123.123.124", 1234, &address));
|
||||
|
||||
CHECK(!token.Validate(NGTCP2_PROTO_VER_MAX, address, secret, 10000000000));
|
||||
}
|
||||
#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
Loading…
x
Reference in New Issue
Block a user