From d65ae9f6784fe1c51d72d95e745e3c99bf818ea8 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Mon, 27 Mar 2023 23:19:27 -0700
Subject: [PATCH] quic: add additional quic implementation utilities
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* 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>
---
 node.gyp                        |   5 +
 src/quic/data.cc                |   9 ++
 src/quic/data.h                 |   3 +
 src/quic/sessionticket.cc       | 177 ++++++++++++++++++++++
 src/quic/sessionticket.h        | 112 ++++++++++++++
 src/quic/tokens.cc              | 255 ++++++++++++++++++++++++++++++++
 src/quic/tokens.h               | 245 ++++++++++++++++++++++++++++++
 test/cctest/test_quic_tokens.cc | 154 +++++++++++++++++++
 8 files changed, 960 insertions(+)
 create mode 100644 src/quic/sessionticket.cc
 create mode 100644 src/quic/sessionticket.h
 create mode 100644 src/quic/tokens.cc
 create mode 100644 src/quic/tokens.h
 create mode 100644 test/cctest/test_quic_tokens.cc

diff --git a/node.gyp b/node.gyp
index 45304452e74f33..1f053394540888 100644
--- a/node.gyp
+++ b/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', {
diff --git a/src/quic/data.cc b/src/quic/data.cc
index e3428141f1ec6b..c5cba75a5c9374 100644
--- a/src/quic/data.cc
+++ b/src/quic/data.cc
@@ -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;
 }
diff --git a/src/quic/data.h b/src/quic/data.h
index 14a613df69196b..f6a741922ad6c5 100644
--- a/src/quic/data.h
+++ b/src/quic/data.h
@@ -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;
diff --git a/src/quic/sessionticket.cc b/src/quic/sessionticket.cc
new file mode 100644
index 00000000000000..6353b0f2949877
--- /dev/null
+++ b/src/quic/sessionticket.cc
@@ -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
diff --git a/src/quic/sessionticket.h b/src/quic/sessionticket.h
new file mode 100644
index 00000000000000..4700af5743954e
--- /dev/null
+++ b/src/quic/sessionticket.h
@@ -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
diff --git a/src/quic/tokens.cc b/src/quic/tokens.cc
new file mode 100644
index 00000000000000..f47aa45c25414f
--- /dev/null
+++ b/src/quic/tokens.cc
@@ -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
diff --git a/src/quic/tokens.h b/src/quic/tokens.h
new file mode 100644
index 00000000000000..00c2bf81233e69
--- /dev/null
+++ b/src/quic/tokens.h
@@ -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
diff --git a/test/cctest/test_quic_tokens.cc b/test/cctest/test_quic_tokens.cc
new file mode 100644
index 00000000000000..c02bab54646f7d
--- /dev/null
+++ b/test/cctest/test_quic_tokens.cc
@@ -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