F-5807: extend EMS resumption check to ticket resumption

Address review on PR #10582:

- The client-side extended_master_secret consistency check skipped all
  session-ticket resumptions, leaving a generic ticket resumption open to
  an undetected EMS downgrade by a malicious server or MITM. The client
  retains the EMS state for ticket sessions too (SetupSession), so the
  check now applies to ticket resumption as well, mirroring the adjacent
  cipher-suite check. Only EAP-FAST style resumption - where the
  session-secret callback supplies the master secret for an opaque PAC
  ticket - is exempt, matched precisely via ssl->sessionSecretCb just as
  the callback invocation in DoServerHello does.

- Add test_tls_ems_resumption_server_downgrade, exercising the
  client-direction downgrade (server resumes but omits EMS from its
  ServerHello) for both session-ID and session-ticket resumption. This
  client-side branch previously had no test coverage.
This commit is contained in:
Juliusz Sosinowicz
2026-06-10 20:28:59 +00:00
parent 2da5b24438
commit 748678715a
4 changed files with 171 additions and 5 deletions
+9 -5
View File
@@ -32343,13 +32343,17 @@ 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. Stateless (session-ticket)
* resumption - e.g. EAP-FAST, whose PAC is a TLS ticket - binds
* the EMS state in the ticket and need not re-advertise the
* extension, so this applies only to session-ID resumption. */
* 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 &&
&& ssl->session->ticketLen > 0
#endif
) &&
#endif
ssl->session->haveEMS != ssl->options.haveEMS) {
WOLFSSL_MSG("Resumed session EMS state does not match "
+1
View File
@@ -35072,6 +35072,7 @@ TEST_CASE testCases[] = {
#endif
TEST_DECL(test_tls_ems_downgrade),
TEST_DECL(test_tls_ems_resumption_downgrade),
TEST_DECL(test_tls_ems_resumption_server_downgrade),
TEST_DECL(test_tls12_chacha20_poly1305_bad_tag),
TEST_DECL(test_tls13_null_cipher_bad_hmac),
TEST_DECL(test_scr_verify_data_mismatch),
+160
View File
@@ -153,6 +153,166 @@ int test_tls_ems_resumption_downgrade(void)
}
#if !defined(WOLFSSL_NO_TLS12) && defined(HAVE_EXTENDED_MASTER) && \
defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \
!defined(NO_SESSION_CACHE)
/* Remove the extended_master_secret extension from the ServerHello record at
* the head of the server-to-client memio buffer, patching up the record,
* handshake and extension-block lengths so the message still parses.
* Returns 0 on success. */
static int StripEmsFromServerHello(struct test_memio_ctx* test_ctx)
{
byte* buf = test_ctx->c_buff;
int len = test_ctx->c_len;
int recLen;
int hsLen;
int extsLenIdx;
int extsLen;
int idx;
int extsEnd;
/* Record header: type(1) version(2) length(2) */
if (len < 5 || buf[0] != handshake)
return -1;
recLen = (buf[3] << 8) | buf[4];
if (5 + recLen > len)
return -1;
/* Handshake header: type(1) length(3) */
if (recLen < HANDSHAKE_HEADER_SZ || buf[5] != server_hello)
return -1;
hsLen = (buf[6] << 16) | (buf[7] << 8) | buf[8];
/* Skip version(2), random(32) to the session ID length, then skip the
* session ID, cipher suite(2) and compression(1) to the extensions
* length. */
extsLenIdx = 5 + HANDSHAKE_HEADER_SZ + OPAQUE16_LEN + RAN_LEN +
OPAQUE8_LEN + buf[5 + HANDSHAKE_HEADER_SZ + OPAQUE16_LEN +
RAN_LEN] +
OPAQUE16_LEN + OPAQUE8_LEN;
if (extsLenIdx + OPAQUE16_LEN > 5 + recLen)
return -1;
extsLen = (buf[extsLenIdx] << 8) | buf[extsLenIdx + 1];
idx = extsLenIdx + OPAQUE16_LEN;
extsEnd = idx + extsLen;
if (extsEnd > 5 + recLen)
return -1;
while (idx + 4 <= extsEnd) {
int extType = (buf[idx] << 8) | buf[idx + 1];
int extLen = (buf[idx + 2] << 8) | buf[idx + 3];
int rmLen = 4 + extLen;
if (idx + rmLen > extsEnd)
return -1;
if (extType == HELLO_EXT_EXTMS) {
XMEMMOVE(buf + idx, buf + idx + rmLen,
(size_t)(len - idx - rmLen));
recLen -= rmLen;
hsLen -= rmLen;
extsLen -= rmLen;
buf[3] = (byte)(recLen >> 8);
buf[4] = (byte)recLen;
buf[6] = (byte)(hsLen >> 16);
buf[7] = (byte)(hsLen >> 8);
buf[8] = (byte)hsLen;
buf[extsLenIdx] = (byte)(extsLen >> 8);
buf[extsLenIdx + 1] = (byte)extsLen;
test_ctx->c_len -= rmLen;
/* The ServerHello record sits wholly inside the first buffered
* message. */
test_ctx->c_msg_sizes[0] -= rmLen;
return 0;
}
idx += rmLen;
}
return -1;
}
/* Full handshake with EMS, then resume and strip the EMS extension from the
* ServerHello in transit. The client must catch the downgrade and abort
* (RFC 7627 Section 5.3). useTicket selects session-ticket resumption
* instead of session-ID resumption. */
static int test_tls_ems_resumption_server_downgrade_ex(int useTicket)
{
EXPECT_DECLS;
struct test_memio_ctx test_ctx;
WOLFSSL_CTX *ctx_c = NULL;
WOLFSSL_CTX *ctx_s = NULL;
WOLFSSL *ssl_c = NULL;
WOLFSSL *ssl_s = NULL;
WOLFSSL_SESSION *session = NULL;
#ifndef HAVE_SESSION_TICKET
(void)useTicket;
#endif
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);
#ifdef HAVE_SESSION_TICKET
if (useTicket)
ExpectIntEQ(wolfSSL_UseSessionTicket(ssl_c), WOLFSSL_SUCCESS);
#endif
ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0);
ExpectNotNull(session = wolfSSL_get1_session(ssl_c));
ExpectTrue(session->haveEMS);
#ifdef HAVE_SESSION_TICKET
if (useTicket)
ExpectIntGT(session->ticketLen, 0);
#endif
wolfSSL_free(ssl_c);
ssl_c = NULL;
wolfSSL_free(ssl_s);
ssl_s = NULL;
test_memio_clear_buffer(&test_ctx, 0);
test_memio_clear_buffer(&test_ctx, 1);
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_session(ssl_c, session), WOLFSSL_SUCCESS);
/* ClientHello */
ExpectIntEQ(wolfSSL_connect(ssl_c), -1);
ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ);
/* Server flight accepting the resumption */
ExpectIntEQ(wolfSSL_accept(ssl_s), -1);
ExpectIntEQ(wolfSSL_get_error(ssl_s, -1), WOLFSSL_ERROR_WANT_READ);
/* Drop EMS from the ServerHello to simulate a downgrading server. */
ExpectIntEQ(StripEmsFromServerHello(&test_ctx), 0);
/* The client must refuse to resume without EMS. */
ExpectIntEQ(wolfSSL_connect(ssl_c), -1);
ExpectIntEQ(wolfSSL_get_error(ssl_c, -1),
WC_NO_ERR_TRACE(EXT_MASTER_SECRET_NEEDED_E));
wolfSSL_SESSION_free(session);
wolfSSL_free(ssl_c);
wolfSSL_free(ssl_s);
wolfSSL_CTX_free(ctx_c);
wolfSSL_CTX_free(ctx_s);
return EXPECT_RESULT();
}
#endif
/* F-5807: a server that resumes an EMS session but omits the
* extended_master_secret extension from its ServerHello must be rejected by
* the client with EXT_MASTER_SECRET_NEEDED_E (RFC 7627 Section 5.3), on both
* session-ID and session-ticket resumption. */
int test_tls_ems_resumption_server_downgrade(void)
{
EXPECT_DECLS;
#if !defined(WOLFSSL_NO_TLS12) && defined(HAVE_EXTENDED_MASTER) && \
defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \
!defined(NO_SESSION_CACHE)
ExpectIntEQ(test_tls_ems_resumption_server_downgrade_ex(0), TEST_SUCCESS);
#if defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB)
ExpectIntEQ(test_tls_ems_resumption_server_downgrade_ex(1), TEST_SUCCESS);
#endif
#endif
return EXPECT_RESULT();
}
#if !defined(WOLFSSL_NO_TLS12) && \
defined(BUILD_TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256) && \
defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES)
+1
View File
@@ -24,6 +24,7 @@
int test_tls_ems_downgrade(void);
int test_tls_ems_resumption_downgrade(void);
int test_tls_ems_resumption_server_downgrade(void);
int test_tls12_chacha20_poly1305_bad_tag(void);
int test_tls13_null_cipher_bad_hmac(void);
int test_scr_verify_data_mismatch(void);