diff --git a/.wolfssl_known_macro_extras b/.wolfssl_known_macro_extras index c676c15524..057b6baf55 100644 --- a/.wolfssl_known_macro_extras +++ b/.wolfssl_known_macro_extras @@ -750,6 +750,7 @@ WOLFSSL_DTLS_RECORDS_CAN_SPAN_DATAGRAMS WOLFSSL_DTLS_RESEND_ONLY_TIMEOUT WOLFSSL_DUMP_MEMIO_STREAM WOLFSSL_DUP_CERTPOL +WOLFSSL_EARLY_DATA_NO_ANTI_REPLAY WOLFSSL_ECC_BLIND_K WOLFSSL_ECC_GEN_REJECT_SAMPLING WOLFSSL_ECC_NO_SMALL_STACK diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index e33ef60f79..707903a98f 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -14407,6 +14407,10 @@ wolfSSL_accept_TLSv13(WOLFSSL* ssl); A server value of zero indicates no early data is to be sent by client using session tickets. A client value of zero indicates that the client will not send any early data. + The default value is zero: per RFC 8446 Appendix E.5, TLS implementations + "MUST NOT enable 0-RTT (either sending or accepting) unless specifically + requested by the application." Servers must call this function (or the + per-SSL equivalent) with a non-zero value to opt in. It is recommended that the number of early data bytes be kept as low as practically possible in the application. diff --git a/examples/client/client.c b/examples/client/client.c index dfcfe5942b..43340b3ffd 100644 --- a/examples/client/client.c +++ b/examples/client/client.c @@ -602,21 +602,23 @@ static void SetKeyShare(WOLFSSL* ssl, int onlyKeyShare, int useX25519, #endif /* WOLFSSL_TLS13 && HAVE_SUPPORTED_CURVES */ #ifdef WOLFSSL_EARLY_DATA -static void EarlyData(WOLFSSL_CTX* ctx, WOLFSSL* ssl, const char* msg, - int msgSz, char* buffer) +static int EarlyData(WOLFSSL_CTX* ctx, WOLFSSL* ssl, const char* msg, + int msgSz, char* buffer) { int err; int ret; + (void)ctx; + (void)buffer; WOLFSSL_ASYNC_WHILE_PENDING(ret = wolfSSL_write_early_data(ssl, msg, msgSz, &msgSz), ret <= 0); if (ret != msgSz) { + err = wolfSSL_get_error(ssl, ret); LOG_ERROR("SSL_write_early_data msg error %d, %s\n", err, - wolfSSL_ERR_error_string((unsigned long)err, buffer)); - wolfSSL_free(ssl); ssl = NULL; - wolfSSL_CTX_free(ctx); ctx = NULL; - err_sys("SSL_write_early_data failed"); + wolfSSL_ERR_error_string((unsigned long)err, buffer)); + return -1; } + return 0; } #endif diff --git a/examples/server/server.c b/examples/server/server.c index 25ae785151..75b6048820 100644 --- a/examples/server/server.c +++ b/examples/server/server.c @@ -2848,6 +2848,10 @@ THREAD_RETURN WOLFSSL_THREAD server_test(void* args) err_sys_ex(catastrophic, "can't set minimum downgrade version"); } +#ifdef WOLFSSL_EARLY_DATA + if (earlyData) + wolfSSL_CTX_set_max_early_data(ctx, 4096); +#endif #ifdef OPENSSL_COMPATIBLE_DEFAULTS /* Restore wolfSSL verify defaults */ wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_DEFAULT, NULL); diff --git a/src/internal.c b/src/internal.c index c10b89d6a6..5e88035e94 100644 --- a/src/internal.c +++ b/src/internal.c @@ -2840,7 +2840,9 @@ int InitSSL_Ctx(WOLFSSL_CTX* ctx, WOLFSSL_METHOD* method, void* heap) #endif #ifdef WOLFSSL_EARLY_DATA - ctx->maxEarlyDataSz = MAX_EARLY_DATA_SZ; + /* RFC 8446 section E.5: 0-RTT off by default; opt in via + * wolfSSL_CTX_set_max_early_data(). */ + ctx->maxEarlyDataSz = 0; #endif #if defined(HAVE_SESSION_TICKET) || !defined(NO_PSK) diff --git a/src/ssl_sess.c b/src/ssl_sess.c index ec79057505..d1d4acf20d 100644 --- a/src/ssl_sess.c +++ b/src/ssl_sess.c @@ -3255,8 +3255,12 @@ static void SESSION_ex_data_cache_update(WOLFSSL_SESSION* session, int idx, #endif #ifndef NO_SESSION_CACHE +/* OpenSSL-compatible return: 1 if the session was found and removed from the + * internal cache, or if the external remove callback (rem_sess_cb) was + * invoked. 0 if neither applied (not present, or null arguments). */ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s) { + int found = 0; #if defined(HAVE_EXT_CACHE) || defined(HAVE_EX_DATA) int rem_called = FALSE; #endif @@ -3265,7 +3269,7 @@ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s) s = ClientSessionToSession(s); if (ctx == NULL || s == NULL) - return BAD_FUNC_ARG; + return 0; #ifdef HAVE_EXT_CACHE if (!ctx->internalCacheOff) @@ -3282,6 +3286,7 @@ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s) ret = TlsSessionCacheGetAndWrLock(id, &sess, &row, ctx->method->side); if (ret == 0 && sess != NULL) { + found = 1; #if defined(HAVE_EXT_CACHE) || defined(HAVE_EX_DATA) if (sess->rem_sess_cb != NULL) { rem_called = TRUE; @@ -3320,13 +3325,12 @@ int wolfSSL_SSL_CTX_remove_session(WOLFSSL_CTX *ctx, WOLFSSL_SESSION *s) #if defined(HAVE_EXT_CACHE) || defined(HAVE_EX_DATA) if (ctx->rem_sess_cb != NULL && !rem_called) { ctx->rem_sess_cb(ctx, s); + /* Assume the external cache had the session. */ + found = 1; } #endif - /* s cannot be resumed at this point */ - s->timeout = 0; - - return 0; + return found; } #if defined(OPENSSL_ALL) || defined(WOLFSSL_NGINX) || defined(WOLFSSL_HAPROXY) \ diff --git a/src/tls13.c b/src/tls13.c index fba9a05cad..20241e4712 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -61,6 +61,7 @@ * * TLS 1.3 Session Tickets: * WOLFSSL_TICKET_HAVE_ID: Session tickets include ID default: off + * Forced on when WOLFSSL_EARLY_DATA is set. * WOLFSSL_TICKET_NONCE_MALLOC: Dynamically allocate ticket nonce default: off * * TLS 1.3 Key Exchange: @@ -81,6 +82,14 @@ #if !defined(NO_TLS) && defined(WOLFSSL_TLS13) +/* 0-RTT anti-replay eviction needs the session cache. */ +#if defined(WOLFSSL_EARLY_DATA) && defined(HAVE_SESSION_TICKET) && \ + defined(NO_SESSION_CACHE) && !defined(NO_WOLFSSL_SERVER) && \ + !defined(WOLFSSL_EARLY_DATA_NO_ANTI_REPLAY) +#error "WOLFSSL_EARLY_DATA with tickets requires !NO_SESSION_CACHE, or " \ + "define WOLFSSL_EARLY_DATA_NO_ANTI_REPLAY to opt out." +#endif + #ifndef WOLFCRYPT_ONLY #ifdef HAVE_ERRNO_H @@ -5901,8 +5910,11 @@ static int DoTls13EncryptedExtensions(WOLFSSL* ssl, const byte* input, #ifdef WOLFSSL_EARLY_DATA if (ssl->earlyData != no_early_data) { TLSX* ext = TLSX_Find(ssl->extensions, TLSX_EARLY_DATA); - if (ext == NULL || !ext->val) + if (ext == NULL || !ext->val) { + WOLFSSL_MSG("Early data rejected by server (no early_data " + "EncryptedExtensions response)"); ssl->earlyData = no_early_data; + } } if (ssl->earlyData == no_early_data) { @@ -6377,18 +6389,6 @@ static int DoPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 inputSz, /* This PSK works, no need to try any more. */ current->chosen = 1; ext->resp = 1; -#if defined(WOLFSSL_EARLY_DATA) && defined(HAVE_SESSION_TICKET) && \ - !defined(NO_SESSION_CACHE) - /* RFC 8446 section 8: accept 0-RTT for a given handshake at most - * once. Evict the session from both the internal cache (under a - * write lock) and any external cache (via ctx->rem_sess_cb) so - * the same ClientHello cannot replay early data. Only when the - * client offered 0-RTT on a session that permits it. */ - if (ssl->earlyData != no_early_data && - ssl->session->maxEarlyDataSz != 0) { - (void)wolfSSL_SSL_CTX_remove_session(ssl->ctx, ssl->session); - } -#endif break; } @@ -6549,8 +6549,16 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz, * RFC 8773bis: early_data is not compatible with * cert_with_extern_psk, so skip key derivation in that case. */ if (ssl->earlyData != no_early_data && first + && ssl->options.maxEarlyDataSz > 0 #ifdef WOLFSSL_CERT_WITH_EXTERN_PSK && !hasCertWithExternPsk + #endif + #if defined(HAVE_SESSION_TICKET) && !defined(NO_SESSION_CACHE) + /* RFC 8446 section 8: evict the session from the cache. + * Accept 0-RTT only when the eviction found the entry + * (single-use). */ + && wolfSSL_SSL_CTX_remove_session(ssl->ctx, ssl->session) + == 1 #endif ) { extEarlyData->resp = 1; @@ -6613,6 +6621,8 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz, * combination in the ClientHello, but clear the response flag * here as a defense-in-depth measure. */ if (extEarlyData != NULL) { + WOLFSSL_MSG("Rejecting early data: " + "cert_with_extern_psk is not 0-RTT compatible"); extEarlyData->resp = 0; ssl->earlyData = no_early_data; } @@ -15388,7 +15398,8 @@ int wolfSSL_accept_TLSv13(WOLFSSL* ssl) #ifdef HAVE_SESSION_TICKET #ifdef WOLFSSL_TLS13_TICKET_BEFORE_FINISHED if (!ssl->options.verifyPeer && !ssl->options.noTicketTls13 && - ssl->ctx->ticketEncCb != NULL) { + ssl->ctx->ticketEncCb != NULL && + ssl->options.maxTicketTls13 > 0) { if ((ssl->error = SendTls13NewSessionTicket(ssl)) != 0) { WOLFSSL_ERROR(ssl->error); return WOLFSSL_FATAL_ERROR; @@ -15529,6 +15540,11 @@ int wolfSSL_send_SessionTicket(WOLFSSL* ssl) * A value of zero indicates no early data is to be sent by client using session * tickets. * + * The default value is zero: per RFC 8446 Appendix E.5, TLS implementations + * "MUST NOT enable 0-RTT (either sending or accepting) unless specifically + * requested by the application." Servers must explicitly opt in by calling + * this function (or the per-SSL equivalent) with a non-zero value. + * * ctx The SSL/TLS CTX object. * sz Maximum size of the early data. * returns BAD_FUNC_ARG when ctx is NULL, SIDE_ERROR when not a server and diff --git a/tests/api.c b/tests/api.c index 05a7688d7f..f5401ab980 100644 --- a/tests/api.c +++ b/tests/api.c @@ -18689,8 +18689,9 @@ static int test_wolfSSL_CTX_sess_set_remove_cb(void) /* Force a cache update */ ExpectNotNull(SSL_SESSION_set_ex_data(clientSess, serverSessRemIdx - 1, 0)); /* This should set the timeout to 0 and call the remove callback from within - * the session cache. */ - ExpectIntEQ(SSL_CTX_remove_session(clientSessCtx, clientSess), 0); + * the session cache. Returns 1 per OpenSSL semantics (session was + * present in the cache and removed). */ + ExpectIntEQ(SSL_CTX_remove_session(clientSessCtx, clientSess), 1); ExpectNull(SSL_SESSION_get_ex_data(clientSess, serverSessRemIdx)); ExpectIntEQ(clientSessRemCountFree, 1); #endif @@ -18702,8 +18703,9 @@ static int test_wolfSSL_CTX_sess_set_remove_cb(void) /* Force a cache update */ ExpectNotNull(SSL_SESSION_set_ex_data(serverSess, serverSessRemIdx - 1, 0)); /* This should set the timeout to 0 and call the remove callback from within - * the session cache. */ - ExpectIntEQ(SSL_CTX_remove_session(serverSessCtx, serverSess), 0); + * the session cache. Returns 1 per OpenSSL semantics (session was + * present in the cache and removed). */ + ExpectIntEQ(SSL_CTX_remove_session(serverSessCtx, serverSess), 1); ExpectNull(SSL_SESSION_get_ex_data(serverSess, serverSessRemIdx)); ExpectIntEQ(serverSessRemCountFree, 1); /* Need to free the references that we kept */ diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index ba12b7e368..f9414c6a53 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -2831,6 +2831,11 @@ int test_tls13_early_data(void) ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, params[i].client_meth, params[i].server_meth), 0); + /* Opt the server into 0-RTT (off by default per RFC 8446 E.5). */ + ExpectIntGE(wolfSSL_CTX_set_max_early_data(ctx_s, MAX_EARLY_DATA_SZ), + 0); + ExpectIntGE(wolfSSL_set_max_early_data(ssl_s, MAX_EARLY_DATA_SZ), 0); + if (params[i].isUdp) { /* Early data is incompatible with HRR usage. Hence, we have to make * sure a group is negotiated that does not cause a fragemented CH. @@ -3203,6 +3208,276 @@ int test_tls13_early_data_0rtt_replay(void) } #endif +/* Verify that maxEarlyDataSz defaults to 0 (RFC 8446 E.5): a server that + * has not called wolfSSL_set_max_early_data must not advertise 0-RTT in its + * NewSessionTicket. Fails without the ctx->maxEarlyDataSz=0 default fix + * because the old default was MAX_EARLY_DATA_SZ (4096). */ +int test_tls13_0rtt_default_off(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_TLS13) && defined(WOLFSSL_EARLY_DATA) && \ + defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + WOLFSSL_SESSION *sess = NULL; + char buf[64]; + int written = 0; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + /* Step 1: handshake WITHOUT opting into 0-RTT on the server. */ + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), + 0); + /* Deliberately do NOT call wolfSSL_set_max_early_data. */ + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + /* Consume NewSessionTicket. */ + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + wolfSSL_free(ssl_c); ssl_c = NULL; + wolfSSL_free(ssl_s); ssl_s = NULL; + + /* Step 2: resume - early data write must fail because the ticket + * was issued without max_early_data_size. Without the default-to-0 + * fix the old default (4096) would let this succeed. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), + 0); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_write_early_data(ssl_c, "test", 4, &written), + WOLFSSL_FATAL_ERROR); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* Verify that a stateless self-encrypted ticket can carry 0-RTT exactly + * once: the first resumption succeeds with early data, the second (replay) + * refuses it because wolfSSL_SSL_CTX_remove_session evicted the cache entry. + * Fails without the WOLFSSL_TICKET_HAVE_ID implication + the + * remove_session-based gate because the old code either never populated + * the cache for stateless tickets or never checked the return value. */ +int test_tls13_0rtt_stateless_replay(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_TLS13) && defined(WOLFSSL_EARLY_DATA) && \ + defined(HAVE_SESSION_TICKET) && defined(WOLFSSL_TICKET_HAVE_ID) && \ + !defined(NO_SESSION_CACHE) && !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + WOLFSSL_SESSION *sess = NULL; + char buf[64]; + int round; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + /* Step 1: full handshake to get a stateless ticket with 0-RTT. */ + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), + 0); + /* Do NOT set WOLFSSL_OP_NO_TICKET - keep stateless tickets. */ + ExpectIntGE(wolfSSL_CTX_set_max_early_data(ctx_s, MAX_EARLY_DATA_SZ), 0); + ExpectIntGE(wolfSSL_set_max_early_data(ssl_s, MAX_EARLY_DATA_SZ), 0); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + ExpectIntEQ(wolfSSL_SessionIsSetup(sess), 1); + + wolfSSL_free(ssl_c); ssl_c = NULL; + wolfSSL_free(ssl_s); ssl_s = NULL; + + /* Suppress ticket reissuance on resume so the eviction from round 0 + * is not undone by AddSession from a new NewSessionTicket. */ + ExpectIntEQ(wolfSSL_CTX_set_num_tickets(ctx_s, 0), WOLFSSL_SUCCESS); + + /* Step 2: resume twice. Round 0 = first use, round 1 = replay. */ + for (round = 0; round < 2 && !EXPECT_FAIL(); round++) { + const char earlyMsg[] = "stateless-0rtt"; + int written = 0; + int earlyRead = 0; + char earlyBuf[sizeof(earlyMsg)]; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + XMEMSET(earlyBuf, 0, sizeof(earlyBuf)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, + &ssl_s, wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), 0); + ExpectIntGE(wolfSSL_set_max_early_data(ssl_s, MAX_EARLY_DATA_SZ), 0); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + + ExpectIntEQ(test_tls13_early_data_write_until_write_ok(ssl_c, + earlyMsg, (int)sizeof(earlyMsg), &written), + sizeof(earlyMsg)); + ExpectIntEQ(written, sizeof(earlyMsg)); + + (void)test_tls13_early_data_read_until_write_ok(ssl_s, earlyBuf, + sizeof(earlyBuf), &earlyRead); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + if (round == 0) { + /* First use: 0-RTT accepted. */ + ExpectIntEQ(earlyRead, sizeof(earlyMsg)); + ExpectStrEQ(earlyMsg, earlyBuf); + } + else { + /* Replay: 0-RTT refused, handshake still completes (1-RTT). */ + ExpectIntEQ(earlyRead, 0); + } + + wolfSSL_free(ssl_c); ssl_c = NULL; + wolfSSL_free(ssl_s); ssl_s = NULL; + } + + wolfSSL_SESSION_free(sess); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* Verify wolfSSL_SSL_CTX_remove_session returns OpenSSL-compatible values: + * 1 when the session was in the cache and removed, 0 otherwise. + * Fails without the return-value fix because the old code returned 0/ + * BAD_FUNC_ARG. */ +int test_tls13_remove_session_return(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && \ + defined(WOLFSSL_TICKET_HAVE_ID) && !defined(NO_SESSION_CACHE) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + WOLFSSL_SESSION *sess = NULL; + char buf[64]; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), + 0); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + /* Consume NewSessionTicket so the cache is populated (AddSession fires + * because WOLFSSL_TICKET_HAVE_ID is defined). */ + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + + ExpectNotNull(sess = wolfSSL_get1_session(ssl_s)); + /* Session is in the cache - first remove returns 1. */ + ExpectIntEQ(wolfSSL_SSL_CTX_remove_session(ctx_s, sess), 1); + /* Already removed - second remove returns 0. */ + ExpectIntEQ(wolfSSL_SSL_CTX_remove_session(ctx_s, sess), 0); + /* NULL args - returns 0 (not BAD_FUNC_ARG). */ + ExpectIntEQ(wolfSSL_SSL_CTX_remove_session(NULL, sess), 0); + ExpectIntEQ(wolfSSL_SSL_CTX_remove_session(ctx_s, NULL), 0); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_TLS13) && defined(WOLFSSL_EARLY_DATA) && \ + defined(HAVE_SESSION_TICKET) && defined(WOLFSSL_TICKET_HAVE_ID) && \ + !defined(NO_SESSION_CACHE) && defined(HAVE_EXT_CACHE) +/* Thin external cache: only tracks rem_calls to verify that + * wolfSSL_SSL_CTX_remove_session counts the callback as "found". */ +static int test_0rtt_ext_only_rem_calls; + +static int test_0rtt_ext_only_new_cb(WOLFSSL* ssl, WOLFSSL_SESSION* s) +{ + (void)ssl; (void)s; + return 0; /* don't retain */ +} + +static WOLFSSL_SESSION* test_0rtt_ext_only_get_cb(WOLFSSL* ssl, + const byte* id, int idLen, int* ref) +{ + (void)ssl; (void)id; (void)idLen; + *ref = 0; + return NULL; +} + +static void test_0rtt_ext_only_rem_cb(WOLFSSL_CTX* ctx, WOLFSSL_SESSION* s) +{ + (void)ctx; (void)s; + test_0rtt_ext_only_rem_calls++; +} +#endif + +/* Verify that when the internal cache is off but an external cache callback + * is registered, wolfSSL_SSL_CTX_remove_session returns 1 (the ext callback + * fired, so we assume the session was present). Fails without the fix + * because the old code only set found=1 on an internal-cache hit. */ +int test_tls13_0rtt_ext_cache_eviction(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_TLS13) && defined(WOLFSSL_EARLY_DATA) && \ + defined(HAVE_SESSION_TICKET) && defined(WOLFSSL_TICKET_HAVE_ID) && \ + !defined(NO_SESSION_CACHE) && defined(HAVE_EXT_CACHE) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + WOLFSSL_SESSION *sess = NULL; + char buf[64]; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_0rtt_ext_only_rem_calls = 0; + + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), + 0); + /* Turn off internal cache; rely on external callbacks only. */ + ExpectIntEQ(wolfSSL_CTX_set_session_cache_mode(ctx_s, + WOLFSSL_SESS_CACHE_NO_INTERNAL), WOLFSSL_SUCCESS); + wolfSSL_CTX_sess_set_new_cb(ctx_s, test_0rtt_ext_only_new_cb); + wolfSSL_CTX_sess_set_get_cb(ctx_s, test_0rtt_ext_only_get_cb); + wolfSSL_CTX_sess_set_remove_cb(ctx_s, test_0rtt_ext_only_rem_cb); + + ExpectTrue(wolfSSL_set_options(ssl_s, WOLFSSL_OP_NO_TICKET) != 0); + ExpectIntGE(wolfSSL_set_max_early_data(ssl_s, MAX_EARLY_DATA_SZ), 0); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + + /* remove_session on an ext-cache-only server: rem_cb should fire and + * the function should return 1 (assumes the ext cache had it). */ + ExpectNotNull(sess = wolfSSL_get1_session(ssl_s)); + ExpectIntEQ(wolfSSL_SSL_CTX_remove_session(ctx_s, sess), 1); + ExpectIntGT(test_0rtt_ext_only_rem_calls, 0); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + /* Check that the client won't send the same CH after a HRR. An HRR without * a KeyShare or a Cookie extension will trigger the error. */ diff --git a/tests/api/test_tls13.h b/tests/api/test_tls13.h index c8b42fc56c..46fe39d04e 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -48,6 +48,10 @@ int test_tls13_pqc_hybrid_truncated_keyshare(void); int test_tls13_empty_record_limit(void); int test_tls13_short_session_ticket(void); int test_tls13_early_data_0rtt_replay(void); +int test_tls13_0rtt_default_off(void); +int test_tls13_0rtt_stateless_replay(void); +int test_tls13_remove_session_return(void); +int test_tls13_0rtt_ext_cache_eviction(void); int test_tls13_corrupted_finished(void); int test_tls13_peerauth_failsafe(void); int test_tls13_hrr_bad_cookie(void); @@ -85,6 +89,10 @@ int test_tls13_cert_with_extern_psk_sh_confirms_resumption(void); TEST_DECL_GROUP("tls13", test_tls13_empty_record_limit), \ TEST_DECL_GROUP("tls13", test_tls13_short_session_ticket), \ TEST_DECL_GROUP("tls13", test_tls13_early_data_0rtt_replay), \ + TEST_DECL_GROUP("tls13", test_tls13_0rtt_default_off), \ + TEST_DECL_GROUP("tls13", test_tls13_0rtt_stateless_replay), \ + TEST_DECL_GROUP("tls13", test_tls13_remove_session_return), \ + TEST_DECL_GROUP("tls13", test_tls13_0rtt_ext_cache_eviction), \ TEST_DECL_GROUP("tls13", test_tls13_unknown_ext_rejected), \ TEST_DECL_GROUP("tls13", test_tls13_corrupted_finished), \ TEST_DECL_GROUP("tls13", test_tls13_peerauth_failsafe), \ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 96606ba4f9..300b455fb3 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -1288,6 +1288,12 @@ enum { #define MAX_EARLY_DATA_SZ 4096 #endif +/* Anti-replay eviction keys off the ticket's session ID. */ +#if defined(WOLFSSL_EARLY_DATA) && defined(HAVE_SESSION_TICKET) && \ + !defined(WOLFSSL_TICKET_HAVE_ID) + #define WOLFSSL_TICKET_HAVE_ID +#endif + #if !defined(NO_RSA) || !defined(NO_DH) || defined(HAVE_ECC) /* MySQL wants to be able to use 8192-bit numbers. */