Skip to main content

src/nquic_new_token.erl

-module(nquic_new_token).

-moduledoc """
Server-side NEW_TOKEN issuance and validation per RFC 9000 ยง8.1.3.

The server hands out an opaque token via the NEW_TOKEN frame after
the handshake completes. A returning client echoes the token on its
next Initial; if the token validates, the server skips the Retry
round-trip and proceeds with handshake immediately.

The token binds the client's IP+port to a freshness timestamp,
authenticated by HMAC-SHA256 with the same static key Retry uses,
**domain-separated** by the leading `"new_token"` tag in the HMAC
input so a Retry token cannot be replayed as a NEW_TOKEN token (and
vice versa).

Token format: `<<HMAC:32/binary, IssuedAt:64, AddrBin/binary>>`
HMAC input: `<<"new_token", Lifetime:32, IssuedAt:64, AddrBin/binary>>`
""".

-export([
    generate/3,
    generate/4,
    validate/3,
    validate/4
]).

-define(DOMAIN_TAG, <<"new_token">>).
-define(DEFAULT_LIFETIME_S, 86400).

-doc "Generate a NEW_TOKEN token bound to `PeerAddr`, valid for 24 hours.".
-spec generate(binary(), nquic_socket:sockaddr(), pos_integer()) -> binary().
generate(StaticKey, PeerAddr, Lifetime) ->
    generate(StaticKey, PeerAddr, Lifetime, erlang:system_time(second)).

-doc "Like `generate/3` but with an explicit issue time (for testing).".
-spec generate(binary(), nquic_socket:sockaddr(), pos_integer(), non_neg_integer()) -> binary().
generate(StaticKey, PeerAddr, Lifetime, IssuedAt) ->
    AddrBin = nquic_retry:encode_addr(PeerAddr),
    HmacInput = <<?DOMAIN_TAG/binary, Lifetime:32, IssuedAt:64, AddrBin/binary>>,
    HMAC = crypto:mac(hmac, sha256, StaticKey, HmacInput),
    <<HMAC/binary, IssuedAt:64, AddrBin/binary>>.

-doc "Validate a NEW_TOKEN token against the current peer address.".
-spec validate(binary(), binary(), nquic_socket:sockaddr()) ->
    ok | {error, invalid_new_token}.
validate(Token, StaticKey, PeerAddr) ->
    validate(Token, StaticKey, PeerAddr, ?DEFAULT_LIFETIME_S).

-spec validate(binary(), binary(), nquic_socket:sockaddr(), pos_integer()) ->
    ok | {error, invalid_new_token}.
validate(<<HMAC:32/binary, IssuedAt:64, AddrBin/binary>>, StaticKey, PeerAddr, Lifetime) ->
    HmacInput = <<?DOMAIN_TAG/binary, Lifetime:32, IssuedAt:64, AddrBin/binary>>,
    Expected = crypto:mac(hmac, sha256, StaticKey, HmacInput),
    Now = erlang:system_time(second),
    Expired = (Now - IssuedAt) > Lifetime,
    AddrMatch = (AddrBin =:= nquic_retry:encode_addr(PeerAddr)),
    maybe
        true ?= nquic_retry:hmac_equal(HMAC, Expected),
        true ?= not Expired,
        true ?= AddrMatch,
        ok
    else
        false -> {error, invalid_new_token}
    end;
validate(_, _, _, _) ->
    {error, invalid_new_token}.