Skip to content

Stateless reset token one-use and CID reuse static recheck #131

@LiD0209

Description

@LiD0209

Stateless reset token one-use and CID reuse static recheck

Summary

quiche does implement the receive-side behavior for an authenticated stateless reset: it recognizes a valid stateless reset token for the current connection and tears down the local connection state. However, the inspected code does not provide static evidence of a persistent, cross-connection or cross-node guard that prevents a connection ID, or the combination of connection ID and static key, from being reused for a new connection after the corresponding stateless reset token has been revealed.

Standard Requirement

RFC 9000 Section 10.3.1 requires an endpoint that detects a valid stateless reset token in the last 16 bytes of a datagram to enter the draining period and stop sending packets on that connection.

RFC 9000 Section 10.3.2 further states that revealing a stateless reset token lets any entity terminate the connection, so the value can only be used once. For the static-key derivation method, the combination of connection ID and static key must not be used for another connection. A connection ID from a connection reset by revealing the stateless reset token must not be reused for new connections at nodes that share a static key.

Official reference: https://www.rfc-editor.org/rfc/rfc9000.html#section-10.3.2

Relevant Source Code

Implemented: authenticated stateless reset closes the current connection

quic_framer.cc peeks at the last 16 bytes of a client-side IETF short-header packet as a possible stateless reset token. If packet protection removal or payload decryption fails, the framer checks that token through the visitor and reports an authenticated stateless reset when it matches.

// quiche/quic/core/quic_framer.cc:1752-1763
if (header->form == IETF_QUIC_SHORT_HEADER_PACKET &&
    perspective_ == Perspective::IS_CLIENT) {
  // Peek possible stateless reset token. Will only be used on decryption
  // failure.
  absl::string_view remaining = encrypted_reader->PeekRemainingPayload();
  if (remaining.length() >= sizeof(header->possible_stateless_reset_token)) {
    header->has_possible_stateless_reset_token = true;
    memcpy(&header->possible_stateless_reset_token,
           &remaining.data()[remaining.length() -
                             sizeof(header->possible_stateless_reset_token)],
           sizeof(header->possible_stateless_reset_token));
  }
}

// quiche/quic/core/quic_framer.cc:1963-1971
bool QuicFramer::IsIetfStatelessResetPacket(
    const QuicPacketHeader& header) const {
  return header.form == IETF_QUIC_SHORT_HEADER_PACKET &&
         header.has_possible_stateless_reset_token &&
         visitor_->IsValidStatelessResetToken(
             header.possible_stateless_reset_token);
}

QuicConnection::IsValidStatelessResetToken() compares the candidate token with the token stored for the current default path. Once the stateless reset is authenticated, OnAuthenticatedIetfStatelessResetPacket() tears down the local connection state.

// quiche/quic/core/quic_connection.cc:2392-2398
bool QuicConnection::IsValidStatelessResetToken(
    const StatelessResetToken& token) const {
  QUICHE_DCHECK_EQ(perspective_, Perspective::IS_CLIENT);
  return default_path_.stateless_reset_token.has_value() &&
         QuicUtils::AreStatelessResetTokensEqual(
             token, *default_path_.stateless_reset_token);
}

// quiche/quic/core/quic_connection.cc:2423-2426
const std::string error_details = "Received stateless reset.";
QUIC_CODE_COUNT(quic_tear_down_local_connection_on_stateless_reset);
TearDownLocalConnectionState(QUIC_PUBLIC_RESET, NO_IETF_QUIC_ERROR,
                             error_details, ConnectionCloseSource::FROM_PEER);

This satisfies the current-connection termination side of the requirement.

Missing proof: post-reveal CID and token reuse prevention for new connections

The stateless reset token sent by the server is derived from the connection ID. This is used for the handshake token and for server-issued connection IDs.

// quiche/quic/core/quic_session.cc:246
config()->SetStatelessResetTokenToSend(GetStatelessResetToken());

// quiche/quic/core/quic_session.cc:2748-2750
StatelessResetToken QuicSession::GetStatelessResetToken() const {
  return QuicUtils::GenerateStatelessResetToken(connection_->connection_id());
}

// quiche/quic/core/quic_connection_id_manager.cc:317-322
QuicNewConnectionIdFrame frame;
frame.connection_id = *new_cid;
frame.sequence_number = next_connection_id_sequence_number_++;
frame.stateless_reset_token =
    QuicUtils::GenerateStatelessResetToken(frame.connection_id);

GenerateStatelessResetToken() is a pure derivation from the connection ID. It does not record whether the token has been revealed, and it does not check whether the corresponding connection ID has been reset and must no longer be used for a new connection.

// quiche/quic/core/quic_utils.cc:513-521
StatelessResetToken QuicUtils::GenerateStatelessResetToken(
    const QuicConnectionId& connection_id) {
  static_assert(sizeof(absl::uint128) == sizeof(StatelessResetToken),
                "Mismatched stateless reset token sizes");
  static_assert(alignof(absl::uint128) >= alignof(StatelessResetToken),
                "Mismatched stateless reset token alignment");
  absl::uint128 hash = FNV1a_128_Hash(
      absl::string_view(connection_id.data(), connection_id.length()));
  return *reinterpret_cast<StatelessResetToken*>(&hash);
}

quiche does have a time-wait mechanism for closed server connection IDs. When a server connection closes, active server connection IDs are removed from the active session map and placed in the time-wait list. This prevents immediate ordinary reuse while the entry remains present.

// quiche/quic/core/quic_dispatcher.cc:966-967
for (const QuicConnectionId& cid :
     connection->GetActiveServerConnectionIds()) {

// quiche/quic/core/quic_time_wait_list_manager.cc:106-130
void QuicTimeWaitListManager::AddConnectionIdToTimeWait(
    TimeWaitAction action, TimeWaitConnectionInfo info) {
  ...
  for (const auto& cid : active_connection_ids) {
    auto it = connection_id_data_map_.find(cid);
    if (it != connection_id_data_map_.end()) {
      QUIC_CODE_COUNT(quic_time_wait_list_manager_duplicated_cid);
      connection_id_data_map_.erase(it);
    }
    connection_id_data_map_.insert({cid, data});

However, the time-wait list is bounded and expires entries after time_wait_period_. It is a short-term routing and response state for closed connections, not a persistent record that a stateless reset token has been revealed and that the corresponding CID or CID-plus-static-key combination must never be used for a new connection.

// quiche/quic/core/quic_time_wait_list_manager.cc:426-431
void QuicTimeWaitListManager::CleanUpOldConnectionIds() {
  QuicTime now = clock_->ApproximateNow();
  QuicTime expiration = now - time_wait_period_;

  while (MaybeExpireOldestConnection(expiration)) {
    QUIC_CODE_COUNT(quic_time_wait_list_expire_connections);
  }

The default deterministic connection ID generator also derives new connection IDs from the previous connection ID without consulting any global or persistent "revealed stateless reset token" or "reset CID" denylist.

// quiche/quic/core/deterministic_connection_id_generator.cc:25-52
std::optional<QuicConnectionId>
DeterministicConnectionIdGenerator::GenerateNextConnectionId(
    const QuicConnectionId& original) {
  const uint64_t connection_id_hash64 = QuicUtils::FNV1a_64_Hash(
      absl::string_view(original.data(), original.length()));
  ...
  const absl::uint128 connection_id_hash128 = QuicUtils::FNV1a_128_Hash(
      absl::string_view(original.data(), original.length()));
  ...
  return QuicConnectionId(new_connection_id_data,
                          expected_connection_id_length_);
}

Implementation Behavior

The implementation satisfies the receive-side reset behavior: a valid stateless reset token for the current default path closes the current connection.

The implementation also provides short-term protection for closed server connection IDs through the dispatcher and time-wait list.

What remains unproven is the RFC 9000 Section 10.3.2 one-use lifecycle requirement after token revelation: the inspected code does not show a durable state transition from "token revealed" to "corresponding CID or CID-plus-static-key combination permanently unavailable for new connections." It also does not show cross-node coordination for deployments where nodes share the same static key.

Decision

quiche is not missing the basic authenticated stateless reset close path. The real compliance gap is the absence of a static proof that, after a stateless reset token has been revealed, the corresponding connection ID or connection ID + static key combination is permanently unavailable for future connections.

This should remain classified as confirmed_partial rather than false_positive: the active-session and time-wait mechanisms prevent some immediate CID collisions or short-term reuse, but they do not satisfy the RFC 9000 Section 10.3.2 one-use lifecycle by themselves because time-wait entries are bounded and expire, and no persistent revealed-token/CID denylist or cross-node coordination path is visible in the inspected implementation.

Important nuance: if a deployment uses a CID generator that operationally guarantees never reusing a connection ID under the same stateless reset derivation key, that deployment could satisfy the lifecycle requirement without an explicit denylist. The audited quiche source, however, does not provide that guarantee as a general implementation invariant. The default DeterministicConnectionIdGenerator is deterministic from an input CID, and the load-balancer encoder uses finite nonce space/random unroutable IDs without recording revealed tokens; neither path records "this CID's stateless reset token was revealed, never issue it again."

Fix Direction

A complete proof would need one of the following:

  • A CID generation policy that cryptographically or operationally guarantees that connection IDs are never reused across connections under the same stateless reset static key.
  • A persistent denylist or equivalent state that records CIDs whose stateless reset token has been revealed and prevents those CIDs from being assigned to new connections.
  • Deployment-level documentation and enforcement showing that all nodes sharing a static key also share the no-reuse state or use a CID allocation scheme that makes reuse impossible.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions