mirror of
https://github.com/wolfSSL/wolfssl.git
synced 2026-07-10 05:20:52 +02:00
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:
+67
-51
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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), \
|
||||
|
||||
Reference in New Issue
Block a user