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:
James M Snell 2023-03-27 23:19:27 -07:00
parent 863ac8fa37
commit d65ae9f678
8 changed files with 960 additions and 0 deletions

View File

@ -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', {

View File

@ -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;
}

View File

@ -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
View 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
View 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
View 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
View 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

View 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