Skip to content

Commit b0fdaa2

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 1c9555c commit b0fdaa2

11 files changed

Lines changed: 354 additions & 30 deletions

File tree

.wolfssl_known_macro_extras

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,7 @@ WOLFSSL_DTLS_RECORDS_CAN_SPAN_DATAGRAMS
750750
WOLFSSL_DTLS_RESEND_ONLY_TIMEOUT
751751
WOLFSSL_DUMP_MEMIO_STREAM
752752
WOLFSSL_DUP_CERTPOL
753+
WOLFSSL_EARLY_DATA_NO_ANTI_REPLAY
753754
WOLFSSL_ECC_BLIND_K
754755
WOLFSSL_ECC_GEN_REJECT_SAMPLING
755756
WOLFSSL_ECC_NO_SMALL_STACK

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 & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3255,8 +3255,12 @@ static void SESSION_ex_data_cache_update(WOLFSSL_SESSION* session, int idx,
32553255
#endif
32563256

32573257
#ifndef NO_SESSION_CACHE
3258+
/* OpenSSL-compatible return: 1 if the session was found and removed from the
3259+
* internal cache, or if the external remove callback (rem_sess_cb) was
3260+
* invoked. 0 if neither applied (not present, or null arguments). */
32583261
int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
32593262
{
3263+
int found = 0;
32603264
#if defined(HAVE_EXT_CACHE) || defined(HAVE_EX_DATA)
32613265
int rem_called = FALSE;
32623266
#endif
@@ -3265,7 +3269,7 @@ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
32653269

32663270
s = ClientSessionToSession(s);
32673271
if (ctx == NULL || s == NULL)
3268-
return BAD_FUNC_ARG;
3272+
return 0;
32693273

32703274
#ifdef HAVE_EXT_CACHE
32713275
if (!ctx->internalCacheOff)
@@ -3282,6 +3286,7 @@ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
32823286

32833287
ret = TlsSessionCacheGetAndWrLock(id, &sess, &row, ctx->method->side);
32843288
if (ret == 0 && sess != NULL) {
3289+
found = 1;
32853290
#if defined(HAVE_EXT_CACHE) || defined(HAVE_EX_DATA)
32863291
if (sess->rem_sess_cb != NULL) {
32873292
rem_called = TRUE;
@@ -3320,13 +3325,12 @@ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s)
33203325
#if defined(HAVE_EXT_CACHE) || defined(HAVE_EX_DATA)
33213326
if (ctx->rem_sess_cb != NULL && !rem_called) {
33223327
ctx->rem_sess_cb(ctx, s);
3328+
/* Assume the external cache had the session. */
3329+
found = 1;
33233330
}
33243331
#endif
33253332

3326-
/* s cannot be resumed at this point */
3327-
s->timeout = 0;
3328-
3329-
return 0;
3333+
return found;
33303334
}
33313335

33323336
#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
@@ -5901,8 +5910,11 @@ static int DoTls13EncryptedExtensions(WOLFSSL* ssl, const byte* input,
59015910
#ifdef WOLFSSL_EARLY_DATA
59025911
if (ssl->earlyData != no_early_data) {
59035912
TLSX* ext = TLSX_Find(ssl->extensions, TLSX_EARLY_DATA);
5904-
if (ext == NULL || !ext->val)
5913+
if (ext == NULL || !ext->val) {
5914+
WOLFSSL_MSG("Early data rejected by server (no early_data "
5915+
"EncryptedExtensions response)");
59055916
ssl->earlyData = no_early_data;
5917+
}
59065918
}
59075919

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

@@ -6549,8 +6549,16 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz,
65496549
* RFC 8773bis: early_data is not compatible with
65506550
* cert_with_extern_psk, so skip key derivation in that case. */
65516551
if (ssl->earlyData != no_early_data && first
6552+
&& ssl->options.maxEarlyDataSz > 0
65526553
#ifdef WOLFSSL_CERT_WITH_EXTERN_PSK
65536554
&& !hasCertWithExternPsk
6555+
#endif
6556+
#if defined(HAVE_SESSION_TICKET) && !defined(NO_SESSION_CACHE)
6557+
/* RFC 8446 section 8: evict the session from the cache.
6558+
* Accept 0-RTT only when the eviction found the entry
6559+
* (single-use). */
6560+
&& wolfSSL_SSL_CTX_remove_session(ssl->ctx, ssl->session)
6561+
== 1
65546562
#endif
65556563
) {
65566564
extEarlyData->resp = 1;
@@ -6613,6 +6621,8 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz,
66136621
* combination in the ClientHello, but clear the response flag
66146622
* here as a defense-in-depth measure. */
66156623
if (extEarlyData != NULL) {
6624+
WOLFSSL_MSG("Rejecting early data: "
6625+
"cert_with_extern_psk is not 0-RTT compatible");
66166626
extEarlyData->resp = 0;
66176627
ssl->earlyData = no_early_data;
66186628
}
@@ -15388,7 +15398,8 @@ int wolfSSL_accept_TLSv13(WOLFSSL* ssl)
1538815398
#ifdef HAVE_SESSION_TICKET
1538915399
#ifdef WOLFSSL_TLS13_TICKET_BEFORE_FINISHED
1539015400
if (!ssl->options.verifyPeer && !ssl->options.noTicketTls13 &&
15391-
ssl->ctx->ticketEncCb != NULL) {
15401+
ssl->ctx->ticketEncCb != NULL &&
15402+
ssl->options.maxTicketTls13 > 0) {
1539215403
if ((ssl->error = SendTls13NewSessionTicket(ssl)) != 0) {
1539315404
WOLFSSL_ERROR(ssl->error);
1539415405
return WOLFSSL_FATAL_ERROR;
@@ -15529,6 +15540,11 @@ int wolfSSL_send_SessionTicket(WOLFSSL* ssl)
1552915540
* A value of zero indicates no early data is to be sent by client using session
1553015541
* tickets.
1553115542
*
15543+
* The default value is zero: per RFC 8446 Appendix E.5, TLS implementations
15544+
* "MUST NOT enable 0-RTT (either sending or accepting) unless specifically
15545+
* requested by the application." Servers must explicitly opt in by calling
15546+
* this function (or the per-SSL equivalent) with a non-zero value.
15547+
*
1553215548
* ctx The SSL/TLS CTX object.
1553315549
* sz Maximum size of the early data.
1553415550
* 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
@@ -18689,8 +18689,9 @@ static int test_wolfSSL_CTX_sess_set_remove_cb(void)
1868918689
/* Force a cache update */
1869018690
ExpectNotNull(SSL_SESSION_set_ex_data(clientSess, serverSessRemIdx - 1, 0));
1869118691
/* This should set the timeout to 0 and call the remove callback from within
18692-
* the session cache. */
18693-
ExpectIntEQ(SSL_CTX_remove_session(clientSessCtx, clientSess), 0);
18692+
* the session cache. Returns 1 per OpenSSL semantics (session was
18693+
* present in the cache and removed). */
18694+
ExpectIntEQ(SSL_CTX_remove_session(clientSessCtx, clientSess), 1);
1869418695
ExpectNull(SSL_SESSION_get_ex_data(clientSess, serverSessRemIdx));
1869518696
ExpectIntEQ(clientSessRemCountFree, 1);
1869618697
#endif
@@ -18702,8 +18703,9 @@ static int test_wolfSSL_CTX_sess_set_remove_cb(void)
1870218703
/* Force a cache update */
1870318704
ExpectNotNull(SSL_SESSION_set_ex_data(serverSess, serverSessRemIdx - 1, 0));
1870418705
/* This should set the timeout to 0 and call the remove callback from within
18705-
* the session cache. */
18706-
ExpectIntEQ(SSL_CTX_remove_session(serverSessCtx, serverSess), 0);
18706+
* the session cache. Returns 1 per OpenSSL semantics (session was
18707+
* present in the cache and removed). */
18708+
ExpectIntEQ(SSL_CTX_remove_session(serverSessCtx, serverSess), 1);
1870718709
ExpectNull(SSL_SESSION_get_ex_data(serverSess, serverSessRemIdx));
1870818710
ExpectIntEQ(serverSessRemCountFree, 1);
1870918711
/* Need to free the references that we kept */

0 commit comments

Comments
 (0)