Skip to content

Manual session reuse does not enforce single-use ticket #11082

Description

@LiD0209

Manual session reuse does not enforce single-use ticket

Summary

  • Root Cause Key: manual-session-reuse-does-not-enforce-single-use-ticket

This report replaces the earlier over-merged conclusion. After re-checking
RFC 8446, the wolfSSL source, and focused runtime behavior, the supported
issue is narrower:

  • If an application keeps the same WOLFSSL_SESSION* and passes it to
    wolfSSL_set_session() more than once, wolfSSL does not force one-time
    consumption of the TLS 1.3 ticket/session state.
  • This maps to RFC 8446 Appendix C.4 client tracking prevention.
  • The broader claims in the earlier report were not confirmed:
    fresh ticket on every connection is implemented, and
    unauthenticated operation without explicit configuration was not
    reproduced.

The correct classification is therefore confirmed_partial: an
API-side support only privacy hardening gap, not the broader standards
violation claimed in the original report.

Standard Requirement

Official standard link: https://www.rfc-editor.org/rfc/rfc8446.html#appendix-C.4
Section: Appendix C.4, Client Tracking Prevention

Clients SHOULD NOT reuse a ticket for multiple connections.  Reuse of
a ticket allows passive observers to correlate different connections.
Servers that issue tickets SHOULD offer at least as many tickets as
the number of connections that a client might use; for example, a web
browser using HTTP/1.1 [RFC7230] might open six connections to a
server.  Servers SHOULD issue new tickets with every connection.

For the current issue, the first sentence is the relevant one. The standard
advises clients not to reuse a ticket across multiple connections because
that enables linkability. This is a SHOULD NOT, so the strongest supported
reading is a privacy-policy expectation rather than a mandatory handshake
legality check that every TLS library must hard-enforce internally.

Relevant Source Code

src/ssl_sess.c:1519-1609

int wolfSSL_SetSession(WOLFSSL* ssl, WOLFSSL_SESSION* session)
{
    SessionRow* sessRow = NULL;
    int ret = WOLFSSL_SUCCESS;

    session = ClientSessionToSession(session);

    if (ssl == NULL || session == NULL || !session->isSetup) {
        WOLFSSL_MSG("ssl or session NULL or not set up");
        return WOLFSSL_FAILURE;
    }

    if (ret == WOLFSSL_SUCCESS) {
        if (ssl->session == session) {
            WOLFSSL_MSG("ssl->session and session same");
        }
        else if (session->type != WOLFSSL_SESSION_TYPE_CACHE) {
            if (wolfSSL_SESSION_up_ref(session) == WOLFSSL_SUCCESS) {
                wolfSSL_FreeSession(ssl->ctx, ssl->session);
                ssl->session = session;
            }
            else
                ret = WOLFSSL_FAILURE;
        }
        else {
            ret = wolfSSL_DupSession(session, ssl->session, 0);
            if (ret != WOLFSSL_SUCCESS)
                WOLFSSL_MSG("Session duplicate failed");
        }
    }

    if (ret != WOLFSSL_SUCCESS)
        return ret;

    ssl->options.resuming = 1;
    ssl->options.haveEMS = (ssl->session->haveEMS) ? 1 : 0;

wolfSSL_SetSession() accepts a caller-supplied WOLFSSL_SESSION, attaches
it to the current ssl, and marks the connection as resuming. There is no
"consume ticket once" or "reject second use" logic in this path.

tests/api.c:34552-34592

ExpectNotNull(sess = wolfSSL_get1_session(ssl_c));
ExpectIntEQ(wolfSSL_set_session(ssl_c2, sess), WOLFSSL_SUCCESS);
ExpectIntEQ(wolfSSL_set_session(ssl_c3, sess), WOLFSSL_SUCCESS);

/* Exchange initial flights for the second connection */
ExpectIntEQ(wolfSSL_connect(ssl_c2), WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR));
ExpectIntEQ(wolfSSL_get_error(ssl_c2, WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)),
    WOLFSSL_ERROR_WANT_READ);
ExpectIntEQ(wolfSSL_accept(ssl_s2), WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR));
ExpectIntEQ(wolfSSL_get_error(ssl_s2, WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)),
    WOLFSSL_ERROR_WANT_READ);

/* Complete third connection so that new tickets are exchanged */
ExpectIntEQ(test_memio_do_handshake(ssl_c3, ssl_s3, 10, NULL), 0);

/* Complete second connection */
ExpectIntEQ(test_memio_do_handshake(ssl_c2, ssl_s2, 10, NULL), 0);

ExpectIntEQ(wolfSSL_session_reused(ssl_c2), 1);
ExpectIntEQ(wolfSSL_session_reused(ssl_c3), 1);

This test explicitly attaches the same sess to both ssl_c2 and ssl_c3.
The second resumed connection starts first, the third connection then
completes and obtains fresh tickets, and the second connection later
completes as well. Both resumed connections are expected to succeed from the
same original session/ticket state.

src/tls13.c:13037-13219 and src/internal.c:41735-41833

static int SendTls13NewSessionTicket(WOLFSSL* ssl)
{
    ...
    if (ssl->session->ticketNonce.len == 0) {
        ssl->session->ticketNonce.len = DEF_TICKET_NONCE_SZ;
        ssl->session->ticketNonce.data[0] = 0;
    }
    else {
        ...
        ssl->session->ticketNonce.data[0]++;
    }
    ...
}
void DoClientTicketFinalize(WOLFSSL* ssl, InternalTicket* it,
        const WOLFSSL_SESSION* sess)
{
    ...
    XMEMCPY(ssl->session->ticketNonce.data, it->ticketNonce,
        it->ticketNonceLen);
    ssl->session->ticketNonce.len = it->ticketNonceLen;
    ...
}

These paths show that wolfSSL does implement fresh-ticket generation and
ticket-state refresh. The confirmed issue is not "no fresh ticket support";
it is specifically that manual wolfSSL_set_session() reuse does not stop an
application from reusing an older saved session.

Implementation Behavior

The re-checked implementation behavior is:

  1. wolfSSL_get1_session() and wolfSSL_set_session() form an explicit
    application-controlled resumption API.
  2. On that manual resumption path, wolfSSL does not mark a TLS 1.3
    session/ticket as consumed after first use.
  3. Existing wolfSSL tests treat repeated reuse of the same saved session as
    allowed behavior.
  4. The two broader subclaims from the earlier report do not hold:
    src/tls13.c:13037-13219 and src/internal.c:41735-41833 cover fresh
    ticket issuance and refresh, and examples/client/client.c:3701-3790
    shows that peer verification is enabled by default unless the caller
    explicitly disables it.

The actual supported issue is therefore narrower than the original merged
report: a client-side privacy policy gap exposed through the public session
resumption API.

Inconsistency Reason

RFC 8446 Appendix C.4 says that clients SHOULD NOT reuse a ticket for
multiple connections because reuse enables correlation by passive observers.
wolfSSL does not enforce that policy on the wolfSSL_set_session() API path.
If an application keeps and reuses the same WOLFSSL_SESSION*, resumption can
proceed multiple times.

However, this is only a partial mismatch:

  • The standard text is SHOULD NOT, not MUST NOT.
  • The requirement is about client privacy policy, not basic parser or
    transcript correctness.
  • wolfSSL exposes the resumption mechanism but leaves the final reuse decision
    to the application.

The correct conclusion is therefore confirmed_partial / API-side support only, not the broader standards-bug framing used in the original report.

Runtime Evidence

Focused unit test

The test ran only test_session_ticket_hs_update against the audited build. It completed two connections using the same retained WOLFSSL_SESSION* and checked whether wolfSSL rejected the second manual reuse.

Observed output:

starting unit tests...
 Begin API Tests
   1763: test_session_ticket_hs_update                      : passed (  0.04310)
 End API Tests
 Failed/Skipped/Passed/All: 0/0/1/1

unit_test: Success for all configured tests.

This test confirms the issue because it succeeds while reusing the same saved
session on two resumed connections:

  1. The test extracts sess from the first connection.
  2. It calls wolfSSL_set_session(ssl_c2, sess) and
    wolfSSL_set_session(ssl_c3, sess).
  3. ssl_c2 begins a resumed handshake.
  4. ssl_c3 completes and receives fresh tickets.
  5. ssl_c2 then also completes, and both connections are asserted to have
    reused a session.

That behavior shows that wolfSSL does not enforce one-time consumption of the
original session/ticket state on the manual resumption path.

Impact

The impact is primarily privacy and policy control, not direct authentication
bypass or handshake failure:

  • An application that assumes the library will automatically prevent repeated
    ticket reuse may unintentionally permit linkability across connections.
  • Callers that expect one-time ticket consumption by default will observe a
    behavior gap.
  • This does not mean wolfSSL fails to issue fresh tickets, and it does not
    mean unauthenticated operation is enabled without explicit configuration.

Overall, this is best described as a low-to-moderate privacy hardening gap,
with the actual risk depending on whether the application already enforces its
own session/ticket reuse policy.

Fix Direction

Reasonable fixes or mitigations include:

  1. Add an optional library-side single-use resumption policy.
    For example, track a consumed bit for TLS 1.3 session state and reject a
    second wolfSSL_set_session() use once resumption succeeds, or enable that
    behavior behind an explicit strict-mode option.
  2. Strengthen API documentation and examples.
    Document that wolfSSL_set_session() does not automatically prevent
    repeated reuse of the same WOLFSSL_SESSION, and that applications should
    destroy, rotate, or replace saved sessions according to their privacy
    policy.
  3. Add clearer tests around policy behavior.
    Keep the current compatibility test, but also add a strict-policy test that
    verifies repeated reuse is rejected when the new option is enabled.

This report now supersedes the earlier broader claim. The previously merged
subclaims about missing fresh tickets and unauthenticated operation without
explicit configuration were not confirmed during re-check.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions