Skip to content

Add 0-RTT (early data) support#148

Open
ycastorium wants to merge 1 commit into
benoitc:mainfrom
ycastorium:0_rtt
Open

Add 0-RTT (early data) support#148
ycastorium wants to merge 1 commit into
benoitc:mainfrom
ycastorium:0_rtt

Conversation

@ycastorium
Copy link
Copy Markdown

Lets a client with a session ticket send application data in its first flight instead of paying a full round trip on resumption. Wires the existing TLS 1.3 / QUIC PSK machinery through the public API and the HTTP/3 layer.

What's in here

Public API (quic.erl, quic_connection.erl)

  • New accessors quic:has_early_keys/1 and quic:early_data_accepted/1. The first reports whether early keys were derived from a stored ticket (0-RTT is possible); the second reports whether the server accepted early data, or unknown before the handshake settles.

TLS (quic_tls.erl)

  • Parse the server's acceptance signal — an empty early_data extension echoed in EncryptedExtensions; presence is the whole signal.

HTTP/3 (quic_h3.erl, quic_h3_connection.erl)

  • New bootstrapping and early_data states. After ownership transfer the client H3 process is primed and routes to either the 0-RTT early_data path or the regular awaiting_quic wait based on quic:has_early_keys/1.
  • quic_h3:early_data_accepted/1 exposes the outcome at the H3 layer.
  • Early-data requests reuse the server's remembered SETTINGS until the new SETTINGS arrive

Replay

  • 0-RTT data isn't forward-secret and is replayable, so resumption tickets are consumed on first use.

I added meck as a dep, but i can remove it if needed.

Closes #147

@benoitc benoitc self-assigned this May 29, 2026
Copy link
Copy Markdown
Owner

@benoitc benoitc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is red for now can you take care of it ? also the branch predates recent changes to support IPv6 can upi ensure to do your changes over the last main ?

i've commented the other parts in the source.

"Resumed in-proc: early_data_accepted=~p status=~p",
[EarlyData, Status]
),
?assert(lists:member(EarlyData, [true, false, unknown])),
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

early_data_accepted/1's entire range is true|false|unknown, so it can never fail . This doesn't prove the server accepted 0-RTT. Rather add a path asserting That EarlyData is tru and was processed before handshake completion

Comment thread src/quic_connection.erl
%% extension in EncryptedExtensions. Absence is rejection.
%% Clients that did not offer 0-RTT keep the default `false'.
OfferedZeroRtt = State#state.early_keys =/= undefined,
EarlyDataAccepted = OfferedZeroRtt andalso EarlyDataInEE,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good

Comment thread src/quic_connection.erl
Comment on lines +7429 to +7448
%% Connection-level flow-control accounting is left untouched: 0-RTT
%% bytes were sent under the resumed limits, and the new 1-RTT keys
%% bring fresh `initial_max_data' / `initial_max_stream_data_*'
%% allowances from the server's transport parameters.
-spec reset_zero_rtt_streams([non_neg_integer()], #state{}) -> #state{}.
reset_zero_rtt_streams([], State) ->
State;
reset_zero_rtt_streams(RejectedIds, #state{streams = Streams, owner = Owner} = State) ->
NewStreams = lists:foldl(fun maps:remove/2, Streams, RejectedIds),
?LOG_INFO(
#{
what => zero_rtt_rejected,
rejected_stream_ids => RejectedIds,
count => length(RejectedIds)
},
?QUIC_LOG_META
),
Owner ! {quic, self(), {early_data_rejected, RejectedIds}},
State#state{
streams = NewStreams,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the application re-issues the same bytes on new 1-RTT streams, the connection-level bytes-sent counter still includes the discarded 0-RTT bytes this can trigger premature MAX_DATA exhaustion. Shouldn't we reset send-side FC to the server's real transport params.? Also: do rejected stream IDs leave a gap the server (which never saw them) handles cleanly?

Comment thread src/quic_connection.erl
Comment on lines +183 to +188
test_state_for_zero_rtt_reset/3,
test_zero_rtt_reset_info/1,
test_finalize_zero_rtt_handshake/2,
test_reset_zero_rtt_streams/2,
test_client_state_for_ee/4,
test_process_encrypted_extensions/2
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Project convention is to keep test-only exports in a separate module, can you move them out in test folder ?

%% RFC 9114 §6.2.1 (SETTINGS-first), §7.2.4.2 (0-RTT initialization).
%%====================================================================

early_data(enter, _OldState, State) ->
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC 9114 §7.2.4.2: early requests must respect the server's remembered SETTINGS . is this loaded/ applied to qpack encoding? will it default to static or defuaults?

{keep_state_and_data, [postpone]};
bootstrapping(info, {quic, QC, {new_stream, _, _}}, #state{quic_conn = QC}) ->
{keep_state_and_data, [postpone]};
bootstrapping(_EventType, _Event, _State) ->
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bootstraping handle early_data_rejected but not session_tickett likeoter states does. Maybe we can make it consistent there too ?

Comment thread rebar.config
{deps, [
{proper, "1.4.0"}
{proper, "1.4.0"},
{meck, "1.0.0"}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump to latest to support erlang 29.

@ycastorium
Copy link
Copy Markdown
Author

CI is red for now can you take care of it ? also the branch predates recent changes to support IPv6 can upi ensure to do your changes over the last main ?

i've commented the other parts in the source.

Sure! I'll check!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing 0-RTT (early data) support

2 participants