Skip to content

Commit 1979425

Browse files
committed
TLS 1.3: gate 0-RTT on a cache-backed resumption ticket
RFC 8446 section 8 requires any server instance to accept 0-RTT for a given ClientHello at most once. Prior to this change wolfSSL's behaviour diverged from that requirement in several ways: * ctx->maxEarlyDataSz defaulted to MAX_EARLY_DATA_SZ whenever the library was built with WOLFSSL_EARLY_DATA, so servers auto- advertised 0-RTT in NewSessionTicket without the application asking. RFC 8446 E.5 says 0-RTT MUST NOT be enabled unless specifically requested. * The post-accept eviction is compiled out under NO_SESSION_CACHE, so builds without the cache accepted 0-RTT with no replay defence. * Stateless self-encrypted tickets do not carry a session ID on the stateless DoClientTicket decrypt path, so wolfSSL_SSL_CTX_remove_ session could not locate them to evict. * wolfSSL_SSL_CTX_remove_session always returned 0 on success regardless of whether the session was actually in the cache, diverging from OpenSSL's SSL_CTX_remove_session (1 on success, 0 on not-found). Changes: * src/internal.c: ctx->maxEarlyDataSz defaults to 0; applications must opt in with wolfSSL_CTX_set_max_early_data. * src/tls13.c: #error when WOLFSSL_EARLY_DATA is built with HAVE_SESSION_TICKET and NO_SESSION_CACHE. Escape hatch WOLFSSL_EARLY_DATA_NO_ANTI_REPLAY for deployments that take application-layer responsibility. * wolfssl/internal.h: imply WOLFSSL_TICKET_HAVE_ID from WOLFSSL_EARLY_DATA so stateless-ticket issuance populates the cache under an ID that eviction can find. * src/ssl_sess.c: wolfSSL_SSL_CTX_remove_session returns 1 when the session was found (internal-cache hit, or ctx->rem_sess_cb fired for an external cache), 0 otherwise. Matches OpenSSL semantics. * src/tls13.c: the 0-RTT acceptance condition in CheckPreSharedKeys now calls wolfSSL_SSL_CTX_remove_session and checks its return: the eviction is the check. If the session was in the cache, 0-RTT is accepted and the single-use requirement is satisfied. If not, the early_data extension is rejected through the normal path so the record layer correctly skips in-flight 0-RTT records. WOLFSSL_MSG at each rejection site. * doc/dox_comments/header_files/ssl.h: document runtime opt-in. * tests: four new tests — test_tls13_0rtt_default_off (fails without default-to-0 fix), test_tls13_0rtt_stateless_replay (fails without TICKET_HAVE_ID implication and remove_session gate), test_tls13_remove_session_return (fails without return-value fix), test_tls13_0rtt_ext_cache_eviction (fails without ext-cache counts-as-found fix). test_tls13_early_data explicitly opts in via wolfSSL_CTX_set_max_early_data. tests/api.c: two SSL_CTX_remove_session == 0 assertions updated to == 1.
1 parent b573823 commit 1979425

8 files changed

Lines changed: 327 additions & 21 deletions

File tree

doc/dox_comments/header_files/ssl.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14407,6 +14407,10 @@ wolfSSL_accept_TLSv13(WOLFSSL* ssl);
1440714407
A server value of zero indicates no early data is to be sent by client using
1440814408
session tickets. A client value of zero indicates that the client will
1440914409
not send any early data.
14410+
The default value is zero: per RFC 8446 Appendix E.5, TLS implementations
14411+
"MUST NOT enable 0-RTT (either sending or accepting) unless specifically
14412+
requested by the application." Servers must call this function (or the
14413+
per-SSL equivalent) with a non-zero value to opt in.
1441014414
It is recommended that the number of early data bytes be kept as low as
1441114415
practically possible in the application.
1441214416

src/internal.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2840,7 +2840,9 @@ int InitSSL_Ctx(WOLFSSL_CTX* ctx, WOLFSSL_METHOD* method, void* heap)
28402840
#endif
28412841

28422842
#ifdef WOLFSSL_EARLY_DATA
2843-
ctx->maxEarlyDataSz = MAX_EARLY_DATA_SZ;
2843+
/* RFC 8446 section E.5: 0-RTT off by default; opt in via
2844+
* wolfSSL_CTX_set_max_early_data(). */
2845+
ctx->maxEarlyDataSz = 0;
28442846
#endif
28452847

28462848
#if defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)

src/ssl_sess.c

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3243,8 +3243,11 @@ static void SESSION_ex_data_cache_update(WOLFSSL_SESSION* session, int idx,
32433243
#endif
32443244

32453245
#ifndef NO_SESSION_CACHE
3246+
/* OpenSSL-compatible return: 1 if the session was found in the internal
3247+
* cache and evicted, 0 otherwise (not present, or null arguments). */
32463248
int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
32473249
{
3250+
int found = 0;
32483251
#if defined(HAVE_EXT_CACHE) || defined(HAVE_EX_DATA)
32493252
int rem_called = FALSE;
32503253
#endif
@@ -3253,7 +3256,7 @@ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
32533256

32543257
s = ClientSessionToSession(s);
32553258
if (ctx == NULL || s == NULL)
3256-
return BAD_FUNC_ARG;
3259+
return 0;
32573260

32583261
#ifdef HAVE_EXT_CACHE
32593262
if (!ctx->internalCacheOff)
@@ -3270,6 +3273,7 @@ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
32703273

32713274
ret = TlsSessionCacheGetAndWrLock(id, &sess, &row, ctx->method->side);
32723275
if (ret == 0 && sess != NULL) {
3276+
found = 1;
32733277
#if defined(HAVE_EXT_CACHE) || defined(HAVE_EX_DATA)
32743278
if (sess->rem_sess_cb != NULL) {
32753279
rem_called = TRUE;
@@ -3308,13 +3312,15 @@ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
33083312
#if defined(HAVE_EXT_CACHE) || defined(HAVE_EX_DATA)
33093313
if (ctx->rem_sess_cb != NULL && !rem_called) {
33103314
ctx->rem_sess_cb(ctx, s);
3315+
/* Assume the external cache had the session. */
3316+
found = 1;
33113317
}
33123318
#endif
33133319

33143320
/* s cannot be resumed at this point */
33153321
s->timeout = 0;
33163322

3317-
return 0;
3323+
return found;
33183324
}
33193325

33203326
#if defined(OPENSSL_ALL) || defined(WOLFSSL_NGINX) || defined(WOLFSSL_HAPROXY) \

src/tls13.c

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
*
6262
* TLS 1.3 Session Tickets:
6363
* WOLFSSL_TICKET_HAVE_ID: Session tickets include ID default: off
64+
* Forced on when WOLFSSL_EARLY_DATA is set.
6465
* WOLFSSL_TICKET_NONCE_MALLOC: Dynamically allocate ticket nonce default: off
6566
*
6667
* TLS 1.3 Key Exchange:
@@ -81,6 +82,14 @@
8182

8283
#if !defined(NO_TLS) && defined(WOLFSSL_TLS13)
8384

85+
/* 0-RTT anti-replay eviction needs the session cache. */
86+
#if defined(WOLFSSL_EARLY_DATA) && defined(HAVE_SESSION_TICKET) && \
87+
defined(NO_SESSION_CACHE) && !defined(NO_WOLFSSL_SERVER) && \
88+
!defined(WOLFSSL_EARLY_DATA_NO_ANTI_REPLAY)
89+
#error "WOLFSSL_EARLY_DATA with tickets requires !NO_SESSION_CACHE, or " \
90+
"define WOLFSSL_EARLY_DATA_NO_ANTI_REPLAY to opt out."
91+
#endif
92+
8493
#ifndef WOLFCRYPT_ONLY
8594

8695
#ifdef HAVE_ERRNO_H
@@ -5897,8 +5906,11 @@ static int DoTls13EncryptedExtensions(WOLFSSL* ssl, const byte* input,
58975906
#ifdef WOLFSSL_EARLY_DATA
58985907
if (ssl->earlyData != no_early_data) {
58995908
TLSX* ext = TLSX_Find(ssl->extensions, TLSX_EARLY_DATA);
5900-
if (ext == NULL || !ext->val)
5909+
if (ext == NULL || !ext->val) {
5910+
WOLFSSL_MSG("Early data rejected by server (no early_data "
5911+
"EncryptedExtensions response)");
59015912
ssl->earlyData = no_early_data;
5913+
}
59025914
}
59035915

59045916
if (ssl->earlyData == no_early_data) {
@@ -6373,18 +6385,6 @@ static int DoPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 inputSz,
63736385
/* This PSK works, no need to try any more. */
63746386
current->chosen = 1;
63756387
ext->resp = 1;
6376-
#if defined(WOLFSSL_EARLY_DATA) && defined(HAVE_SESSION_TICKET) && \
6377-
!defined(NO_SESSION_CACHE)
6378-
/* RFC 8446 section 8: accept 0-RTT for a given handshake at most
6379-
* once. Evict the session from both the internal cache (under a
6380-
* write lock) and any external cache (via ctx->rem_sess_cb) so
6381-
* the same ClientHello cannot replay early data. Only when the
6382-
* client offered 0-RTT on a session that permits it. */
6383-
if (ssl->earlyData != no_early_data &&
6384-
ssl->session->maxEarlyDataSz != 0) {
6385-
(void)wolfSSL_SSL_CTX_remove_session(ssl->ctx, ssl->session);
6386-
}
6387-
#endif
63886388
break;
63896389
}
63906390

@@ -6547,6 +6547,13 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz,
65476547
if (ssl->earlyData != no_early_data && first
65486548
#ifdef WOLFSSL_CERT_WITH_EXTERN_PSK
65496549
&& !hasCertWithExternPsk
6550+
#endif
6551+
#if defined(HAVE_SESSION_TICKET) && !defined(NO_SESSION_CACHE)
6552+
/* RFC 8446 section 8: evict the session from the cache.
6553+
* Accept 0-RTT only when the eviction found the entry
6554+
* (single-use). */
6555+
&& wolfSSL_SSL_CTX_remove_session(ssl->ctx, ssl->session)
6556+
== 1
65506557
#endif
65516558
) {
65526559
extEarlyData->resp = 1;
@@ -6562,8 +6569,10 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz,
65626569
ssl->keys.encryptionOn = 1;
65636570
ssl->earlyData = process_early_data;
65646571
}
6565-
else
6572+
else {
6573+
WOLFSSL_MSG("Rejecting early data");
65666574
extEarlyData->resp = 0;
6575+
}
65676576
}
65686577
#endif
65696578

@@ -6609,6 +6618,8 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz,
66096618
* combination in the ClientHello, but clear the response flag
66106619
* here as a defense-in-depth measure. */
66116620
if (extEarlyData != NULL) {
6621+
WOLFSSL_MSG("Rejecting early data: "
6622+
"cert_with_extern_psk is not 0-RTT compatible");
66126623
extEarlyData->resp = 0;
66136624
ssl->earlyData = no_early_data;
66146625
}
@@ -15516,6 +15527,11 @@ int wolfSSL_send_SessionTicket(WOLFSSL* ssl)
1551615527
* A value of zero indicates no early data is to be sent by client using session
1551715528
* tickets.
1551815529
*
15530+
* The default value is zero: per RFC 8446 Appendix E.5, TLS implementations
15531+
* "MUST NOT enable 0-RTT (either sending or accepting) unless specifically
15532+
* requested by the application." Servers must explicitly opt in by calling
15533+
* this function (or the per-SSL equivalent) with a non-zero value.
15534+
*
1551915535
* ctx The SSL/TLS CTX object.
1552015536
* sz Maximum size of the early data.
1552115537
* returns BAD_FUNC_ARG when ctx is NULL, SIDE_ERROR when not a server and

tests/api.c

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18514,8 +18514,9 @@ static int test_wolfSSL_CTX_sess_set_remove_cb(void)
1851418514
/* Force a cache update */
1851518515
ExpectNotNull(SSL_SESSION_set_ex_data(clientSess, serverSessRemIdx - 1, 0));
1851618516
/* This should set the timeout to 0 and call the remove callback from within
18517-
* the session cache. */
18518-
ExpectIntEQ(SSL_CTX_remove_session(clientSessCtx, clientSess), 0);
18517+
* the session cache. Returns 1 per OpenSSL semantics (session was
18518+
* present in the cache and removed). */
18519+
ExpectIntEQ(SSL_CTX_remove_session(clientSessCtx, clientSess), 1);
1851918520
ExpectNull(SSL_SESSION_get_ex_data(clientSess, serverSessRemIdx));
1852018521
ExpectIntEQ(clientSessRemCountFree, 1);
1852118522
#endif
@@ -18527,8 +18528,9 @@ static int test_wolfSSL_CTX_sess_set_remove_cb(void)
1852718528
/* Force a cache update */
1852818529
ExpectNotNull(SSL_SESSION_set_ex_data(serverSess, serverSessRemIdx - 1, 0));
1852918530
/* This should set the timeout to 0 and call the remove callback from within
18530-
* the session cache. */
18531-
ExpectIntEQ(SSL_CTX_remove_session(serverSessCtx, serverSess), 0);
18531+
* the session cache. Returns 1 per OpenSSL semantics (session was
18532+
* present in the cache and removed). */
18533+
ExpectIntEQ(SSL_CTX_remove_session(serverSessCtx, serverSess), 1);
1853218534
ExpectNull(SSL_SESSION_get_ex_data(serverSess, serverSessRemIdx));
1853318535
ExpectIntEQ(serverSessRemCountFree, 1);
1853418536
/* Need to free the references that we kept */

0 commit comments

Comments
 (0)