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