Skip to main content

src/nquic_session_ticket.erl

-module(nquic_session_ticket).
-moduledoc """
Client-side NewSessionTicket processing (RFC 8446 §4.6.1, RFC 9000 §7.4.1).

Decodes a post-handshake CRYPTO `NewSessionTicket`, derives the PSK
from the resumption secret + ticket nonce, attaches the server's
transport parameters (so a later 0-RTT connect can seed
`remote_params`), persists the ticket via the configured session
cache, and notifies the connection owner.
""".

-include("nquic_conn.hrl").
-export([process_new_session_ticket/2]).

%%%-----------------------------------------------------------------------------
%% API
%%%-----------------------------------------------------------------------------
-doc """
Process a received NewSessionTicket from post-handshake CRYPTO.

`Bin` is the raw TLS handshake message (msg_type=4); anything else is
returned untouched. Updates the connection state with the cached
ticket map, persists to the configured session cache (if any), and
sends `{quic_session_ticket, _, _}` to the owner.
""".
-spec process_new_session_ticket(binary(), #conn_state{}) -> #conn_state{}.
process_new_session_ticket(<<4:8, _/binary>> = Bin, Data) ->
    case nquic_tls:decode_new_session_ticket(Bin) of
        {ok, Ticket0} ->
            Ticket1 = enrich_with_psk(Ticket0, Data),
            Ticket = enrich_with_remote_params(Ticket1, Data),
            Crypto0 = Data#conn_state.crypto,
            Data1 = Data#conn_state{
                crypto = Crypto0#conn_crypto{session_ticket = Ticket}
            },
            cache(Data1, Ticket),
            notify_owner(Data1, Ticket),
            Data1;
        {error, _} ->
            Data
    end;
process_new_session_ticket(_, Data) ->
    Data.

%%%-----------------------------------------------------------------------------
%% INTERNAL
%%%-----------------------------------------------------------------------------
-spec cache(#conn_state{}, map()) -> ok.
cache(
    #conn_state{
        crypto = #conn_crypto{hostname = Host, session_cache = Cache},
        peer = Peer
    },
    Ticket
) when Host =/= undefined, Peer =/= undefined ->
    Port = maps:get(port, Peer, 0),
    store(Cache, Host, Port, Ticket);
cache(_, _) ->
    ok.

-spec enrich_with_psk(map(), #conn_state{}) -> map().
enrich_with_psk(
    Ticket,
    #conn_state{crypto = #conn_crypto{resumption_secret = ResSecret, cipher = Cipher}}
) when
    is_binary(ResSecret)
->
    Nonce = maps:get(nonce, Ticket, <<>>),
    Hash = nquic_keys:cipher_to_hash(Cipher),
    HashLen =
        case Hash of
            sha256 -> 32;
            sha384 -> 48
        end,
    PSK = nquic_keys:qhkdf_expand(ResSecret, <<"resumption">>, Nonce, HashLen, Hash),
    Ticket#{psk => PSK, cipher => Cipher};
enrich_with_psk(Ticket, _Data) ->
    Ticket.

-spec enrich_with_remote_params(map(), #conn_state{}) -> map().
enrich_with_remote_params(Ticket, #conn_state{remote_params = RP}) when
    RP =/= undefined
->
    Ticket#{remote_params => RP};
enrich_with_remote_params(Ticket, _Data) ->
    Ticket.

-spec notify_owner(#conn_state{}, map()) -> ok.
notify_owner(#conn_state{owner = Owner}, Ticket) when is_pid(Owner) ->
    Owner ! {quic_session_ticket, self(), Ticket},
    ok;
notify_owner(_, _) ->
    ok.

-spec store(
    atom() | false | {module, module()} | undefined,
    inet:hostname() | inet:ip_address(),
    inet:port_number(),
    map()
) -> ok.
store(false, _Host, _Port, _Ticket) ->
    ok;
store(undefined, _Host, _Port, _Ticket) ->
    ok;
store({module, Mod}, Host, Port, Ticket) ->
    Mod:store(Host, Port, Ticket);
store(Name, Host, Port, Ticket) when is_atom(Name) ->
    nquic_session_cache:store(Name, Host, Port, Ticket).