From 6115286faeb8fb023d79660e973a3252b142f6c1 Mon Sep 17 00:00:00 2001 From: Daniel Kubec Date: Mon, 30 Mar 2026 13:43:41 +0200 Subject: [PATCH] TLSv1.3: reissue session ticket after full handshake on ciphersuite mismatch When session resumption falls back to a full handshake due to a ciphersuite mismatch, ensure a new session ticket is issued with the newly negotiated ciphersuite. Fixes #18549 Reviewed-by: Tomas Mraz Reviewed-by: Matt Caswell MergeDate: Thu Apr 16 11:27:51 2026 (Merged from https://github.com/openssl/openssl/pull/30626) --- ssl/statem/extensions_srvr.c | 7 +- test/build.info | 7 ++ test/recipes/70-test_tls13ticket.t | 26 +++++ test/tls13tickettest.c | 157 +++++++++++++++++++++++++++++ 4 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 test/recipes/70-test_tls13ticket.t create mode 100644 test/tls13tickettest.c diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c index 85727825c4..61b496b677 100644 --- a/ssl/statem/extensions_srvr.c +++ b/ssl/statem/extensions_srvr.c @@ -1512,7 +1512,12 @@ int tls_parse_ctos_psk(SSL_CONNECTION *s, PACKET *pkt, unsigned int context, SSL_SESSION_free(sess); sess = NULL; s->ext.early_data_ok = 0; - s->ext.ticket_expected = 0; + /* + * We fall back to a full handshake. The new session ticket will be + * issued to the client with the newly negotiated ciphersuite, + * allowing successful resumption on future connections. + */ + s->ext.ticket_expected = 1; continue; } break; diff --git a/test/build.info b/test/build.info index 4789b5b596..742737d873 100644 --- a/test/build.info +++ b/test/build.info @@ -943,6 +943,13 @@ IF[{- !$disabled{tests} -}] INCLUDE[dsa_no_digest_size_test]=../include ../apps/include DEPEND[dsa_no_digest_size_test]=../libcrypto.a libtestutil.a + IF[{- !$disabled{ecx} && !$disabled{tls1_3} -}] + PROGRAMS{noinst}=tls13ticket_test + SOURCE[tls13ticket_test]=tls13tickettest.c helpers/ssltestlib.c + INCLUDE[tls13ticket_test]=../include ../apps/include + DEPEND[tls13ticket_test]=../libcrypto ../libssl libtestutil.a + ENDIF + SOURCE[tls13encryptiontest]=tls13encryptiontest.c INCLUDE[tls13encryptiontest]=.. ../include ../apps/include DEPEND[tls13encryptiontest]=../libcrypto.a ../libssl.a libtestutil.a diff --git a/test/recipes/70-test_tls13ticket.t b/test/recipes/70-test_tls13ticket.t new file mode 100644 index 0000000000..0fb782bd0d --- /dev/null +++ b/test/recipes/70-test_tls13ticket.t @@ -0,0 +1,26 @@ +#! /usr/bin/env perl +# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. +# +# Licensed under the Apache License 2.0 (the "License"). You may not use +# this file except in compliance with the License. You can obtain a copy +# in the file LICENSE in the source distribution or at +# https://www.openssl.org/source/license.html + +use OpenSSL::Test::Simple; +use OpenSSL::Test qw/:DEFAULT srctop_file/; +use OpenSSL::Test::Utils qw(disabled); + +setup("test_tls13ticket"); + +plan skip_all => "needs TLSv1.3 enabled" + if disabled("tls1_3"); + +plan skip_all => "needs ECX enabled" + if disabled("ecx"); + + +plan tests => 1; + +ok(run(test(["tls13ticket_test", srctop_file("apps", "server.pem"), + srctop_file("apps", "server.pem")])), + "running tls13ticket_test"); diff --git a/test/tls13tickettest.c b/test/tls13tickettest.c new file mode 100644 index 0000000000..9470f41696 --- /dev/null +++ b/test/tls13tickettest.c @@ -0,0 +1,157 @@ +/* + * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the Apache License 2.0 (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#include +#include "helpers/ssltestlib.h" +#include "testutil.h" + +struct stats { + unsigned int tickets; +}; + +static char *cert = NULL; +static char *pkey = NULL; +static int stats_idx = -1; + +static int stats_init(struct stats *stats) +{ + memset(stats, 0, sizeof(*stats)); + return 1; +} + +static int sess_new_cb(SSL *ssl, SSL_SESSION *session) +{ + struct stats *stats = SSL_get_ex_data(ssl, stats_idx); + if (stats == NULL) + return 0; + if (SSL_is_init_finished(ssl) == 0) + stats->tickets++; + return 0; +} + +static void handshake_finished(const SSL *ssl) +{ + const char *endpoint = SSL_is_server(ssl) ? "server" : "client"; + if (SSL_session_reused(ssl)) + TEST_info("%s: Abbreviated handshake finished", endpoint); + else + TEST_info("%s: Full handshake finished", endpoint); +} + +static void info_cb(const SSL *ssl, int type, int val) +{ + const char *endpoint = SSL_is_server(ssl) ? "server" : "client"; + + if (type & SSL_CB_ALERT) { + const char *dir = (type & SSL_CB_READ) ? "read" : "write"; + + TEST_info("%s: alert %s: %s : %s", endpoint, dir, + SSL_alert_type_string_long(val), + SSL_alert_desc_string_long(val)); + } + if (type & SSL_CB_HANDSHAKE_DONE) + handshake_finished(ssl); +} + +static int set_callbacks(SSL *ssl) +{ + SSL_set_info_callback(ssl, info_cb); + return 1; +} + +static int set_shutdown(SSL *c, SSL *s) +{ + SSL_set_shutdown(c, SSL_SENT_SHUTDOWN | SSL_RECEIVED_SHUTDOWN); + SSL_set_shutdown(s, SSL_SENT_SHUTDOWN | SSL_RECEIVED_SHUTDOWN); + return 1; +} + +static int enable_tickets(SSL_CTX *s, SSL_CTX *c) +{ + unsigned int cf = SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL_STORE; + unsigned int sf = SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_INTERNAL_STORE; + + SSL_CTX_set_session_cache_mode(s, sf); + SSL_CTX_set_session_cache_mode(c, cf); + SSL_CTX_set_verify(c, SSL_VERIFY_NONE, NULL); + + SSL_CTX_sess_set_new_cb(s, sess_new_cb); + SSL_CTX_sess_set_new_cb(c, sess_new_cb); + + return 1; +} + +/* + * Verify ticket regeneration after fallback to a full handshake. If session + * resumption fails due to a ciphersuite mismatch, it falls back to a full + * handshake. In that case, ensure a new session ticket is issued reflecting the + * negotiated ciphersuite. + */ +static int test_tls13_ticket_ciphersuite_mismatch(void) +{ + struct stats stats1, stats2; + SSL_CTX *s_ctx = NULL, *c_ctx = NULL; + SSL *s_ssl = NULL, *c_ssl = NULL, *s = NULL, *c = NULL; + SSL_SESSION *sess = NULL; + int test; + + test = create_ssl_ctx_pair(NULL, TLS_server_method(), TLS_client_method(), + TLS1_3_VERSION, TLS1_3_VERSION, &s_ctx, &c_ctx, cert, pkey) + && TEST_true(SSL_CTX_set_ciphersuites(s_ctx, "TLS_AES_128_GCM_SHA256")) + && TEST_true(SSL_CTX_set_ciphersuites(c_ctx, "TLS_AES_128_GCM_SHA256")) + && TEST_true(enable_tickets(s_ctx, c_ctx)) + && TEST_true(create_ssl_objects(s_ctx, c_ctx, &s, &c, NULL, NULL)) + && TEST_true(set_callbacks(c)) + && TEST_true(set_callbacks(s)) + && TEST_true(stats_init(&stats1)) + && TEST_true(SSL_set_ex_data(c, stats_idx, &stats1)) + && TEST_true(create_ssl_connection(s, c, SSL_ERROR_NONE)) + && TEST_uint_eq(stats1.tickets, 2) + && TEST_true(set_shutdown(c, s)) + && TEST_ptr(sess = SSL_get1_session(c)) + && TEST_true(SSL_CTX_set_ciphersuites(s_ctx, "TLS_AES_256_GCM_SHA384")) + && TEST_true(SSL_CTX_set_ciphersuites(c_ctx, "TLS_AES_256_GCM_SHA384")) + && TEST_true(create_ssl_objects(s_ctx, c_ctx, &s_ssl, &c_ssl, NULL, NULL)) + && TEST_true(SSL_set_session(c_ssl, sess)) + && TEST_true(set_callbacks(c_ssl)) + && TEST_true(set_callbacks(s_ssl)) + && TEST_true(stats_init(&stats2)) + && TEST_true(SSL_set_ex_data(c_ssl, stats_idx, &stats2)) + && TEST_true(create_ssl_connection(s_ssl, c_ssl, SSL_ERROR_NONE)) + && TEST_false(SSL_session_reused(c_ssl)) + && TEST_uint_eq(stats2.tickets, 2); + + SSL_SESSION_free(sess); + SSL_free(s_ssl); + SSL_free(c_ssl); + SSL_free(s); + SSL_free(c); + SSL_CTX_free(s_ctx); + SSL_CTX_free(c_ctx); + return test; +} + +OPT_TEST_DECLARE_USAGE("\n") + +int setup_tests(void) +{ + if (!test_skip_common_options()) { + TEST_error("Error parsing test options\n"); + return 0; + } + + if (!TEST_ptr(cert = test_get_argument(0)) + || !TEST_ptr(pkey = test_get_argument(1))) + return 0; + + stats_idx = SSL_get_ex_new_index(0, NULL, NULL, NULL, NULL); + ADD_TEST(test_tls13_ticket_ciphersuite_mismatch); + + return 1; +} -- 2.47.3