Skip to content

Stateless Reset Token Uses Unkeyed FNV Derivation #132

@LiD0209

Description

@LiD0209

Stateless Reset Token Uses Unkeyed FNV Derivation

Summary

RFC 9000 requires a stateless reset token to be difficult to guess. In this quiche source tree, the server-side token used for transport parameters, NEW_CONNECTION_ID frames, and time-wait stateless reset packets is derived by applying unkeyed FNV1a_128_Hash to the connection ID.

The reviewed production paths do not show a per-connection random secret, a server static secret, an HMAC/HKDF-based keyed PRF, or another configurable token-generation hook for the default stateless reset token path.

Standard Requirement

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

RFC 9000 Section 10.3.2, "Calculating a Stateless Reset Token":

The stateless reset token MUST be difficult to guess.

A single static key can be used across all connections to the same endpoint by generating the proof using a pseudorandom function that takes a static key and the connection ID chosen by the endpoint as input.

The standard allows different designs, but the token still needs to be unpredictable to an observer. If the token is derived from a connection ID, the derivation needs secret material, such as an endpoint static key used with a pseudorandom function.

Relevant Source Code

Token Generation Helper

quiche-main/quiche-main/quiche/quic/core/quic_utils.cc:105

absl::uint128 QuicUtils::FNV1a_128_Hash(absl::string_view data) {
  return FNV1a_128_Hash_Three(data, absl::string_view(), absl::string_view());
}

quiche-main/quiche-main/quiche/quic/core/quic_utils.cc:513

StatelessResetToken QuicUtils::GenerateStatelessResetToken(
    const QuicConnectionId& connection_id) {
  static_assert(sizeof(absl::uint128) == sizeof(StatelessResetToken),
                "bad size");
  static_assert(alignof(absl::uint128) >= alignof(StatelessResetToken),
                "bad alignment");
  absl::uint128 hash = FNV1a_128_Hash(
      absl::string_view(connection_id.data(), connection_id.length()));
  return *reinterpret_cast<StatelessResetToken*>(&hash);
}

The only input is connection_id. There is no secret or key parameter, and FNV-1a is not a keyed pseudorandom function.

Transport Parameter Path

quiche-main/quiche-main/quiche/quic/core/quic_session.cc:244

if (perspective() == Perspective::IS_SERVER &&
    connection_->version().IsIetfQuic()) {
  config()->SetStatelessResetTokenToSend(GetStatelessResetToken());
}

quiche-main/quiche-main/quiche/quic/core/quic_session.cc:2748

StatelessResetToken QuicSession::GetStatelessResetToken() const {
  return QuicUtils::GenerateStatelessResetToken(connection_->connection_id());
}

quiche-main/quiche-main/quiche/quic/core/quic_config.cc:1215

if (stateless_reset_token_.HasSendValue()) {
  StatelessResetToken stateless_reset_token =
      stateless_reset_token_.GetSendValue();
  params->stateless_reset_token.assign(
      reinterpret_cast<const char*>(&stateless_reset_token),
      reinterpret_cast<const char*>(&stateless_reset_token) +
          sizeof(stateless_reset_token));
}

The token generated from connection_id is serialized into the IETF stateless_reset_token transport parameter.

NEW_CONNECTION_ID Path

quiche-main/quiche-main/quiche/quic/core/quic_connection_id_manager.cc:317

QuicNewConnectionIdFrame frame;
frame.connection_id = *new_cid;
frame.sequence_number = next_connection_id_sequence_number_++;
frame.stateless_reset_token =
    QuicUtils::GenerateStatelessResetToken(frame.connection_id);

The stateless reset token sent in NEW_CONNECTION_ID is also derived from the connection ID through the same helper.

Time-Wait Stateless Reset Packet Path

quiche-main/quiche-main/quiche/quic/core/quic_time_wait_list_manager.cc:323

return QuicFramer::BuildIetfStatelessResetPacket(
    connection_id, received_packet_length,
    GetStatelessResetToken(connection_id));

quiche-main/quiche-main/quiche/quic/core/quic_time_wait_list_manager.cc:472

StatelessResetToken QuicTimeWaitListManager::GetStatelessResetToken(
    QuicConnectionId connection_id) const {
  return QuicUtils::GenerateStatelessResetToken(connection_id);
}

The stateless reset packet builder receives a token produced by the same unkeyed derivation.

Review Notes

The function is not an unused helper. Static call-chain review shows it feeds the server transport parameter, NEW_CONNECTION_ID frames, and stateless reset packet construction.

The issue is also separate from duplicate-token history tracking. RFC 9000 says endpoints are not required to compare every new token against all previous values. The relevant point here is that the token value is derived from public input using a public, unkeyed algorithm.

Additional checks:

  • No production GetStatelessResetToken() const override was found that replaces QuicSession::GetStatelessResetToken().
  • SetStatelessResetTokenToSend() only stores the token in config; the production call site passes GetStatelessResetToken().
  • SetPreferredAddressConnectionIdAndTokenToSend() is limited to preferred address handling. Its token comes from a QuicNewConnectionIdFrame, which is populated with QuicUtils::GenerateStatelessResetToken(frame.connection_id).
  • A random connection ID does not make this derivation secret. The connection ID is visible on the wire, so an observer who knows the algorithm can compute FNV1a_128_Hash(connection_id).
  • Address-validation token code using source_address_token_secret, HKDF, and CryptoSecretBoxer protects NEW_TOKEN/Retry-style address validation tokens. It is not used by the stateless reset token paths above.

Security Impact

An observer that knows the connection ID and the public quiche derivation algorithm can compute the stateless reset token for that connection ID. This weakens the intended protection that only an endpoint with the relevant secret material can produce a valid stateless reset token.

Fix Direction

Use endpoint-private secret material when deriving stateless reset tokens. A typical design is HMAC-SHA256 or HKDF with a server static secret and the connection ID as input, truncated to 16 bytes. The transport parameter path, NEW_CONNECTION_ID path, and time-wait stateless reset packet path should share the same keyed derivation.

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