Commit bbac7bd032 for openssl.org
commit bbac7bd03284afbca2bcdbbaf2f7b776af6bf46f
Author: Viktor Dukhovni <viktor@openssl.org>
Date: Wed Jul 15 00:30:02 2026 +1000
PSK: Check for invalid server PSK ciphers
If an application fails to set the PSK session cipher, or sets it
to a TLS 1.2 cipher by mistake, skip the invalid PSK, avoiding a
crash or an invalid outcome.
Tests added to make sure a full handshake takes place instead.
Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
Merge-date: Fri Oct 9 13:56:05 2026
Merged-from: https://github.com/openssl/openssl/pull/31925
diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c
index 64a46af4b9..8b0ed79ad2 100644
--- a/ssl/statem/extensions_srvr.c
+++ b/ssl/statem/extensions_srvr.c
@@ -1373,6 +1373,9 @@ int tls_parse_ctos_psk(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
unsigned long ticket_agel;
size_t idlen;
+ /* Reset for each fresh iteration. */
+ sess = NULL;
+
if (!PACKET_get_length_prefixed_2(&identities, &identity)
|| !PACKET_get_net_4(&identities, &ticket_agel)) {
SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
@@ -1539,6 +1542,26 @@ int tls_parse_ctos_psk(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
ext = 0;
}
+ /*
+ * The binder below, and the 0-RTT gate, both key off sess->cipher --
+ * specifically its handshake digest (algorithm2), which is only
+ * meaningful for a TLS 1.3 (or later) ciphersuite. A
+ * psk_find_session_cb() may return a session with no cipher, or a
+ * pre-TLS-1.3 one; ignore it and fall back to a full handshake rather
+ * than dereference a NULL cipher or misread algorithm2.
+ *
+ * The protocol version is deliberately not checked here:
+ * ssl_get_prev_session() vets it the moment we return and discards any
+ * mismatch, freeing the session so nothing leaks into a ticket or cache.
+ */
+ if (sess->cipher == NULL || sess->cipher->min_tls < TLS1_3_VERSION) {
+ SSL_SESSION_free(sess);
+ sess = NULL;
+ s->ext.early_data_ok = 0;
+ s->ext.ticket_expected = 1;
+ continue;
+ }
+
md = ssl_md(sctx, sess->cipher->algorithm2);
if (md == NULL) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
diff --git a/test/tls13tickettest.c b/test/tls13tickettest.c
index 4fad87d93c..75dad82508 100644
--- a/test/tls13tickettest.c
+++ b/test/tls13tickettest.c
@@ -2083,6 +2083,117 @@ static int test_tls13_external_psk_no_master_key(void)
return test;
}
+/*
+ * A server psk_find_session callback whose result is chosen by badsess_kind,
+ * to exercise the server-side vetting in tls_parse_ctos_psk():
+ * 0: a TLS 1.3 session with no ciphersuite -- its NULL cipher must not be
+ * dereferenced (rejected by the guard);
+ * 1: a valid TLS 1.3 cipher but a foreign (TLS 1.2) protocol version --
+ * rejected downstream by ssl_get_prev_session(), not by the guard;
+ * 2: a well-formed TLS 1.3 session -- the positive control, which resumes;
+ * 3: a TLS 1.2 ciphersuite (matching SHA-256 digest) tagged with a TLS 1.3
+ * version -- slips past the version and digest checks, so only the guard's
+ * min_tls test rejects it.
+ * Every kind but the control (2) must be ignored, falling back to a full
+ * (certificate) handshake rather than crashing or resuming.
+ */
+static int badsess_kind = 0;
+
+static int badsess_psk_find_cb(SSL *ssl, const unsigned char *id, size_t idlen,
+ SSL_SESSION **sess)
+{
+ static const unsigned char tls13_aes128gcmsha256_id[] = { 0x13, 0x01 };
+ SSL_SESSION *ns;
+
+ if (idlen != sizeof(ext_psk_id) || memcmp(id, ext_psk_id, idlen) != 0) {
+ *sess = NULL;
+ return 1;
+ }
+ if ((ns = SSL_SESSION_new()) == NULL
+ || !SSL_SESSION_set1_master_key(ns, ext_psk_key, sizeof(ext_psk_key))) {
+ SSL_SESSION_free(ns);
+ return 0;
+ }
+ if (badsess_kind == 0) {
+ /* TLS 1.3 version but no ciphersuite. */
+ if (!SSL_SESSION_set_protocol_version(ns, TLS1_3_VERSION)) {
+ SSL_SESSION_free(ns);
+ return 0;
+ }
+ } else if (badsess_kind == 3) {
+ /*
+ * A TLS 1.2 ciphersuite whose handshake digest (SHA-256) matches the
+ * negotiated TLS 1.3 one, paired with a TLS 1.3 version. This slips
+ * past ssl_get_prev_session()'s version check and the digest-compat
+ * check, and its binder even matches -- so only the min_tls guard
+ * distinguishes it. It must be rejected.
+ */
+ static const unsigned char tls12_aes128gcmsha256_id[] = { 0x00, 0x9c };
+ const SSL_CIPHER *c12 = SSL_CIPHER_find(ssl, tls12_aes128gcmsha256_id);
+
+ if (c12 == NULL
+ || !SSL_SESSION_set_cipher(ns, c12)
+ || !SSL_SESSION_set_protocol_version(ns, TLS1_3_VERSION)) {
+ SSL_SESSION_free(ns);
+ return 0;
+ }
+ } else {
+ /*
+ * A valid TLS 1.3 cipher, paired with either a foreign (TLS 1.2)
+ * protocol version (kind 1, must be rejected) or the correct one
+ * (kind 2, the positive control that must resume).
+ */
+ const SSL_CIPHER *cipher = SSL_CIPHER_find(ssl, tls13_aes128gcmsha256_id);
+ int version = badsess_kind == 1 ? TLS1_2_VERSION : TLS1_3_VERSION;
+
+ if (cipher == NULL
+ || !SSL_SESSION_set_cipher(ns, cipher)
+ || !SSL_SESSION_set_protocol_version(ns, version)) {
+ SSL_SESSION_free(ns);
+ return 0;
+ }
+ }
+ *sess = ns;
+ return 1;
+}
+
+/*
+ * The client offers a well-formed external PSK; the server's find_session
+ * callback resolves it to a malformed session (see badsess_psk_find_cb). The
+ * server must reject the PSK and complete a full handshake (not resume, not
+ * crash).
+ */
+static int test_tls13_psk_bad_server_session(int idx)
+{
+ SSL_CTX *c = NULL, *s = NULL;
+ struct tls13_channel conn = { .c.ssl = NULL, .s.ssl = NULL };
+ int test;
+
+ badsess_kind = idx;
+ test = TEST_true(create_ssl_ctx_pair(NULL, TLS_server_method(),
+ TLS_client_method(), TLS1_3_VERSION, TLS1_3_VERSION,
+ &s, &c, cert, pkey))
+ && TEST_true(set_ctx_callbacks(c, s))
+ && TEST_true(SSL_CTX_set_ciphersuites(s, "TLS_AES_128_GCM_SHA256"))
+ && TEST_true(SSL_CTX_set_ciphersuites(c, "TLS_AES_128_GCM_SHA256"))
+ && TEST_true(tls_channel_init(c, s, &conn))
+ && TEST_true((SSL_set_psk_use_session_callback(conn.c.ssl,
+ ext_psk_use_cb),
+ 1))
+ && TEST_true((SSL_set_psk_find_session_callback(conn.s.ssl,
+ badsess_psk_find_cb),
+ 1))
+ && TEST_true(create_ssl_connection(conn.s.ssl, conn.c.ssl,
+ SSL_ERROR_NONE))
+ /* Only the well-formed control (idx 2) resumes; the rest fall back. */
+ && TEST_int_eq(SSL_session_reused(conn.c.ssl), idx == 2);
+
+ tls_channel_fini(&conn);
+ SSL_CTX_free(c);
+ SSL_CTX_free(s);
+ return test;
+}
+
int setup_tests(void)
{
if (!test_skip_common_options()) {
@@ -2118,6 +2229,7 @@ int setup_tests(void)
ADD_TEST(test_tls13_ticket_cipher_retire_full_handshake);
ADD_TEST(test_tls13_external_psk_digest_not_offered);
ADD_TEST(test_tls13_external_psk_no_master_key);
+ ADD_ALL_TESTS(test_tls13_psk_bad_server_session, 4);
return 1;
}