From 9c4d2c7bff92e678b88a5964a11c7057925a0bbb Mon Sep 17 00:00:00 2001 From: Mounir IDRASSI Date: Sat, 6 Jun 2026 22:08:42 +0900 Subject: [PATCH] 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 Reviewed-by: Norbert Pocs MergeDate: Wed Aug 5 08:16:57 2026 (Merged from https://github.com/openssl/openssl/pull/32032) --- CHANGES.md | 6 +++ ssl/statem/extensions_srvr.c | 13 +++++-- ssl/statem/statem_clnt.c | 2 +- ssl/statem/statem_lib.c | 4 +- ssl/statem/statem_local.h | 4 +- ssl/statem/statem_srvr.c | 4 +- ssl/t1_lib.c | 2 +- test/sslapitest.c | 71 ++++++++++++++++++++++++++++++++++++ 8 files changed, 95 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4e5b0219684..fb46f8afea9 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 a597085e1b6..802081045e2 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 d719ef8a845..dc99eccea8a 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 465d2eff33c..ed71defac1a 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 de9ff299be5..ffb1d428fd8 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 fc3769a017b..11b8df52ee8 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 f8056c82da9..15a4e095915 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 1c762016540..b6081be7028 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); -- 2.47.3