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;
 }