Skip to content

Commit e5f569a

Browse files
committed
TLS 1.3: evict session from cache after accepted 0-RTT resumption
Per RFC 8446 section 8, a server MUST ensure that any instance of it would accept 0-RTT for the same 0-RTT handshake at most once. Without this, the same ClientHello could be replayed to re-accept early data on a subsequent connection. After the PSK is authenticated (binder verified) in DoPreSharedKeys, call wolfSSL_SSL_CTX_remove_session on ssl->session when the client offered 0-RTT and the session permits it. That evicts the entry from the internal cache (under the row's write lock) and invokes the application's ctx->rem_sess_cb so any external cache can drop its copy too. The session's timeout is also cleared so the live reference held by the current handshake cannot be resumed again. The mutation is paid only when the client actually included the early_data extension on a 0-RTT-capable session, so normal resumptions are unaffected and the existing remove-callback counts in test_wolfSSL_CTX_add_session_ext_{tls13,dtls13} stay correct. wolfSSL_SSL_CTX_remove_session was previously declared and defined only under the OpenSSL compatibility layer. Because it is now called from the core TLS 1.3 PSK path, the declaration in wolfssl/ssl.h and the definition in src/ssl_sess.c are moved out of that block to match the existing !NO_SESSION_CACHE gate under which the function is meaningful. wolfSSL_SSL_get0_session stays in the compat block. test_tls13_early_data_0rtt_replay verifies the behaviour. It does a full TLS 1.3 handshake with stateful tickets (SSL_OP_NO_TICKET) and max_early_data > 0, then tries to resume the saved session twice while offering 0-RTT each time. A minimal single-slot external session cache is wired up via wolfSSL_CTX_sess_set_{new,get,remove}_cb to confirm both caches are cleared. Round 0 must resume and deliver the early data, and rem_calls must hit 1 (the fix's single eviction). Round 1 must fall back to a full handshake (session_reused == 0), deliver no early data, and leave rem_calls at 1. Verified against multiple configurations (incl. --enable-all --enable-earlydata, the no-compat -DHAVE_EXT_CACHE build, and the os-check.yml combo). Valgrind under -g2 -O0 with OPENSSL_EXTRA + HAVE_EXT_CACHE + HAVE_EX_DATA reports no errors and no definitely-lost bytes. Refs #10197
1 parent 9176185 commit e5f569a

5 files changed

Lines changed: 214 additions & 8 deletions

File tree

src/ssl_sess.c

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3242,9 +3242,6 @@ static void SESSION_ex_data_cache_update(WOLFSSL_SESSION* session, int idx,
32423242

32433243
#endif
32443244

3245-
#if defined(OPENSSL_ALL) || defined(WOLFSSL_NGINX) || defined(WOLFSSL_HAPROXY) \
3246-
|| defined(OPENSSL_EXTRA) || defined(HAVE_LIGHTY)
3247-
32483245
#ifndef NO_SESSION_CACHE
32493246
int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
32503247
{
@@ -3320,18 +3317,19 @@ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
33203317
return 0;
33213318
}
33223319

3320+
#if defined(OPENSSL_ALL) || defined(WOLFSSL_NGINX) || defined(WOLFSSL_HAPROXY) \
3321+
|| defined(OPENSSL_EXTRA) || defined(HAVE_LIGHTY)
33233322
WOLFSSL_SESSION *wolfSSL_SSL_get0_session(const WOLFSSL *ssl)
33243323
{
33253324
WOLFSSL_ENTER("wolfSSL_SSL_get0_session");
33263325

33273326
return ssl->session;
33283327
}
3329-
3330-
#endif /* NO_SESSION_CACHE */
3331-
33323328
#endif /* OPENSSL_ALL || WOLFSSL_NGINX || WOLFSSL_HAPROXY ||
33333329
OPENSSL_EXTRA || HAVE_LIGHTY */
33343330

3331+
#endif /* NO_SESSION_CACHE */
3332+
33353333
#ifdef WOLFSSL_SESSION_EXPORT
33363334
/* Used to import a serialized TLS session.
33373335
* WARNING: buf contains sensitive information about the state and is best to be

src/tls13.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6287,6 +6287,18 @@ static int DoPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 inputSz,
62876287
/* This PSK works, no need to try any more. */
62886288
current->chosen = 1;
62896289
ext->resp = 1;
6290+
#if defined(WOLFSSL_EARLY_DATA) && defined(HAVE_SESSION_TICKET) && \
6291+
!defined(NO_SESSION_CACHE)
6292+
/* RFC 8446 section 8: accept 0-RTT for a given handshake at most
6293+
* once. Evict the session from both the internal cache (under a
6294+
* write lock) and any external cache (via ctx->rem_sess_cb) so
6295+
* the same ClientHello cannot replay early data. Only when the
6296+
* client offered 0-RTT on a session that permits it. */
6297+
if (ssl->earlyData != no_early_data &&
6298+
ssl->session->maxEarlyDataSz != 0) {
6299+
(void)wolfSSL_SSL_CTX_remove_session(ssl->ctx, ssl->session);
6300+
}
6301+
#endif
62906302
break;
62916303
}
62926304

tests/api/test_tls13.c

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2401,6 +2401,198 @@ int test_tls13_early_data(void)
24012401
}
24022402

24032403

2404+
#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \
2405+
defined(WOLFSSL_TLS13) && defined(WOLFSSL_EARLY_DATA) && \
2406+
defined(HAVE_SESSION_TICKET) && defined(WOLFSSL_TICKET_HAVE_ID) && \
2407+
!defined(NO_SESSION_CACHE) && defined(HAVE_EXT_CACHE)
2408+
/* Single-slot external session cache keyed by altSessionID, used by
2409+
* test_tls13_early_data_0rtt_replay to assert the 0-RTT anti-replay
2410+
* fix clears both caches. */
2411+
static struct {
2412+
byte id[ID_LEN];
2413+
byte has_entry;
2414+
WOLFSSL_SESSION* sess;
2415+
int new_calls;
2416+
int get_calls;
2417+
int rem_calls;
2418+
} test_tls13_0rtt_replay_cache;
2419+
2420+
static void test_tls13_0rtt_replay_cache_reset(void)
2421+
{
2422+
/* wolfSSL_SESSION_free is NULL-safe, so unconditionally drop any
2423+
* stored session without touching has_entry first. */
2424+
wolfSSL_SESSION_free(test_tls13_0rtt_replay_cache.sess);
2425+
XMEMSET(&test_tls13_0rtt_replay_cache, 0,
2426+
sizeof(test_tls13_0rtt_replay_cache));
2427+
}
2428+
2429+
/* Stateful-ticket sessions always have haveAltSessionID set, so key the
2430+
* cache on altSessionID directly (wolfSSL_SESSION_get_id is only
2431+
* declared under the OpenSSL compatibility layer). */
2432+
static int test_tls13_0rtt_replay_new_cb(WOLFSSL* ssl, WOLFSSL_SESSION* s)
2433+
{
2434+
(void)ssl;
2435+
test_tls13_0rtt_replay_cache.new_calls++;
2436+
if (s == NULL || !s->haveAltSessionID)
2437+
return 0;
2438+
wolfSSL_SESSION_free(test_tls13_0rtt_replay_cache.sess);
2439+
XMEMCPY(test_tls13_0rtt_replay_cache.id, s->altSessionID, ID_LEN);
2440+
test_tls13_0rtt_replay_cache.sess = s;
2441+
test_tls13_0rtt_replay_cache.has_entry = 1;
2442+
return 1; /* retain the reference; freed in the rem callback */
2443+
}
2444+
2445+
static WOLFSSL_SESSION* test_tls13_0rtt_replay_get_cb(WOLFSSL* ssl,
2446+
const byte* id, int idLen, int* ref)
2447+
{
2448+
(void)ssl;
2449+
test_tls13_0rtt_replay_cache.get_calls++;
2450+
*ref = 1; /* keep ownership; wolfSSL duplicates from us */
2451+
if (!test_tls13_0rtt_replay_cache.has_entry || idLen != ID_LEN)
2452+
return NULL;
2453+
if (XMEMCMP(test_tls13_0rtt_replay_cache.id, id, ID_LEN) != 0)
2454+
return NULL;
2455+
return test_tls13_0rtt_replay_cache.sess;
2456+
}
2457+
2458+
static void test_tls13_0rtt_replay_rem_cb(WOLFSSL_CTX* ctx,
2459+
WOLFSSL_SESSION* s)
2460+
{
2461+
const byte* id;
2462+
(void)ctx;
2463+
if (!test_tls13_0rtt_replay_cache.has_entry || s == NULL)
2464+
return;
2465+
/* Internal-cache-evicted sessions have haveAltSessionID cleared
2466+
* (that field sits before the DupSession copy offset), so fall
2467+
* back to sessionID when altSessionID is not set. Both carry the
2468+
* ID_LEN lookup key. */
2469+
if (s->haveAltSessionID)
2470+
id = s->altSessionID;
2471+
else if (s->sessionIDSz == ID_LEN)
2472+
id = s->sessionID;
2473+
else
2474+
return;
2475+
if (XMEMCMP(test_tls13_0rtt_replay_cache.id, id, ID_LEN) != 0)
2476+
return;
2477+
wolfSSL_SESSION_free(test_tls13_0rtt_replay_cache.sess);
2478+
test_tls13_0rtt_replay_cache.sess = NULL;
2479+
test_tls13_0rtt_replay_cache.has_entry = 0;
2480+
test_tls13_0rtt_replay_cache.rem_calls++;
2481+
}
2482+
2483+
/* RFC 8446 section 8 anti-replay: a 0-RTT-eligible session must be
2484+
* evicted from both the internal and external caches on resumption so
2485+
* the same ClientHello cannot replay early data. */
2486+
int test_tls13_early_data_0rtt_replay(void)
2487+
{
2488+
EXPECT_DECLS;
2489+
struct test_memio_ctx test_ctx;
2490+
WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL;
2491+
WOLFSSL *ssl_c = NULL, *ssl_s = NULL;
2492+
WOLFSSL_SESSION *sess = NULL;
2493+
char buf[64];
2494+
int round;
2495+
2496+
XMEMSET(&test_ctx, 0, sizeof(test_ctx));
2497+
test_tls13_0rtt_replay_cache_reset();
2498+
2499+
/* Step 1: full handshake populates both caches. */
2500+
ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s,
2501+
wolfTLSv1_3_client_method, wolfTLSv1_3_server_method),
2502+
0);
2503+
/* Stateful tickets + 0-RTT enabled. */
2504+
ExpectTrue(wolfSSL_set_options(ssl_s, WOLFSSL_OP_NO_TICKET) != 0);
2505+
#if defined(OPENSSL_EXTRA) || defined(WOLFSSL_ERROR_CODE_OPENSSL)
2506+
ExpectIntEQ(wolfSSL_set_max_early_data(ssl_s, 128), WOLFSSL_SUCCESS);
2507+
#else
2508+
ExpectIntEQ(wolfSSL_set_max_early_data(ssl_s, 128), 0);
2509+
#endif
2510+
wolfSSL_CTX_sess_set_new_cb(ctx_s, test_tls13_0rtt_replay_new_cb);
2511+
wolfSSL_CTX_sess_set_get_cb(ctx_s, test_tls13_0rtt_replay_get_cb);
2512+
wolfSSL_CTX_sess_set_remove_cb(ctx_s, test_tls13_0rtt_replay_rem_cb);
2513+
2514+
ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0);
2515+
/* Let the client consume NewSessionTicket. */
2516+
ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), -1);
2517+
ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ);
2518+
ExpectNotNull(sess = wolfSSL_get1_session(ssl_c));
2519+
ExpectIntEQ(wolfSSL_SessionIsSetup(sess), 1);
2520+
/* Stateful (ID-only) ticket on the client side. */
2521+
ExpectIntEQ(sess->ticketLen, ID_LEN);
2522+
ExpectIntEQ((int)sess->maxEarlyDataSz, 128);
2523+
/* External cache saw the add. */
2524+
ExpectIntGT(test_tls13_0rtt_replay_cache.new_calls, 0);
2525+
ExpectIntEQ(test_tls13_0rtt_replay_cache.has_entry, 1);
2526+
2527+
wolfSSL_free(ssl_c); ssl_c = NULL;
2528+
wolfSSL_free(ssl_s); ssl_s = NULL;
2529+
2530+
/* Resume the same session twice, offering 0-RTT each time. */
2531+
for (round = 0; round < 2 && !EXPECT_FAIL(); round++) {
2532+
const char earlyMsg[] = "early-data-0rtt";
2533+
int written = 0;
2534+
int earlyRead = 0;
2535+
char earlyBuf[sizeof(earlyMsg)];
2536+
2537+
XMEMSET(&test_ctx, 0, sizeof(test_ctx));
2538+
XMEMSET(earlyBuf, 0, sizeof(earlyBuf));
2539+
/* Reuse the CTXs so both caches survive (test_memio_setup
2540+
* leaves *ctx alone when non-NULL). */
2541+
ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c,
2542+
&ssl_s, wolfTLSv1_3_client_method,
2543+
wolfTLSv1_3_server_method), 0);
2544+
ExpectTrue(wolfSSL_set_options(ssl_s, WOLFSSL_OP_NO_TICKET) != 0);
2545+
#if defined(OPENSSL_EXTRA) || defined(WOLFSSL_ERROR_CODE_OPENSSL)
2546+
ExpectIntEQ(wolfSSL_set_max_early_data(ssl_s, 128),
2547+
WOLFSSL_SUCCESS);
2548+
#else
2549+
ExpectIntEQ(wolfSSL_set_max_early_data(ssl_s, 128), 0);
2550+
#endif
2551+
ExpectIntEQ(wolfSSL_SessionIsSetup(sess), 1);
2552+
ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS);
2553+
2554+
ExpectIntEQ(test_tls13_early_data_write_until_write_ok(ssl_c,
2555+
earlyMsg, (int)sizeof(earlyMsg), &written),
2556+
sizeof(earlyMsg));
2557+
ExpectIntEQ(written, sizeof(earlyMsg));
2558+
2559+
(void)test_tls13_early_data_read_until_write_ok(ssl_s, earlyBuf,
2560+
sizeof(earlyBuf), &earlyRead);
2561+
ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0);
2562+
2563+
if (round == 0) {
2564+
ExpectTrue(wolfSSL_session_reused(ssl_s));
2565+
ExpectIntEQ(earlyRead, sizeof(earlyMsg));
2566+
ExpectStrEQ(earlyMsg, earlyBuf);
2567+
/* Fix fired exactly once to evict the cached entry. */
2568+
ExpectIntEQ(test_tls13_0rtt_replay_cache.rem_calls, 1);
2569+
}
2570+
else {
2571+
ExpectFalse(wolfSSL_session_reused(ssl_s));
2572+
ExpectIntEQ(earlyRead, 0);
2573+
/* No additional eviction in the replay round. */
2574+
ExpectIntEQ(test_tls13_0rtt_replay_cache.rem_calls, 1);
2575+
}
2576+
2577+
wolfSSL_free(ssl_c); ssl_c = NULL;
2578+
wolfSSL_free(ssl_s); ssl_s = NULL;
2579+
}
2580+
2581+
wolfSSL_SESSION_free(sess);
2582+
wolfSSL_CTX_free(ctx_c);
2583+
wolfSSL_CTX_free(ctx_s);
2584+
test_tls13_0rtt_replay_cache_reset();
2585+
return EXPECT_RESULT();
2586+
}
2587+
#else
2588+
int test_tls13_early_data_0rtt_replay(void)
2589+
{
2590+
EXPECT_DECLS;
2591+
return EXPECT_RESULT();
2592+
}
2593+
#endif
2594+
2595+
24042596
/* Check that the client won't send the same CH after a HRR. An HRR without
24052597
* a KeyShare or a Cookie extension will trigger the error. */
24062598
int test_tls13_same_ch(void)

tests/api/test_tls13.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ int test_tls13_cert_req_sigalgs(void);
4545
int test_tls13_derive_keys_no_key(void);
4646
int test_tls13_pqc_hybrid_truncated_keyshare(void);
4747
int test_tls13_short_session_ticket(void);
48+
int test_tls13_early_data_0rtt_replay(void);
4849

4950
#define TEST_TLS13_DECLS \
5051
TEST_DECL_GROUP("tls13", test_tls13_apis), \
@@ -67,6 +68,7 @@ int test_tls13_short_session_ticket(void);
6768
TEST_DECL_GROUP("tls13", test_tls13_derive_keys_no_key), \
6869
TEST_DECL_GROUP("tls13", test_tls13_pqc_hybrid_truncated_keyshare), \
6970
TEST_DECL_GROUP("tls13", test_tls13_short_session_ticket), \
71+
TEST_DECL_GROUP("tls13", test_tls13_early_data_0rtt_replay), \
7072
TEST_DECL_GROUP("tls13", test_tls13_unknown_ext_rejected)
7173

7274
#endif /* WOLFCRYPT_TEST_TLS13_H */

wolfssl/ssl.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2940,6 +2940,10 @@ WOLFSSL_API WOLFSSL_SESSION* wolfSSL_SESSION_new_ex(void* heap);
29402940
WOLFSSL_API void wolfSSL_SESSION_free(WOLFSSL_SESSION* session);
29412941
WOLFSSL_API int wolfSSL_CTX_add_session(WOLFSSL_CTX* ctx,
29422942
WOLFSSL_SESSION* session);
2943+
#ifndef NO_SESSION_CACHE
2944+
WOLFSSL_API int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX* ctx,
2945+
WOLFSSL_SESSION *c);
2946+
#endif
29432947
WOLFSSL_API int wolfSSL_SESSION_set_cipher(WOLFSSL_SESSION* session,
29442948
const WOLFSSL_CIPHER* cipher);
29452949
WOLFSSL_API int wolfSSL_is_init_finished(const WOLFSSL* ssl);
@@ -5858,8 +5862,6 @@ WOLFSSL_API int wolfSSL_SSL_in_before(const WOLFSSL* ssl);
58585862
WOLFSSL_API int wolfSSL_SSL_in_connect_init(WOLFSSL* ssl);
58595863

58605864
#ifndef NO_SESSION_CACHE
5861-
WOLFSSL_API int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX* ctx,
5862-
WOLFSSL_SESSION *c);
58635865
WOLFSSL_API WOLFSSL_SESSION *wolfSSL_SSL_get0_session(const WOLFSSL *s);
58645866
#endif
58655867

0 commit comments

Comments
 (0)