Skip to main content

src/nquic_stateless_reset.erl

-module(nquic_stateless_reset).

-moduledoc """
Stateless reset token generation and detection per RFC 9000 Section 10.3.

Stateless reset allows an endpoint to terminate a connection when it has lost
state. The token is an HMAC-SHA256 of the connection ID, truncated to 16 bytes.
Reset packets look like short header packets to avoid identification.
""".

-export([build_packet/1, detect/2, generate_token/2]).

-doc "Build a stateless reset packet that looks like a short header packet.".
-spec build_packet(binary()) -> binary().
build_packet(Token) when byte_size(Token) =:= 16 ->
    PrefixLen = 5 + rand:uniform(21) - 1,
    Prefix = crypto:strong_rand_bytes(PrefixLen),
    <<_:1, _:1, Rest:6>> = <<(binary:first(Prefix))>>,
    <<0:1, 1:1, Rest:6, (binary:part(Prefix, 1, PrefixLen - 1))/binary, Token/binary>>.

-doc """
Check whether a packet is a stateless reset by comparing the last 16 bytes.
Comparison is constant-time (RFC 9000 Section 10.3.1: "An endpoint MUST
use a comparison that is constant-time with respect to the contents of
the token") to avoid timing side channels.
""".
-spec detect(binary(), binary()) -> boolean().
detect(Packet, Token) when byte_size(Packet) >= 21, byte_size(Token) =:= 16 ->
    PacketLen = byte_size(Packet),
    nquic_crypto:constant_time_equal(binary:part(Packet, PacketLen - 16, 16), Token);
detect(_, _) ->
    false.

-doc "Generate a stateless reset token from a static key and connection ID.".
-spec generate_token(binary(), binary()) -> binary().
generate_token(StaticKey, CID) ->
    <<Token:16/binary, _/binary>> = crypto:mac(hmac, sha256, StaticKey, CID),
    Token.