From 154f2e2ea41b9bc1b6ac6e7867bd1fe527fb13c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 29 Jun 2026 13:14:24 +0200 Subject: [PATCH] F-6547 - Reject TLS KeyUpdate on QUIC connections QUIC performs key updates at the packet-protection layer via the Key Phase bit, so RFC 9001 section 6 requires a QUIC endpoint to reject any received TLS KeyUpdate handshake message as a fatal unexpected_message connection error and to never send one. The TLS 1.3 receive path processed the message normally, rotating traffic secrets and possibly emitting a prohibited KeyUpdate response, and the send path allowed a QUIC connection to originate a KeyUpdate. Guard the key_update case in SanityCheckTls13MsgReceived so a QUIC connection aborts with a fatal unexpected_message alert, and guard Tls13UpdateKeys so a QUIC connection cannot send a KeyUpdate. Add a QUIC unit test that feeds a post-handshake KeyUpdate and confirms the connection is refused. --- src/tls13.c | 20 +++++++++++++++++++- tests/quic.c | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/tls13.c b/src/tls13.c index 0778cc186b..92e89cecb3 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -13568,6 +13568,16 @@ static int SanityCheckTls13MsgReceived(WOLFSSL* ssl, byte type) case key_update: /* Valid on both sides. */ +#ifdef WOLFSSL_QUIC + /* RFC 9001 Section 6: QUIC performs key updates at the QUIC + * packet-protection layer, so a TLS KeyUpdate message must be + * rejected as a fatal unexpected_message connection error. */ + if (WOLFSSL_IS_QUIC(ssl)) { + WOLFSSL_MSG("KeyUpdate received over QUIC"); + WOLFSSL_ERROR_VERBOSE(SANITY_MSG_E); + return SANITY_MSG_E; + } +#endif /* Check state. * Client and server must have received finished message from other * side. @@ -14974,6 +14984,13 @@ int Tls13UpdateKeys(WOLFSSL* ssl) if (ssl == NULL || !IsAtLeastTLSv1_3(ssl->version)) return BAD_FUNC_ARG; +#ifdef WOLFSSL_QUIC + /* RFC 9001 Section 6: a QUIC connection must not send a TLS KeyUpdate; + * key updates are handled at the QUIC packet-protection layer. */ + if (WOLFSSL_IS_QUIC(ssl)) + return BAD_FUNC_ARG; +#endif + #ifdef WOLFSSL_DTLS13 /* we are already waiting for the ack of a sent key update message. We can't send another one before receiving its ack. Either wolfSSL_update_keys() @@ -14993,7 +15010,8 @@ int Tls13UpdateKeys(WOLFSSL* ssl) * calling wolfSSL_write() will have the message sent when ready. * * ssl The SSL/TLS object. - * returns BAD_FUNC_ARG when ssl is NULL, or not using TLS v1.3, + * returns BAD_FUNC_ARG when ssl is NULL, not using TLS v1.3, or running over + * QUIC (RFC 9001 handles key updates at the QUIC packet-protection layer), * WOLFSSL_ERROR_WANT_WRITE when non-blocking I/O is not ready to write, * WOLFSSL_SUCCESS on success and otherwise failure. */ diff --git a/tests/quic.c b/tests/quic.c index 3e8ed2a428..e11f7bfb93 100644 --- a/tests/quic.c +++ b/tests/quic.c @@ -1470,6 +1470,56 @@ static int test_quic_server_hello_fail(int verbose) { return EXPECT_RESULT(); } +static int test_quic_key_update_rejected(int verbose) { + EXPECT_DECLS; + WOLFSSL_CTX * ctx_c = NULL; + WOLFSSL_CTX * ctx_s = NULL; + QuicTestContext tclient, tserver; + QuicConversation conv; + uint8_t lbuffer[16]; + size_t len; + int ret; + + ExpectNotNull(ctx_c = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ctx_s = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectTrue(wolfSSL_CTX_use_certificate_file(ctx_s, svrCertFile, + WOLFSSL_FILETYPE_PEM)); + ExpectTrue(wolfSSL_CTX_use_PrivateKey_file(ctx_s, svrKeyFile, + WOLFSSL_FILETYPE_PEM)); + + /* complete a normal QUIC handshake */ + QuicTestContext_init(&tclient, ctx_c, "client", verbose); + QuicTestContext_init(&tserver, ctx_s, "server", verbose); + QuicConversation_init(&conv, &tclient, &tserver); + QuicConversation_do(&conv); + + /* RFC 9001 section 6: a QUIC connection must not send a TLS KeyUpdate; + * key updates are handled at the QUIC packet-protection layer. The + * public wolfSSL_update_keys() must refuse on a QUIC connection. */ + ExpectIntEQ(wolfSSL_update_keys(tserver.ssl), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + /* RFC 9001 section 6: a TLS KeyUpdate handshake message must be rejected + * as a fatal unexpected_message connection error when running over QUIC. + * Feed a key_update (update_not_requested) as post-handshake CRYPTO data + * and confirm the server refuses to process it. */ + len = fake_record(key_update, OPAQUE8_LEN, lbuffer); + lbuffer[HANDSHAKE_HEADER_SZ] = update_not_requested; + ExpectIntEQ(wolfSSL_provide_quic_data(tserver.ssl, + wolfssl_encryption_application, lbuffer, len), WOLFSSL_SUCCESS); + ret = wolfSSL_process_quic_post_handshake(tserver.ssl); + ExpectIntEQ(ret, WC_NO_ERR_TRACE(SANITY_MSG_E)); + + QuicTestContext_free(&tclient); + QuicTestContext_free(&tserver); + + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + printf(" test_quic_key_update_rejected: %s\n", + EXPECT_RESULT() ? pass : fail); + return EXPECT_RESULT(); +} + /* This has gotten a bit out of hand. */ #if (defined(OPENSSL_ALL) || (defined(OPENSSL_EXTRA) && \ (defined(HAVE_STUNNEL) || defined(WOLFSSL_NGINX) || \ @@ -2005,6 +2055,7 @@ int QuicTest(void) #if !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) if ((ret = test_quic_server_hello(verbose)) != TEST_SUCCESS) goto leave; if ((ret = test_quic_server_hello_fail(verbose)) != TEST_SUCCESS) goto leave; + if ((ret = test_quic_key_update_rejected(verbose)) != TEST_SUCCESS) goto leave; #ifdef REALLY_HAVE_ALPN_AND_SNI if ((ret = test_quic_alpn(verbose)) != TEST_SUCCESS) goto leave; #endif /* REALLY_HAVE_ALPN_AND_SNI */