Commit 9c4d2c7bff for openssl.org
commit 9c4d2c7bff92e678b88a5964a11c7057925a0bbb
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date: Sat Jun 6 22:08:42 2026 +0900
Reject TLS 1.3 early data when PSK ciphersuite differs
TLS 1.3 PSK resumption can use any ciphersuite with the same hash, but 0-RTT
acceptance requires the selected ciphersuite to match the one associated with
the selected PSK.
Keep same-hash PSK resumption working and reject only early data on a
ciphersuite mismatch. Document the user-visible fix in CHANGES.md.
Also updated RFC8446 references in ssl/*.[ch] code comments to corresponding
RFC9846 locations.
Fixes #31803
Reviewed-by: Bob Beck <beck@openssl.org>
Reviewed-by: Norbert Pocs <norbertp@openssl.org>
MergeDate: Wed Aug 5 08:16:57 2026
(Merged from https://github.com/openssl/openssl/pull/32032)
diff --git a/CHANGES.md b/CHANGES.md
index 4e5b021968..fb46f8afea 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -101,6 +101,12 @@ OpenSSL Releases
*Jakub Zelenka*
+ * Fixed TLS 1.3 servers to reject early data when the selected ciphersuite
+ differs from the ciphersuite associated with the selected PSK. Same-hash
+ PSK resumption can still continue without accepting 0-RTT data.
+
+ *Mounir IDRASSI*
+
* Added support for Ed25519 and Ed448 certificates in DTLS 1.2. Previously,
these certificate types were only supported in TLS 1.2 and TLS 1.3.
diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c
index a597085e1b..802081045e 100644
--- a/ssl/statem/extensions_srvr.c
+++ b/ssl/statem/extensions_srvr.c
@@ -702,7 +702,7 @@ static KS_EXTRACTION_RESULT extract_keyshares(SSL_CONNECTION *s, PACKET *key_sha
/*
* Check if this share is in supported_groups sent from client
- * RFC 8446 also mandates that clients send keyshares in the same
+ * RFC 9846 also mandates that clients send keyshares in the same
* order as listed in the supported groups extension, but its not
* required that the server check that, and some clients violate this
* so instead of failing the connection when that occurs, log a trace
@@ -714,7 +714,7 @@ static KS_EXTRACTION_RESULT extract_keyshares(SSL_CONNECTION *s, PACKET *key_sha
}
if (key_share_pos < previous_key_share_pos)
- OSSL_TRACE1(TLS, "key share group id %d is out of RFC 8446 order\n", group_id);
+ OSSL_TRACE1(TLS, "key share group id %d is out of RFC 9846 order\n", group_id);
previous_key_share_pos = key_share_pos;
@@ -1511,6 +1511,13 @@ int tls_parse_ctos_psk(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
s->ext.ticket_expected = 1;
continue;
}
+ /*
+ * Same-hash ciphersuite changes are allowed for TLSv1.3 PSK
+ * resumption, but RFC 9846 Section 4.3.10 requires the selected
+ * ciphersuite to match the selected PSK before accepting early data.
+ */
+ if (sess->cipher->id != s->s3.tmp.new_cipher->id)
+ s->ext.early_data_ok = 0;
break;
}
@@ -1525,7 +1532,7 @@ int tls_parse_ctos_psk(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
}
/*
* decrypt_error here to keep the alert the same as if the binder
- * failed. See RFC8446 Appendix E.6. Note we make no attempt to do this
+ * failed. See RFC9846 Appendix F.6. Note we make no attempt to do this
* in constant time compared to verifying the binder. None of this code
* is constant time anyway.
*/
diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c
index d719ef8a84..dc99eccea8 100644
--- a/ssl/statem/statem_clnt.c
+++ b/ssl/statem/statem_clnt.c
@@ -3179,7 +3179,7 @@ MSG_PROCESS_RETURN tls_process_new_session_ticket(SSL_CONNECTION *s,
PACKET extpkt;
/*
- * Fulfilling RFC8446:4.6.1 requirement: Clients MUST NOT cache
+ * Fulfilling RFC9846:4.7.1 requirement: Clients MUST NOT cache
* tickets for longer than 7 days.
*/
if (ticket_lifetime_hint > 604800) {
diff --git a/ssl/statem/statem_lib.c b/ssl/statem/statem_lib.c
index 465d2eff33..ed71defac1 100644
--- a/ssl/statem/statem_lib.c
+++ b/ssl/statem/statem_lib.c
@@ -1126,7 +1126,7 @@ int tls_process_rpk(SSL_CONNECTION *sc, PACKET *pkt, EVP_PKEY **peer_rpk)
* ----------------------------
* TLS 1.3 Certificate message:
* ----------------------------
- * https://datatracker.ietf.org/doc/html/rfc8446#section-4.4.2
+ * https://datatracker.ietf.org/doc/html/rfc9846#section-4.5.1
*
* enum {
* X509(0),
@@ -1961,7 +1961,7 @@ static int is_tls13_capable(const SSL_CONNECTION *s)
/*
* Prior to TLSv1.3 sig algs allowed any curve to be used. TLSv1.3 is
* more restrictive so check that our sig algs are consistent with this
- * EC cert. See section 4.2.3 of RFC8446.
+ * EC cert. See section 4.3.3 of RFC9846.
*/
curve = ssl_get_EC_curve_nid(s->cert->pkeys[SSL_PKEY_ECC].privatekey);
if (tls_check_sigalg_curve(s, curve))
diff --git a/ssl/statem/statem_local.h b/ssl/statem/statem_local.h
index de9ff299be..ffb1d428fd 100644
--- a/ssl/statem/statem_local.h
+++ b/ssl/statem/statem_local.h
@@ -37,10 +37,10 @@
#define KEY_UPDATE_MAX_LENGTH 1
#define CCS_MAX_LENGTH 1
-/* Max ServerHello size permitted by RFC 8446 */
+/* Max ServerHello size permitted by RFC 9846 */
#define SERVER_HELLO_MAX_LENGTH 65607
-/* Max CertificateVerify size permitted by RFC 8446 */
+/* Max CertificateVerify size permitted by RFC 9846 */
#define CERTIFICATE_VERIFY_MAX_LENGTH 65539
/* Max should actually be 36 but we are generous */
diff --git a/ssl/statem/statem_srvr.c b/ssl/statem/statem_srvr.c
index fc3769a017..11b8df52ee 100644
--- a/ssl/statem/statem_srvr.c
+++ b/ssl/statem/statem_srvr.c
@@ -701,7 +701,7 @@ static WRITE_TRAN ossl_statem_server13_write_transition(SSL_CONNECTION *s)
* parameters such as new_session_count = 0 or resumption_count = 0, is
* effectively signaling no interest in session tickets or resumption.
*
- * RFC 8446 section 4.2.9: Servers MUST NOT select a key exchange mode
+ * RFC 9846 section 4.3.9: Servers MUST NOT select a key exchange mode
* that is not listed by the client. This extension also restricts the
* modes for use with PSK resumption. Servers SHOULD NOT send
* NewSessionTicket with tickets that are not compatible with the
@@ -4243,7 +4243,7 @@ static int create_ticket_prequel(SSL_CONNECTION *s, WPACKET *pkt,
/*
* Ticket lifetime hint:
* In TLSv1.3 we reset the "time" field above, and always specify the
- * timeout, limited to a 1 week period per RFC8446.
+ * timeout, limited to a 1 week period per RFC9846.
* For TLSv1.2 this is advisory only and we leave this unspecified for
* resumed session (for simplicity).
*/
diff --git a/ssl/t1_lib.c b/ssl/t1_lib.c
index f8056c82da..15a4e09591 100644
--- a/ssl/t1_lib.c
+++ b/ssl/t1_lib.c
@@ -4080,7 +4080,7 @@ static int tls1_check_sig_alg(SSL_CONNECTION *s, X509 *x, int default_nid)
size_t sigalgslen;
/*-
- * RFC 8446, section 4.2.3:
+ * RFC 9846, section 4.3.3:
*
* The signatures on certificates that are self-signed or certificates
* that are trust anchors are not validated, since they begin a
diff --git a/test/sslapitest.c b/test/sslapitest.c
index 1c76201654..b6081be702 100644
--- a/test/sslapitest.c
+++ b/test/sslapitest.c
@@ -5290,6 +5290,76 @@ end:
return testresult;
}
+/*
+ * Locks in the requirement that a resumed PSK's exact ciphersuite, not
+ * merely a shared digest, must match the negotiated one before 0-RTT data
+ * is accepted: the client encrypts its early data with the AEAD bound to
+ * its own PSK session before it can know what cipher the server will
+ * negotiate, so a same-digest-but-different-cipher negotiation must fall
+ * back to an ordinary connection rather than attempt decryption with the
+ * wrong cipher.
+ */
+static int test_early_data_psk_cipher_mismatch(void)
+{
+#if !defined(OPENSSL_NO_CHACHA) && !defined(OPENSSL_NO_POLY1305)
+ SSL_CTX *cctx = NULL, *sctx = NULL;
+ SSL *clientssl = NULL, *serverssl = NULL;
+ int testresult = 0;
+ SSL_SESSION *sess = NULL;
+ unsigned char buf[20];
+ size_t readbytes, written;
+
+ if (is_fips) {
+ testresult = TEST_skip("CHACHA is not supported in FIPS");
+ return 1;
+ }
+
+ if (!TEST_true(setupearly_data_test(&cctx, &sctx, &clientssl,
+ &serverssl, &sess, 2, SHA256_DIGEST_LENGTH)))
+ goto end;
+
+ /*
+ * The PSK is bound to AES-128-GCM (SHA256 digest), but both ends can
+ * only negotiate ChaCha20-Poly1305 -- same digest, different cipher.
+ */
+ if (!TEST_true(SSL_set_ciphersuites(clientssl,
+ "TLS_CHACHA20_POLY1305_SHA256"))
+ || !TEST_true(SSL_set_ciphersuites(serverssl,
+ "TLS_CHACHA20_POLY1305_SHA256")))
+ goto end;
+
+ SSL_set_connect_state(clientssl);
+ if (!TEST_true(SSL_write_early_data(clientssl, MSG1, strlen(MSG1),
+ &written)))
+ goto end;
+
+ if (!TEST_int_eq(SSL_read_early_data(serverssl, buf, sizeof(buf),
+ &readbytes),
+ SSL_READ_EARLY_DATA_FINISH)
+ || !TEST_int_eq(SSL_get_early_data_status(serverssl),
+ SSL_EARLY_DATA_REJECTED))
+ goto end;
+
+ if (!TEST_true(create_ssl_connection(serverssl, clientssl,
+ SSL_ERROR_NONE)))
+ goto end;
+
+ testresult = 1;
+end:
+ SSL_SESSION_free(sess);
+ SSL_SESSION_free(clientpsk);
+ SSL_SESSION_free(serverpsk);
+ clientpsk = serverpsk = NULL;
+ SSL_free(serverssl);
+ SSL_free(clientssl);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+#else
+ return 1;
+#endif
+}
+
/*
* Test that a server that doesn't try to read early data can handle a
* client sending some.
@@ -15537,6 +15607,7 @@ int setup_tests(void)
ADD_ALL_TESTS(test_early_data_not_sent, 3);
ADD_ALL_TESTS(test_early_data_psk, 8);
ADD_ALL_TESTS(test_early_data_psk_with_all_ciphers, 7);
+ ADD_TEST(test_early_data_psk_cipher_mismatch);
ADD_ALL_TESTS(test_early_data_not_expected, 3);
#ifndef OPENSSL_NO_TLS1_2
ADD_ALL_TESTS(test_early_data_tls1_2, 3);