TLS 1.3: gate 0-RTT on a session cache hit - #11295
julek-wolfssl wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR strengthens TLS 1.3 0-RTT anti-replay behavior (RFC 8446 §8) by ensuring early data is accepted only when the ticket’s corresponding session is still present in the session cache, and by preventing reissued self-encrypted tickets from reusing (and thereby resurrecting) the same cache entry.
Changes:
- Gate 0-RTT acceptance on a recorded session-cache hit (
ticketCacheHit) and evict the cache entry upon accepting early data. - Generate a fresh per-ticket session ID for TLS 1.3 tickets (including self-encrypted tickets when
WOLFSSL_TICKET_HAVE_IDis enabled) to avoid restoring consumed entries. - Extend/adjust tests to cover external-cache replay behavior, remove-callback replay behavior, and QUIC early-data ticket chaining.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
wolfssl/internal.h |
Adds ticketCacheHit option bit to track whether the ticket session was found in cache. |
src/internal.c |
Sets ticketCacheHit based on whether ticket finalization had a cache-backed session (or session lookup succeeded). |
src/tls13.c |
Uses ticketCacheHit to gate 0-RTT and evicts the session entry on acceptance; ensures new per-ticket IDs are generated appropriately. |
src/ssl.c |
Clears ticketCacheHit in wolfSSL_clear() to avoid cross-handshake state leakage. |
tests/quic.c |
Updates QUIC early-data test to resume using the newly issued ticket after consuming one for 0-RTT. |
tests/api/test_tls13.h |
Registers new TLS 1.3 replay-related tests. |
tests/api/test_tls13.c |
Adds replay-round helper + new tests covering external-cache replay and remove-callback replay cases. |
tests/api/test_session.c |
Adjusts session removal expectations due to new per-ticket session IDs increasing cache entries. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Can one of the admins verify this patch? |
|
retest this please |
2 similar comments
|
retest this please |
|
retest this please |
ef3c453 to
e85d320
Compare
|
retest this please |
1 similar comment
|
retest this please |
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #11295
Scan targets checked: wolfcrypt-rs-bugs, wolfssl-bugs, wolfssl-src
Findings: 3
3 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Reported findings require changes before merge.
|
retest this please |
5f2c003 to
10ec03c
Compare
RFC 8446 Section 8 asks that one server instance accept 0-RTT for the same handshake at most once, so repeated replay cannot be used for timing measurements or to exhaust rate limits. That bound was not held for self-encrypted tickets. A ticket sent as an ID was unaffected: it cannot resume at all without a cache hit. CheckPreSharedKeys() gated 0-RTT on wolfSSL_SSL_CTX_remove_session() reporting that it removed the session. That return value cannot carry the answer: it reports success whenever it invokes ctx->rem_sess_cb, which returns void, so any context with a remove callback registered passed the gate every time. Separately, SendTls13NewSessionTicket() generated a fresh session ID only for a ticket sent as an ID, so a reissued self-encrypted ticket reused the ID and AddSession() restored the entry that accepting the earlier ticket's early data had removed. 0-RTT is now gated on the ticket's session still being in the session cache, which DoClientTicketFinalize() already looks up and which an external cache reports through wolfSSL_CTX_sess_set_get_cb(). The accepted connection evicts the entry, so the next use of the same ticket misses. Eviction is best effort, so the bound is too. Every ticket now carries its own session ID, so a reissued one does not restore a consumed entry. A server issuing many tickets over the same session therefore uses more cache entries than before.
Generating a new session ID for every NewSessionTicket detached server-side session state from the cache on resumption: wolfSSL keys WOLFSSL_SESSION ex_data on the session ID, so an application that marks a session at handshake time (stunnel's redirect authentication marker) lost the mark on every resume. stunnel test 183 caught this. Only a session that can carry early data needs its own cache entry per ticket to back the RFC 8446 Sect. 8 single-use bound, so gate the new ID on maxEarlyDataSz. Also drop an unused array in test_tls13_0rtt_remove_cb_replay that clang-tidy flagged as a dead store.
…ello Reset ticketCacheHit at the top of DoPreSharedKeys() next to ticketPredatesCtx. It was only cleared by wolfSSL_clear(), so the second ClientHello of an HRR exchange gated 0-RTT on the first hello's stale value. Only accept early data when wolfSSL_SSL_CTX_remove_session() reports an eviction. With HAVE_EXT_CACHE, WOLFSSL_SESS_CACHE_NO_INTERNAL and no rem_sess_cb it removes nothing, so one ticket's 0-RTT was accepted an unlimited number of times. Clear session after freeing it in test_quic_early_data. Expect() skips its body once an earlier expectation failed, leaving the freed pointer to be freed again during cleanup.
10ec03c to
569ae5f
Compare
|
retest this please |
RFC 8446 Section 8 requires that a server accept 0-RTT for the same handshake at most once, so replay cannot be used for timing measurements or to exhaust rate limits. This bound did not hold for self-encrypted tickets.
CheckPreSharedKeys()gated 0-RTT onwolfSSL_SSL_CTX_remove_session()reporting removal, but that return value cannot carry the answer: it reports success whenever it invokesctx->rem_sess_cb, which returns void, so any context with a remove callback registered passed the gate every time.SendTls13NewSessionTicket()generated a fresh session ID only for a ticket sent as an ID, so a reissued self-encrypted ticket reused the ID andAddSession()restored the entry that accepting the earlier ticket's early data had removed.0-RTT is now gated on the ticket's session still being present in the session cache, which
DoClientTicketFinalize()already looks up and which an external cache reports viawolfSSL_CTX_sess_set_get_cb(). The accepted connection evicts the entry, so a later use of the same ticket misses (eviction, and thus the bound, is best effort).Every ticket now carries its own session ID, so a reissued ticket no longer restores a consumed entry. As a result, a server issuing many tickets over the same session will use more cache entries than before.
A ticket sent as an ID was unaffected by the original bug, since it cannot resume at all without a cache hit.