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/cid.cc',
|
||||||
'src/quic/data.cc',
|
'src/quic/data.cc',
|
||||||
'src/quic/preferredaddress.cc',
|
'src/quic/preferredaddress.cc',
|
||||||
|
'src/quic/sessionticket.cc',
|
||||||
|
'src/quic/tokens.cc',
|
||||||
'src/quic/cid.h',
|
'src/quic/cid.h',
|
||||||
'src/quic/data.h',
|
'src/quic/data.h',
|
||||||
'src/quic/preferredaddress.h',
|
'src/quic/preferredaddress.h',
|
||||||
|
'src/quic/sessionticket.h',
|
||||||
|
'src/quic/tokens.h',
|
||||||
],
|
],
|
||||||
'node_mksnapshot_exec': '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)node_mksnapshot<(EXECUTABLE_SUFFIX)',
|
'node_mksnapshot_exec': '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)node_mksnapshot<(EXECUTABLE_SUFFIX)',
|
||||||
'conditions': [
|
'conditions': [
|
||||||
@ -1033,6 +1037,7 @@
|
|||||||
'test/cctest/test_crypto_clienthello.cc',
|
'test/cctest/test_crypto_clienthello.cc',
|
||||||
'test/cctest/test_node_crypto.cc',
|
'test/cctest/test_node_crypto.cc',
|
||||||
'test/cctest/test_quic_cid.cc',
|
'test/cctest/test_quic_cid.cc',
|
||||||
|
'test/cctest/test_quic_tokens.cc',
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
['v8_enable_inspector==1', {
|
['v8_enable_inspector==1', {
|
||||||
|
@ -15,6 +15,7 @@ using v8::BigInt;
|
|||||||
using v8::Integer;
|
using v8::Integer;
|
||||||
using v8::Local;
|
using v8::Local;
|
||||||
using v8::MaybeLocal;
|
using v8::MaybeLocal;
|
||||||
|
using v8::Uint8Array;
|
||||||
using v8::Undefined;
|
using v8::Undefined;
|
||||||
using v8::Value;
|
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 {
|
Store::operator bool() const {
|
||||||
return store_ != nullptr;
|
return store_ != nullptr;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||||
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||||
|
|
||||||
|
#include <env.h>
|
||||||
#include <memory_tracker.h>
|
#include <memory_tracker.h>
|
||||||
#include <nghttp3/nghttp3.h>
|
#include <nghttp3/nghttp3.h>
|
||||||
#include <ngtcp2/ngtcp2.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::ArrayBuffer> buffer, Option option = Option::NONE);
|
||||||
Store(v8::Local<v8::ArrayBufferView> view, 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 uv_buf_t() const;
|
||||||
operator ngtcp2_vec() const;
|
operator ngtcp2_vec() const;
|
||||||
operator nghttp3_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