diff options
| author | Viktor Dukhovni <[email protected]> | 2026-07-14 16:30:02 +0200 |
|---|---|---|
| committer | Norbert Pocs <[email protected]> | 2026-10-09 15:56:02 +0200 |
| commit | bbac7bd03284afbca2bcdbbaf2f7b776af6bf46f (patch) | |
| tree | b23ad495ff7cc5cac9b7ae19fa4b4c45c1fe388d | |
| parent | Polish psk callback error path cleanup (diff) | |
| download | openssl-bbac7bd03284afbca2bcdbbaf2f7b776af6bf46f.tar.gz openssl-bbac7bd03284afbca2bcdbbaf2f7b776af6bf46f.zip | |
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 <[email protected]>
Reviewed-by: Nikola Pajkovsky <[email protected]>
Merge-date: Fri Oct 9 13:56:05 2026
Merged-from: https://github.com/openssl/openssl/pull/31925
| -rw-r--r-- | ssl/statem/extensions_srvr.c | 23 | ||||
| -rw-r--r-- | test/tls13tickettest.c | 112 |
2 files changed, 135 insertions, 0 deletions
diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c index 64a46af4b9e..8b0ed79ad27 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 4fad87d93c3..75dad825086 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; } |
