Commit 54aef373f3 for openssl.org
commit 54aef373f3265a6b3f2d757a2f24473381157dea
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date: Wed Jun 17 20:35:05 2026 +0900
Reject HelloRequest in TLS 1.3
TLS 1.3 reserves handshake message type 0 and must not silently
ignore HelloRequest records. The legacy client-side HelloRequest skip
path in tls_get_message_header() could run before the TLS 1.3 state
machine had a chance to reject the message, so a zero-length
HelloRequest injected after ClientHello was discarded instead of
triggering unexpected_message.
Restrict the skip to cases where TLS 1.3 is no longer possible.
Before ServerHello selects a version, s->version is the configured
maximum; after ServerHello or during renegotiation, it is the
negotiated version. Skip only when that value is below TLS 1.3,
preserving the existing TLS 1.2-and-below behavior.
Add TLSProxy regression tests covering rejection while TLS 1.3 is
possible and the preserved TLS 1.2 skip after ServerHello.
Fixes #31531
Reviewed-by: Bob Beck <beck@openssl.org>
Reviewed-by: Norbert Pocs <norbertp@openssl.org>
Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
MergeDate: Wed Jun 24 13:01:12 2026
(Merged from https://github.com/openssl/openssl/pull/31577)
diff --git a/ssl/statem/statem_lib.c b/ssl/statem/statem_lib.c
index bacbd58218..c9d76fe8a7 100644
--- a/ssl/statem/statem_lib.c
+++ b/ssl/statem/statem_lib.c
@@ -1535,6 +1535,23 @@ WORK_STATE tls_finish_handshake(SSL_CONNECTION *s, ossl_unused WORK_STATE wst,
return WORK_FINISHED_STOP;
}
+/*
+ * TLS 1.3 reserves handshake message type 0, so a HelloRequest must reach the
+ * state machine and be rejected there whenever TLS 1.3 is still possible.
+ *
+ * By the time a client reads a server handshake message, s->version is either
+ * the configured maximum for an initial pre-ServerHello handshake, or the
+ * already negotiated version after ServerHello or during renegotiation. Skip
+ * only when that version is below TLS 1.3.
+ */
+static int should_skip_hello_request(const SSL_CONNECTION *s)
+{
+ if (SSL_CONNECTION_IS_TLS13(s))
+ return 0;
+
+ return s->version > 0 && s->version < TLS1_3_VERSION;
+}
+
int tls_get_message_header(SSL_CONNECTION *s, int *mt)
{
/* s->init_num < SSL3_HM_HEADER_LENGTH */
@@ -1594,7 +1611,8 @@ int tls_get_message_header(SSL_CONNECTION *s, int *mt)
skip_message = 0;
if (!s->server)
if (s->statem.hand_state != TLS_ST_OK
- && p[0] == SSL3_MT_HELLO_REQUEST)
+ && p[0] == SSL3_MT_HELLO_REQUEST
+ && should_skip_hello_request(s))
/*
* The server may always send 'Hello Request' messages --
* we are doing a handshake anyway now, so ignore them if
diff --git a/test/recipes/70-test_tls13messages.t b/test/recipes/70-test_tls13messages.t
index f3a3f4789f..b2e356763c 100644
--- a/test/recipes/70-test_tls13messages.t
+++ b/test/recipes/70-test_tls13messages.t
@@ -211,6 +211,9 @@ my $proxy = TLSProxy::Proxy->new(
(!$ENV{HARNESS_ACTIVE} || $ENV{HARNESS_VERBOSE}),
have_IPv6()
);
+my $fatal_alert = 0;
+my $hello_request_added = 0;
+my $hello_request_after_server_hello = 0;
#Test 1: Check we get all the right messages for a default handshake
(undef, my $session) = tempfile();
@@ -219,7 +222,7 @@ $proxy->cipherc("DEFAULT:\@SECLEVEL=2");
$proxy->clientflags("-no_rx_cert_comp -sess_out ".$session);
$proxy->sessionfile($session);
$proxy->start() or plan skip_all => "Unable to start up Proxy for tests";
-plan tests => 17;
+plan tests => 19;
checkhandshake($proxy, checkhandshake::DEFAULT_HANDSHAKE,
checkhandshake::DEFAULT_EXTENSIONS,
"Default handshake test");
@@ -419,4 +422,90 @@ checkhandshake($proxy, checkhandshake::DEFAULT_HANDSHAKE,
| checkhandshake::SUPPORTED_GROUPS_SRV_EXTENSION,
"Acceptable but non preferred key_share");
+#Test 18: HelloRequest is reserved in TLSv1.3
+$proxy->clear();
+$fatal_alert = 0;
+$hello_request_added = 0;
+$hello_request_after_server_hello = 0;
+$proxy->filter(\&inject_hello_request);
+$proxy->cipherc("DEFAULT:\@SECLEVEL=2");
+$proxy->clientflags("-no_rx_cert_comp");
+$proxy->start();
+ok($fatal_alert, "HelloRequest rejected in TLSv1.3");
+
+#Test 19: A HelloRequest received after selecting TLSv1.2 in the initial
+# handshake is still ignored, confirming the legacy skip path is
+# preserved even when TLSv1.3 was initially enabled.
+SKIP: {
+ skip "TLSv1.2 disabled", 1 if disabled("tls1_2");
+
+ $proxy->clear();
+ $fatal_alert = 0;
+ $hello_request_added = 0;
+ $hello_request_after_server_hello = 1;
+ $proxy->filter(\&inject_hello_request);
+ $proxy->cipherc("DEFAULT:\@SECLEVEL=2");
+ $proxy->clientflags("-no_rx_cert_comp");
+ $proxy->serverflags("-no_tls1_3");
+ $proxy->start();
+ ok(TLSProxy::Message->success() && !$fatal_alert,
+ "HelloRequest ignored in TLSv1.2");
+}
+
unlink $session;
+
+sub inject_hello_request
+{
+ my $proxy = shift;
+ my $records = $proxy->record_list;
+ my $hello_request;
+ my $record;
+ my $server_hello_record;
+ my $i;
+
+ if ($hello_request_added) {
+ $fatal_alert = 1
+ if @{$records}[-1]->is_fatal_alert(0)
+ == TLSProxy::Message::AL_DESC_UNEXPECTED_MESSAGE;
+ return;
+ }
+
+ return if $proxy->flight != 1;
+
+ $hello_request = pack("C4", TLSProxy::Message::MT_HELLO_REQUEST,
+ 0, 0, 0);
+ $record = TLSProxy::Record->new(
+ 1,
+ TLSProxy::Record::RT_HANDSHAKE,
+ TLSProxy::Record::VERS_TLS_1_2,
+ length($hello_request),
+ length($hello_request),
+ length($hello_request),
+ $hello_request,
+ $hello_request
+ );
+
+ if ($hello_request_after_server_hello) {
+ foreach my $message (@{$proxy->message_list}) {
+ next if $message->mt != TLSProxy::Message::MT_SERVER_HELLO
+ || ${$message->records}[0]->flight != 1;
+
+ $server_hello_record = @{$message->records}[-1];
+ last;
+ }
+
+ return if !defined $server_hello_record;
+
+ for ($i = 0; $i < @{$records}; $i++) {
+ last if ${$records}[$i] == $server_hello_record;
+ }
+ $i++;
+ } else {
+ for ($i = 0; ${$records}[$i]->flight() < 1; $i++) {
+ next;
+ }
+ }
+
+ splice @{$records}, $i, 0, $record;
+ $hello_request_added = 1;
+}