diff --git a/src/internal.c b/src/internal.c index 00335f4fd9..e946ecb2b1 100644 --- a/src/internal.c +++ b/src/internal.c @@ -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; diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index 9a567125e4..960c3bf9cb 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -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) diff --git a/tests/api/test_tls.h b/tests/api/test_tls.h index eecafab511..28fe41abfe 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -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), \