Skip to content

[Bug]: ticket_lifetime = 0 and Immediate Discard Semantics #10322

@LiD0209

Description

@LiD0209

Contact Details

lxd_dong@bupt.edu.cn

Version

V5.9.1

Description

ticket_lifetime = 0 and Immediate Discard Semantics

Problem Summary

This issue concerns the TLS 1.3 NewSessionTicket.ticket_lifetime field when its value is 0.

The relevant rule is:

  • Variable: ticket_lifetime
  • Action: indicates immediate discard
  • Condition: ticket_lifetime is zero

The core question is whether wolfSSL discards such a ticket immediately, as required by the TLS 1.3 specification, or whether it still stores the ticket and only rejects it later during session resumption.

RFC 8446 Original Text

RFC 8446 Section 4.6.1, "New Session Ticket Message":
https://www.rfc-editor.org/rfc/rfc8446.html#section-4.6.1

Relevant text:

ticket_lifetime: Indicates the lifetime in seconds as a 32-bit
unsigned integer in network byte order. The value of zero
indicates that the ticket should be discarded immediately.

Additional surrounding text from the same section:

Clients MUST NOT cache tickets for longer than 7 days, regardless
of the ticket_lifetime, and MAY delete tickets earlier based on
local policy. A server MAY treat a ticket as valid for a shorter
period of time than what is stated in the ticket_lifetime.

The important point here is that the specification does not say "reject later if used".
It says the ticket should be discarded immediately.

wolfSSL Code Behavior

1. Ticket is stored first

When a TLS 1.3 NewSessionTicket is received, wolfSSL parses the ticket and stores it into the session object:

  • src/tls13.c:12201 calls SetTicket(...)
  • src/tls13.c:12209 sets ssl->timeout = lifetime
  • src/tls13.c:12210 sets ssl->session->timeout = lifetime
  • src/tls13.c:12212 sets ssl->session->ticketSeen = now
  • src/tls13.c:12250 calls SetupSession(ssl)
  • src/tls13.c:12252 calls AddSession(ssl)

This means that even when lifetime == 0, the ticket is still copied into the session and inserted into the cache.

2. No explicit immediate-discard branch

The ticket copy path itself does not contain a special lifetime == 0 discard branch:

  • src/internal.c:35030

That path stores the ticket data and prepares the session for caching.
There is no logic there that says "if lifetime is zero, free the ticket and skip caching".

3. Rejection happens later during resumption checks

wolfSSL performs timeout validation when the ticket is later used for resumption:

  • src/tls13.c:6195 calls DoClientTicketCheck(...)
  • src/internal.c:39520 checks:
if (diff > timeout * 1000 ||
    diff > (sword64)TLS13_MAX_TICKET_AGE * 1000)
    return WOLFSSL_FATAL_ERROR;

So the practical behavior is:

  • receive ticket
  • store ticket
  • cache session
  • only later, when resumption is attempted, reject it because timeout == 0

This is not the same as immediate discard at receive time.

Why This Is Inconsistent with the RFC

The inconsistency is not about whether wolfSSL can eventually reject the ticket.
It can.

The inconsistency is about when the invalidation happens.

RFC 8446 expectation:

  • ticket_lifetime = 0
  • ticket should be discarded immediately
  • ticket should not remain as a cached resumable object

wolfSSL behavior:

  • ticket_lifetime = 0
  • ticket is still stored and cached
  • invalidation is deferred until a later resumption check

So the implementation matches the intent only partially:

  • it does make the ticket unusable later
  • but it does not immediately discard it when received

Practical Security Impact

By itself, this is not a high-severity cryptographic break.
The more realistic impact is cache pollution and resumption interference.

Because the ticket is cached first, a server that keeps issuing new tickets can consume shared session-cache capacity and push out other cached resumptions.

In other words, the problem is not only a wording mismatch with the RFC.
It can also become a resource-management problem.

Runtime Reproduction

To validate the practical impact, a small reproduction program was added:

The test logic is:

  1. Create a resumable session for logical server A
  2. Confirm that A can resume normally
  3. Repeatedly connect using logical server B and keep obtaining fresh tickets
  4. Test whether A can still resume

Observed results:

  • with flood=0, A resumed successfully in all rounds
  • with flood=20, some A resumptions were lost
  • with flood=50, loss became more frequent
  • with flood=100, A resumption was lost in all tested rounds
  • with flood=200, A resumption was lost in all tested rounds

This shows that cached resumptions for one logical server can be displaced by repeated new tickets associated with another logical server.

Root Cause

The root cause is the combination of two behaviors:

  1. ticket_lifetime = 0 does not trigger an explicit discard at receive time
  2. session tickets are inserted into shared cache structures before later timeout validation removes them from actual use

Because of this, zero-lifetime tickets and aggressively refreshed tickets can still occupy cache entries for some period of time.

Recommended Fixes

Implementation

The clearest fix is to discard the ticket immediately when parsing NewSessionTicket if ticket_lifetime == 0.

That means:

  • do not keep the ticket in the session object
  • do not call the normal cache insertion path for that ticket
  • treat it as non-resumable as soon as it is received

Defensive improvement

Even if the receive-time fix is not applied immediately, a second-best improvement would be:

  • refuse to insert timeout == 0 tickets into the cache
  • or evict them immediately after parsing

This would still align much better with the RFC than the current deferred invalidation model.

Testing

Regression tests should cover at least these cases:

  1. ticket_lifetime = 0 must not leave a resumable cached ticket
  2. repeated zero-lifetime or short-lifetime tickets should not pollute the cache
  3. one logical server's repeated ticket refresh should not trivially evict unrelated resumptions without bounds

Conclusion

The RFC requires that a ticket with ticket_lifetime = 0 should be discarded immediately.

wolfSSL currently stores such a ticket first and only rejects it later during resumption checks.
Therefore, the implementation is not fully consistent with the TLS 1.3 semantic requirement.

This inconsistency is small at the pure protocol-wording level, but it has a concrete operational consequence: cached resumption state can be polluted or displaced before the ticket is eventually rejected.

Reproduction steps

No response

Relevant log output

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions