Skip to content

Commit 52d62bb

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 936f8e5 commit 52d62bb

10 files changed

Lines changed: 341 additions & 27 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

examples/client/client.c

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -602,21 +602,23 @@ static void SetKeyShare(WOLFSSL* ssl, int onlyKeyShare, int useX25519,
602602
#endif /* WOLFSSL_TLS13 && HAVE_SUPPORTED_CURVES */
603603

604604
#ifdef WOLFSSL_EARLY_DATA
605-
static void EarlyData(WOLFSSL_CTX* ctx, WOLFSSL* ssl, const char* msg,
606-
int msgSz, char* buffer)
605+
static int EarlyData(WOLFSSL_CTX* ctx, WOLFSSL* ssl, const char* msg,
606+
int msgSz, char* buffer)
607607
{
608608
int err;
609609
int ret;
610610

611+
(void)ctx;
612+
(void)buffer;
611613
WOLFSSL_ASYNC_WHILE_PENDING(ret = wolfSSL_write_early_data(ssl, msg, msgSz, &msgSz),
612614
ret <= 0);
613615
if (ret != msgSz) {
616+
err = wolfSSL_get_error(ssl, ret);
614617
LOG_ERROR("SSL_write_early_data msg error %d, %s\n", err,
615-
wolfSSL_ERR_error_string((unsigned long)err, buffer));
616-
wolfSSL_free(ssl); ssl = NULL;
617-
wolfSSL_CTX_free(ctx); ctx = NULL;
618-
err_sys("SSL_write_early_data failed");
618+
wolfSSL_ERR_error_string((unsigned long)err, buffer));
619+
return -1;
619620
}
621+
return 0;
620622
}
621623
#endif
622624

examples/server/server.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2848,6 +2848,10 @@ THREAD_RETURN WOLFSSL_THREAD server_test(void* args)
28482848
err_sys_ex(catastrophic, "can't set minimum downgrade version");
28492849
}
28502850

2851+
#ifdef WOLFSSL_EARLY_DATA
2852+
if (earlyData)
2853+
wolfSSL_CTX_set_max_early_data(ctx, 4096);
2854+
#endif
28512855
#ifdef OPENSSL_COMPATIBLE_DEFAULTS
28522856
/* Restore wolfSSL verify defaults */
28532857
wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_DEFAULT, NULL);

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: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3243,8 +3243,12 @@ 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 and removed from the
3247+
* internal cache, or if the external remove callback (rem_sess_cb) was
3248+
* invoked. 0 if neither applied (not present, or null arguments). */
32463249
int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
32473250
{
3251+
int found = 0;
32483252
#if defined(HAVE_EXT_CACHE) || defined(HAVE_EX_DATA)
32493253
int rem_called = FALSE;
32503254
#endif
@@ -3253,7 +3257,7 @@ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
32533257

32543258
s = ClientSessionToSession(s);
32553259
if (ctx == NULL || s == NULL)
3256-
return BAD_FUNC_ARG;
3260+
return 0;
32573261

32583262
#ifdef HAVE_EXT_CACHE
32593263
if (!ctx->internalCacheOff)
@@ -3270,6 +3274,7 @@ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
32703274

32713275
ret = TlsSessionCacheGetAndWrLock(id, &sess, &row, ctx->method->side);
32723276
if (ret == 0 && sess != NULL) {
3277+
found = 1;
32733278
#if defined(HAVE_EXT_CACHE) || defined(HAVE_EX_DATA)
32743279
if (sess->rem_sess_cb != NULL) {
32753280
rem_called = TRUE;
@@ -3308,13 +3313,15 @@ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
33083313
#if defined(HAVE_EXT_CACHE) || defined(HAVE_EX_DATA)
33093314
if (ctx->rem_sess_cb != NULL && !rem_called) {
33103315
ctx->rem_sess_cb(ctx, s);
3316+
/* Assume the external cache had the session. */
3317+
found = 1;
33113318
}
33123319
#endif
33133320

33143321
/* s cannot be resumed at this point */
33153322
s->timeout = 0;
33163323

3317-
return 0;
3324+
return found;
33183325
}
33193326

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

src/tls13.c

Lines changed: 31 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

@@ -6545,8 +6545,16 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz,
65456545
* RFC 8773bis: early_data is not compatible with
65466546
* cert_with_extern_psk, so skip key derivation in that case. */
65476547
if (ssl->earlyData != no_early_data && first
6548+
&& ssl->options.maxEarlyDataSz > 0
65486549
#ifdef WOLFSSL_CERT_WITH_EXTERN_PSK
65496550
&& !hasCertWithExternPsk
6551+
#endif
6552+
#if defined(HAVE_SESSION_TICKET) && !defined(NO_SESSION_CACHE)
6553+
/* RFC 8446 section 8: evict the session from the cache.
6554+
* Accept 0-RTT only when the eviction found the entry
6555+
* (single-use). */
6556+
&& wolfSSL_SSL_CTX_remove_session(ssl->ctx, ssl->session)
6557+
== 1
65506558
#endif
65516559
) {
65526560
extEarlyData->resp = 1;
@@ -6562,8 +6570,10 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz,
65626570
ssl->keys.encryptionOn = 1;
65636571
ssl->earlyData = process_early_data;
65646572
}
6565-
else
6573+
else {
6574+
WOLFSSL_MSG("Rejecting early data");
65666575
extEarlyData->resp = 0;
6576+
}
65676577
}
65686578
#endif
65696579

@@ -6609,6 +6619,8 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz,
66096619
* combination in the ClientHello, but clear the response flag
66106620
* here as a defense-in-depth measure. */
66116621
if (extEarlyData != NULL) {
6622+
WOLFSSL_MSG("Rejecting early data: "
6623+
"cert_with_extern_psk is not 0-RTT compatible");
66126624
extEarlyData->resp = 0;
66136625
ssl->earlyData = no_early_data;
66146626
}
@@ -15522,6 +15534,11 @@ int wolfSSL_send_SessionTicket(WOLFSSL* ssl)
1552215534
* A value of zero indicates no early data is to be sent by client using session
1552315535
* tickets.
1552415536
*
15537+
* The default value is zero: per RFC 8446 Appendix E.5, TLS implementations
15538+
* "MUST NOT enable 0-RTT (either sending or accepting) unless specifically
15539+
* requested by the application." Servers must explicitly opt in by calling
15540+
* this function (or the per-SSL equivalent) with a non-zero value.
15541+
*
1552515542
* ctx The SSL/TLS CTX object.
1552615543
* sz Maximum size of the early data.
1552715544
* 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
@@ -18547,8 +18547,9 @@ static int test_wolfSSL_CTX_sess_set_remove_cb(void)
1854718547
/* Force a cache update */
1854818548
ExpectNotNull(SSL_SESSION_set_ex_data(clientSess, serverSessRemIdx - 1, 0));
1854918549
/* This should set the timeout to 0 and call the remove callback from within
18550-
* the session cache. */
18551-
ExpectIntEQ(SSL_CTX_remove_session(clientSessCtx, clientSess), 0);
18550+
* the session cache. Returns 1 per OpenSSL semantics (session was
18551+
* present in the cache and removed). */
18552+
ExpectIntEQ(SSL_CTX_remove_session(clientSessCtx, clientSess), 1);
1855218553
ExpectNull(SSL_SESSION_get_ex_data(clientSess, serverSessRemIdx));
1855318554
ExpectIntEQ(clientSessRemCountFree, 1);
1855418555
#endif
@@ -18560,8 +18561,9 @@ static int test_wolfSSL_CTX_sess_set_remove_cb(void)
1856018561
/* Force a cache update */
1856118562
ExpectNotNull(SSL_SESSION_set_ex_data(serverSess, serverSessRemIdx - 1, 0));
1856218563
/* This should set the timeout to 0 and call the remove callback from within
18563-
* the session cache. */
18564-
ExpectIntEQ(SSL_CTX_remove_session(serverSessCtx, serverSess), 0);
18564+
* the session cache. Returns 1 per OpenSSL semantics (session was
18565+
* present in the cache and removed). */
18566+
ExpectIntEQ(SSL_CTX_remove_session(serverSessCtx, serverSess), 1);
1856518567
ExpectNull(SSL_SESSION_get_ex_data(serverSess, serverSessRemIdx));
1856618568
ExpectIntEQ(serverSessRemCountFree, 1);
1856718569
/* Need to free the references that we kept */

0 commit comments

Comments
 (0)