Skip to content

Do not report completion while a 0-RTT client is in early data - #21

Merged
ydnar merged 1 commit into
mainfrom
quic-0rtt-complete
Jul 2, 2026
Merged

Do not report completion while a 0-RTT client is in early data#21
ydnar merged 1 commit into
mainfrom
quic-0rtt-complete

Conversation

@ydnar

@ydnar ydnar commented Jul 2, 2026

Copy link
Copy Markdown

Motivation:

A client that offers 0-RTT early data (Resumption.offerEarlyData) reaches SSL_do_handshake == 1 as soon as it may send early data, while SSL_in_early_data() is still true and the handshake has not completed — the server confirms or rejects the 0-RTT in its response (RFC 9001 § 4.6.1). advance() treated that first 1 as State.complete, so a client reported the handshake done before it had consumed the server's confirmation. A driver that stops advancing on .complete then never learns whether its 0-RTT was accepted, and SSL_early_data_accepted stays false. Diagnosed downstream in swift-nio-quic, where a resumed 0-RTT handshake reused the session at 1-RTT but never registered the early-data acceptance the server had actually granted (the #20 test masked it because its pump keeps advancing past the premature .complete).

Modifications:

advance() checks SSL_in_early_data() when SSL_do_handshake returns 1: while the client is in the early-data state it reports .wantsMoreData — the handshake is still in flight — reaching .complete only once the client has left early data. Non-early-data handshakes are unaffected, since SSL_in_early_data() is always false for them.

Validated by testOfferedEarlyDataStaysInHandshakeUntilConfirmed (an offering client's first advance() is .wantsMoreData, not .complete); the existing testOfferedEarlyDataIsAccepted and the rest of the QUIC handshake suite still pass.

Result:

A 0-RTT-offering client stays in the handshake until the server resolves its early data, so earlyDataAccepted reflects the server's decision.

🤖 Generated with Claude Code


Generated by Claude Code

Motivation:

A client that offers 0-RTT early data (Resumption.offerEarlyData) reaches
SSL_do_handshake == 1 as soon as it may send early data, while
SSL_in_early_data() is still true and the handshake has not completed: the
server confirms or rejects the 0-RTT in its response (RFC 9001 section 4.6.1).
advance() treated that first 1 as State.complete, so a client reported the
handshake done before it had consumed the server's confirmation. A driver that
stops advancing on .complete then never learns whether its 0-RTT was accepted,
and SSL_early_data_accepted stays false. Diagnosed downstream in swift-nio-quic,
where a resumed 0-RTT handshake reused the session at 1-RTT but never registered
the early-data acceptance the server had actually granted.

Modifications:

advance() checks SSL_in_early_data() when SSL_do_handshake returns 1: while the
client is in the early-data state it reports .wantsMoreData — the handshake is
still in flight — reaching .complete only once the client has left early data.
Non-early-data handshakes are unaffected, since SSL_in_early_data() is always
false for them.

Validated by testOfferedEarlyDataStaysInHandshakeUntilConfirmed (an offering
client's first advance() is .wantsMoreData, not .complete); the existing
testOfferedEarlyDataIsAccepted and the rest of the QUIC handshake suite still
pass.

Result:

A 0-RTT-offering client stays in the handshake until the server resolves its
early data, so earlyDataAccepted reflects the server's decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR

@ydnar ydnar left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

LGTM

@ydnar
ydnar marked this pull request as ready for review July 2, 2026 21:47
@ydnar
ydnar merged commit dcab7ec into main Jul 2, 2026
43 of 45 checks passed
ydnar pushed a commit to alta/swift-nio-quic that referenced this pull request Jul 2, 2026
Bump the swift-nio-ssl fork pin to its `main` after alta/swift-nio-ssl#21, which
stops a 0-RTT-offering client from reporting handshake completion while it is
still in the early-data window (RFC 9001 § 4.6.1). Before the fix, a resumed
client that offered early data reported `.complete` from its first advance, so
the driver stopped short and never consumed the server's acceptance.

With the fix, a resumed client that offers early data and a server that accepts
it agree end to end, verified by the new
`TLSHandshakeTests.acceptsOfferedEarlyData`: both report `earlyDataAccepted` and
the client is not rejected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR
ydnar added a commit that referenced this pull request Jul 17, 2026
Motivation:

A client that offers 0-RTT early data (Resumption.offerEarlyData) reaches
SSL_do_handshake == 1 as soon as it may send early data, while
SSL_in_early_data() is still true and the handshake has not completed: the
server confirms or rejects the 0-RTT in its response (RFC 9001 section 4.6.1).
advance() treated that first 1 as State.complete, so a client reported the
handshake done before it had consumed the server's confirmation. A driver that
stops advancing on .complete then never learns whether its 0-RTT was accepted,
and SSL_early_data_accepted stays false. Diagnosed downstream in swift-nio-quic,
where a resumed 0-RTT handshake reused the session at 1-RTT but never registered
the early-data acceptance the server had actually granted.

Modifications:

advance() checks SSL_in_early_data() when SSL_do_handshake returns 1: while the
client is in the early-data state it reports .wantsMoreData — the handshake is
still in flight — reaching .complete only once the client has left early data.
Non-early-data handshakes are unaffected, since SSL_in_early_data() is always
false for them.

Validated by testOfferedEarlyDataStaysInHandshakeUntilConfirmed (an offering
client's first advance() is .wantsMoreData, not .complete); the existing
testOfferedEarlyDataIsAccepted and the rest of the QUIC handshake suite still
pass.

Result:

A 0-RTT-offering client stays in the handshake until the server resolves its
early data, so earlyDataAccepted reflects the server's decision.


Claude-Session: https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR

Co-authored-by: Claude <noreply@anthropic.com>
@ydnar
ydnar deleted the quic-0rtt-complete branch August 5, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants