F-5811: defer resumed-session consistency checks to confirmed resumption

The client's resumed-session EMS (F-5807) and cipher-suite (F-5811) checks
were enforced in CompleteServerHello at ServerHello-parse time. For stateless
ticket resumption the client sends an empty session ID and cannot yet tell
whether the server accepted the ticket (RFC 5077 3.4): a server that declines
the ticket falls back to a full handshake under a freshly negotiated
suite/EMS state, which these checks wrongly aborted with MATCH_SUITE_ERROR,
breaking the RFC 5077 ticket-decline fallback to a full handshake.

Move both checks into CheckResumptionConsistency and run it only once
resumption is confirmed - from whichever the server sends first in the
abbreviated flight: a renewed NewSessionTicket (before SetupSession refreshes
the cached suite/EMS to the current values) or its ChangeCipherSpec. By then
the "Not resuming as thought" path has cleared 'resuming' for any ticket
decline, so the full-handshake fallback proceeds.

Add test_tls12_resume_ticket_decline_fallback (ticket declined by a fresh
server CTX, full handshake under a different suite must succeed) and gate
test_tls12_resume_ticket_wrong_suite on WOLFSSL_NO_DEF_TICKET_ENC_CB so it
skips rather than fails in builds without the default ticket encryption
callback.
This commit is contained in:
Juliusz Sosinowicz
2026-06-11 19:22:35 +00:00
parent 748678715a
commit 2352d73f7f
3 changed files with 138 additions and 51 deletions
+67 -51
View File
@@ -23150,6 +23150,52 @@ static void DropAndRestartProcessReply(WOLFSSL* ssl)
#endif /* WOLFSSL_DTLS_DROP_STATS */
}
#endif /* WOLFSSL_DTLS */
#ifndef WOLFSSL_NO_TLS12
/* On a confirmed TLS 1.2 / DTLS 1.2 client resumption, check the abbreviated
* ServerHello's EMS state (RFC 7627 5.3) and cipher suite (RFC 5246 7.4.1.3)
* match the resumed session. Called once resumption is confirmed - at a renewed
* NewSessionTicket (before SetupSession refreshes the cached values) or the
* server ChangeCipherSpec. Deferred from ServerHello because a declined ticket
* (RFC 5077 3.4) falls back to a full handshake that must not be rejected.
* Returns 0 if consistent, else sends a fatal alert and returns an error. */
static int CheckResumptionConsistency(WOLFSSL* ssl)
{
if (ssl->session == NULL) /* nothing to compare against */
return 0;
/* EMS must match (RFC 7627 5.3); skip EAP-FAST (session-secret callback). */
if (
#ifdef HAVE_SECRET_CALLBACK
!(ssl->sessionSecretCb != NULL
#ifdef HAVE_SESSION_TICKET
&& ssl->session->ticketLen > 0
#endif
) &&
#endif
ssl->session->haveEMS != ssl->options.haveEMS) {
WOLFSSL_MSG("Resumed session EMS state does not match "
"ServerHello EMS state");
SendAlert(ssl, alert_fatal, handshake_failure);
WOLFSSL_ERROR_VERBOSE(EXT_MASTER_SECRET_NEEDED_E);
return EXT_MASTER_SECRET_NEEDED_E;
}
#ifndef NO_RESUME_SUITE_CHECK
/* Suite must match (RFC 5246 7.4.1.3), tickets included. Skip when no suite
* was retained (both zero = TLS_NULL_WITH_NULL_NULL, e.g. EAP-FAST PAC). */
if ((ssl->session->cipherSuite0 != 0 || ssl->session->cipherSuite != 0) &&
(ssl->options.cipherSuite0 != ssl->session->cipherSuite0 ||
ssl->options.cipherSuite != ssl->session->cipherSuite)) {
WOLFSSL_MSG("Resumed session cipher suite does not match "
"ServerHello cipher suite");
SendAlert(ssl, alert_fatal, illegal_parameter);
WOLFSSL_ERROR_VERBOSE(MATCH_SUITE_ERROR);
return MATCH_SUITE_ERROR;
}
#endif /* NO_RESUME_SUITE_CHECK */
return 0;
}
#endif /* !WOLFSSL_NO_TLS12 */
/* Process input requests. Return 0 is done, 1 is call again to complete, and
negative number is error. If allowSocketErr is set, SOCKET_ERROR_E in
ssl->error will be whitelisted. This is useful when the connection has been
@@ -23980,6 +24026,15 @@ default:
}
}
/* Server CCS confirms the abbreviated handshake: validate
* the resumed session before installing keys. */
if (ssl->options.side == WOLFSSL_CLIENT_END &&
ssl->options.resuming) {
ret = CheckResumptionConsistency(ssl);
if (ret != 0)
return ret;
}
ssl->keys.encryptionOn = 1;
/* setup decrypt keys for following messages */
@@ -32342,57 +32397,9 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key)
}
else {
if (DSH_CheckSessionId(ssl)) {
/* RFC 7627 5.3: resumed session EMS state must match the
* ServerHello; abort on mismatch. Covers ticket resumption
* too. Skip only EAP-FAST (sessionSecretCb supplied the PAC
* ticket's secret in DoServerHello), whose synthetic session
* has no negotiated EMS state to compare. */
if (
#ifdef HAVE_SECRET_CALLBACK
!(ssl->sessionSecretCb != NULL
#ifdef HAVE_SESSION_TICKET
&& ssl->session->ticketLen > 0
#endif
) &&
#endif
ssl->session->haveEMS != ssl->options.haveEMS) {
WOLFSSL_MSG("Resumed session EMS state does not match "
"ServerHello EMS state");
SendAlert(ssl, alert_fatal, handshake_failure);
WOLFSSL_ERROR_VERBOSE(EXT_MASTER_SECRET_NEEDED_E);
return EXT_MASTER_SECRET_NEEDED_E;
}
#ifndef NO_RESUME_SUITE_CHECK
/* RFC 5246 Section 7.4.1.3: on resumption the ServerHello
* reuses the previously negotiated cipher suite. Reject a
* server that resumes the session but selects a different
* suite. Unlike the EMS check above this also covers ticket
* resumption: the ticket is opaque to the client, so it cannot
* rely on the suite being bound inside the ticket and must
* enforce the match against the suite retained in the cached
* session (SetupSession stores it for ticket sessions too).
* Otherwise a server could resume a ticket under a different,
* weaker suite the client offered and the downgrade would go
* undetected.
*
* Skip only when the client retained no suite for the session
* (cipherSuite0/cipherSuite both zero): then there is nothing to
* compare against. This is the case for EAP-FAST, whose PAC is a
* TLS ticket whose keys are supplied through the session-secret
* callback and which never populates the cached suite. (0,0) is
* TLS_NULL_WITH_NULL_NULL, never a negotiated suite, so it
* unambiguously means "no retained suite". */
if ((ssl->session->cipherSuite0 != 0 ||
ssl->session->cipherSuite != 0) &&
(ssl->options.cipherSuite0 != ssl->session->cipherSuite0 ||
ssl->options.cipherSuite != ssl->session->cipherSuite)) {
WOLFSSL_MSG("Resumed session cipher suite does not match "
"ServerHello cipher suite");
SendAlert(ssl, alert_fatal, illegal_parameter);
WOLFSSL_ERROR_VERBOSE(MATCH_SUITE_ERROR);
return MATCH_SUITE_ERROR;
}
#endif /* NO_RESUME_SUITE_CHECK */
/* EMS/suite consistency is checked once resumption is confirmed
* (CheckResumptionConsistency), not here: a ticket the server
* declines (RFC 5077 3.4) must fall back to a full handshake. */
if (SetCipherSpecs(ssl) == 0) {
if (!HaveUniqueSessionObj(ssl)) {
WOLFSSL_MSG("Unable to have unique session object");
@@ -35641,6 +35648,15 @@ static int DoSessionTicket(WOLFSSL* ssl, const byte* input, word32* inOutIdx,
return SESSION_TICKET_EXPECT_E;
}
/* A renewed ticket while resuming confirms resumption; check before the
* SetupSession() below refreshes the cached suite/EMS and masks a downgrade.
* (The ChangeCipherSpec check covers the no-renewal case.) */
if (ssl->options.resuming) {
ret = CheckResumptionConsistency(ssl);
if (ret != 0)
return ret;
}
if (OPAQUE32_LEN > size)
return BUFFER_ERROR;
+69
View File
@@ -890,6 +890,7 @@ int test_tls12_resume_ticket_wrong_suite(void)
EXPECT_DECLS;
#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \
!defined(WOLFSSL_NO_TLS12) && defined(HAVE_SESSION_TICKET) && \
!defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \
!defined(NO_RESUME_SUITE_CHECK) && !defined(NO_RSA) && defined(HAVE_ECC) && \
!defined(NO_AES) && defined(HAVE_AESGCM) && !defined(NO_SHA256) && \
defined(BUILD_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256)
@@ -969,6 +970,74 @@ int test_tls12_resume_ticket_wrong_suite(void)
return EXPECT_RESULT();
}
/* A ticket the server can't honor must fall back to a full handshake (RFC 5077
* 3.4), even under a different suite than the cached ticket session - the
* F-5811 suite check must not abort it. The second handshake uses a fresh
* server CTX (new ticket key -> decline) offering only suite B while the client
* offers B and the session's suite A. */
int test_tls12_resume_ticket_decline_fallback(void)
{
EXPECT_DECLS;
#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \
!defined(WOLFSSL_NO_TLS12) && defined(HAVE_SESSION_TICKET) && \
!defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_SESSION_CACHE) && \
!defined(NO_RESUME_SUITE_CHECK) && !defined(NO_RSA) && defined(HAVE_ECC) && \
!defined(NO_AES) && defined(HAVE_AESGCM) && !defined(NO_SHA256) && \
defined(BUILD_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) && \
defined(BUILD_TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384)
const char* suiteA = "ECDHE-RSA-AES128-GCM-SHA256";
const char* suiteB = "ECDHE-RSA-AES256-GCM-SHA384";
const char* suiteBA =
"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256";
WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL, *ctx_s2 = NULL;
WOLFSSL *ssl_c = NULL, *ssl_s = NULL;
WOLFSSL *ssl_c2 = NULL, *ssl_s2 = NULL;
WOLFSSL_SESSION *sess = NULL;
struct test_memio_ctx test_ctx;
struct test_memio_ctx test_ctx2;
/* First handshake: establish a ticket-based TLS 1.2 session on suite A. */
XMEMSET(&test_ctx, 0, sizeof(test_ctx));
ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s,
wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0);
ExpectIntEQ(wolfSSL_set_cipher_list(ssl_c, suiteA), WOLFSSL_SUCCESS);
ExpectIntEQ(wolfSSL_set_cipher_list(ssl_s, suiteA), WOLFSSL_SUCCESS);
ExpectIntEQ(wolfSSL_UseSessionTicket(ssl_c), WOLFSSL_SUCCESS);
ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0);
ExpectNotNull(sess = wolfSSL_get1_session(ssl_c));
ExpectIntGT(sess->ticketLen, 0);
/* Second handshake: fresh server CTX (NULL ctx_s2 -> new ticket key) so the
* ticket is declined and the server does a full handshake on suite B. */
XMEMSET(&test_ctx2, 0, sizeof(test_ctx2));
ExpectIntEQ(test_memio_setup(&test_ctx2, &ctx_c, &ctx_s2, &ssl_c2, &ssl_s2,
wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0);
ExpectIntEQ(wolfSSL_set_cipher_list(ssl_c2, suiteBA), WOLFSSL_SUCCESS);
ExpectIntEQ(wolfSSL_set_cipher_list(ssl_s2, suiteB), WOLFSSL_SUCCESS);
/* Session cache off so the declining server emits an empty session ID and
* the client takes the graceful full-handshake fallback (set on the SSL as
* the flag is copied from the CTX at wolfSSL_new() time). */
if (ssl_s2 != NULL)
ssl_s2->options.sessionCacheOff = 1;
ExpectIntEQ(wolfSSL_UseSessionTicket(ssl_c2), WOLFSSL_SUCCESS);
ExpectIntEQ(wolfSSL_set_session(ssl_c2, sess), WOLFSSL_SUCCESS);
/* Fallback must succeed (no MATCH_SUITE_ERROR), not resume, and use B. */
ExpectIntEQ(test_memio_do_handshake(ssl_c2, ssl_s2, 10, NULL), 0);
ExpectIntEQ(wolfSSL_session_reused(ssl_c2), 0);
ExpectStrEQ(wolfSSL_get_cipher_name(ssl_c2), suiteB);
wolfSSL_SESSION_free(sess);
wolfSSL_free(ssl_c);
wolfSSL_free(ssl_s);
wolfSSL_free(ssl_c2);
wolfSSL_free(ssl_s2);
wolfSSL_CTX_free(ctx_c);
wolfSSL_CTX_free(ctx_s);
wolfSSL_CTX_free(ctx_s2);
#endif
return EXPECT_RESULT();
}
/* wolfSSL_set_session() must reject a TLS 1.2 session when minDowngrade is
* set to TLS 1.3. */
int test_tls_set_session_min_downgrade(void)
+2
View File
@@ -33,6 +33,7 @@ int test_tls12_bad_cv_sig_alg(void);
int test_tls12_no_null_compression(void);
int test_tls12_etm_failed_resumption(void);
int test_tls12_resume_ticket_wrong_suite(void);
int test_tls12_resume_ticket_decline_fallback(void);
int test_tls_set_session_min_downgrade(void);
int test_tls12_session_id_resumption_sni_mismatch(void);
int test_tls13_session_resumption_sni_mismatch(void);
@@ -57,6 +58,7 @@ int test_record_size_cache_invalidated_on_renegotiation(void);
TEST_DECL_GROUP("tls", test_tls12_no_null_compression), \
TEST_DECL_GROUP("tls", test_tls12_etm_failed_resumption), \
TEST_DECL_GROUP("tls", test_tls12_resume_ticket_wrong_suite), \
TEST_DECL_GROUP("tls", test_tls12_resume_ticket_decline_fallback), \
TEST_DECL_GROUP("tls", test_tls_set_session_min_downgrade), \
TEST_DECL_GROUP("tls", test_tls12_session_id_resumption_sni_mismatch), \
TEST_DECL_GROUP("tls", test_tls13_session_resumption_sni_mismatch), \