Commit 53fd1bd29a for openssl.org
commit 53fd1bd29aa216f89728098d6afcb87c7dfd4a24
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date: Fri Jun 19 20:25:13 2026 +0900
Reject HelloRequest in TLS and DTLS 1.3
TLS and DTLS 1.3 reserve handshake message type 0, but legacy
client-side HelloRequest skip paths could consume such messages before
the state machines rejected them.
Keep the skip paths only for connections that cannot use TLS or DTLS
1.3. Add TLSProxy coverage for TLS and DTLS 1.3 rejection, and for the
preserved TLS and DTLS 1.2 legacy behavior. The DTLS test insertion
also adjusts record sequencing so the injected HelloRequest is a valid
standalone server record.
Reviewed-by: Andrew Dinh <andrewd@openssl.org>
Reviewed-by: Matt Caswell <matt@openssl.foundation>
Merge-date: Mon Aug 17 08:32:42 2026
Merged-from: https://github.com/openssl/openssl/pull/31637
diff --git a/ssl/statem/statem_dtls.c b/ssl/statem/statem_dtls.c
index f8853611b2..c63213c5d8 100644
--- a/ssl/statem/statem_dtls.c
+++ b/ssl/statem/statem_dtls.c
@@ -925,6 +925,23 @@ static int dtls1_read_hm_header(unsigned char *msgheaderstart,
return 1;
}
+/*
+ * DTLS 1.3 reserves handshake message type 0, so a HelloRequest must reach the
+ * state machine and be rejected there whenever DTLS 1.3 is still possible.
+ *
+ * s->version is the negotiated, or maximum version: before ServerHello this is
+ * the effective maximum, and after ServerHello or during renegotiation it is the
+ * selected version.
+ */
+static int dtls_should_skip_hello_request(const SSL_CONNECTION *s)
+{
+ if (SSL_CONNECTION_IS_DTLS13(s))
+ return 0;
+
+ return s->version > 0
+ && ssl_version_cmp(s, s->version, DTLS1_3_VERSION) < 0;
+}
+
static int dtls_get_reassembled_message(SSL_CONNECTION *s, int *errtype,
size_t *len)
{
@@ -1087,19 +1104,13 @@ redo:
if (!s->server
&& s->statem.hand_state != TLS_ST_OK
- && msg_hdr.type == SSL3_MT_HELLO_REQUEST) {
+ && msg_hdr.type == SSL3_MT_HELLO_REQUEST
+ && dtls_should_skip_hello_request(s)) {
/*
- * HelloRequest is reserved in DTLS 1.3 (like TLS 1.3). For earlier
- * versions, the server may send 'Hello Request' messages -- we are
+ * The server may always send 'Hello Request' messages -- we are
* doing a handshake anyway now, so ignore them if their format is
* correct. Does not count for 'Finished' MAC.
*/
- if (SSL_CONNECTION_IS_DTLS13(s)) {
- /* DTLS 1.3 reserves message type 0, reject HelloRequest */
- SSLfatal(s, SSL_AD_UNEXPECTED_MESSAGE, SSL_R_UNEXPECTED_MESSAGE);
- goto f_err;
- }
-
if (msg_hdr.msg_len == 0) {
if (s->msg_callback)
s->msg_callback(0, s->version, SSL3_RT_HANDSHAKE,
diff --git a/test/recipes/70-test_tls13messages.t b/test/recipes/70-test_tls13messages.t
index c0526bdbb4..df47080eb8 100644
--- a/test/recipes/70-test_tls13messages.t
+++ b/test/recipes/70-test_tls13messages.t
@@ -208,11 +208,12 @@ sub setup_extensions
$ENV{OPENSSL_MODULES} = abs_path(bldtop_dir("test"));
+my $testcount = 19;
my $fatal_alert = 0;
my $hello_request_added = 0;
my $hello_request_after_server_hello = 0;
-
-my $testcount = 19;
+my $hello_request_record_epoch = -1;
+my $hello_request_record_seq = -1;
plan tests => 2 * $testcount;
@@ -470,39 +471,54 @@ sub run_tests
| checkhandshake::SUPPORTED_GROUPS_SRV_EXTENSION,
"Acceptable but non preferred key_share");
- SKIP: {
- skip "TLSProxy does not support partial messages for dtls", 1
- if $run_test_as_dtls == 1;
- #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");
+ #Test 18: HelloRequest is reserved in (D)TLSv1.3
+ $proxy->clear();
+ $fatal_alert = 0;
+ $hello_request_added = 0;
+ $hello_request_after_server_hello = 0;
+ $hello_request_record_epoch = -1;
+ $hello_request_record_seq = -1;
+ $proxy->filter(\&inject_hello_request);
+ $proxy->cipherc("DEFAULT:\@SECLEVEL=2");
+ if ($run_test_as_dtls) {
+ $proxy->clientflags("-no_rx_cert_comp -mtu 16384");
+ $proxy->serverflags("-timeout -mtu 16384");
+ } else {
$proxy->clientflags("-no_rx_cert_comp");
- $proxy->start();
- ok($fatal_alert, "HelloRequest rejected in TLSv1.3");
}
+ $proxy->start();
+ ok($fatal_alert, "HelloRequest rejected in "
+ . ($run_test_as_dtls ? "DTLSv1.3" : "TLSv1.3"));
- #Test 19: A HelloRequest received after selecting TLSv1.2 in the initial
+ #Test 19: A HelloRequest received after selecting (D)TLSv1.2 in the initial
# handshake is still ignored, confirming the legacy skip path is
- # preserved even when TLSv1.3 was initially enabled.
+ # preserved even when (D)TLSv1.3 was initially enabled.
SKIP: {
- skip "TLSv1.2 disabled", 1 if disabled("tls1_2");
- skip "TLSProxy does not support partial messages for dtls", 1
- if $run_test_as_dtls == 1;
+ my $legacy_version = $run_test_as_dtls ? "DTLSv1.2" : "TLSv1.2";
+ my $legacy_version_disabled = $run_test_as_dtls
+ ? disabled("dtls1_2")
+ : disabled("tls1_2");
+
+ skip "$legacy_version disabled", 1 if $legacy_version_disabled;
+
$proxy->clear();
$fatal_alert = 0;
$hello_request_added = 0;
$hello_request_after_server_hello = 1;
+ $hello_request_record_epoch = -1;
+ $hello_request_record_seq = -1;
$proxy->filter(\&inject_hello_request);
$proxy->cipherc("DEFAULT:\@SECLEVEL=2");
- $proxy->clientflags("-no_rx_cert_comp");
- $proxy->serverflags("-no_tls1_3");
+ if ($run_test_as_dtls) {
+ $proxy->clientflags("-no_rx_cert_comp -mtu 16384");
+ $proxy->serverflags("-max_protocol DTLSv1.2 -mtu 16384");
+ } else {
+ $proxy->clientflags("-no_rx_cert_comp");
+ $proxy->serverflags("-no_tls1_3");
+ }
$proxy->start();
ok(TLSProxy::Message->success() && !$fatal_alert,
- "HelloRequest ignored in TLSv1.2");
+ "HelloRequest ignored in $legacy_version");
}
unlink $session;
@@ -514,37 +530,95 @@ sub inject_hello_request
my $records = $proxy->record_list;
my $hello_request;
my $record;
+ my $server_hello;
my $server_hello_record;
+ my $record_epoch;
+ my $record_seq;
+ my $record_version;
+ my $target_message;
+ my $target_record;
+ my $msgseq;
my $i;
if ($hello_request_added) {
- $fatal_alert = 1
- if @{$records}[-1]->is_fatal_alert(0)
- == TLSProxy::Message::AL_DESC_UNEXPECTED_MESSAGE;
+ if ($proxy->isdtls()) {
+ foreach my $existing_record (@{$records}) {
+ next if $existing_record->{sent};
+ next if !$existing_record->serverissender;
+ next if $existing_record->epoch != $hello_request_record_epoch;
+ next if $existing_record->seq < $hello_request_record_seq;
+
+ $existing_record->seq($existing_record->seq + 1);
+ }
+
+ foreach my $existing_record (reverse @{$records}) {
+ if ($existing_record->is_fatal_alert(0)
+ == TLSProxy::Message::AL_DESC_UNEXPECTED_MESSAGE) {
+ $fatal_alert = 1;
+ last;
+ }
+ }
+ } elsif (@{$records}[-1]->is_fatal_alert(0)
+ == TLSProxy::Message::AL_DESC_UNEXPECTED_MESSAGE) {
+ $fatal_alert = 1;
+ }
return;
}
- return if $proxy->flight != 1;
+ if (!$proxy->isdtls()) {
+ return if $proxy->flight != 1;
+
+ $hello_request = pack("C4", TLSProxy::Message::MT_HELLO_REQUEST,
+ 0, 0, 0);
+ $record = TLSProxy::Record->new(
+ 1,
+ $proxy->flight,
+ TLSProxy::Record::RT_HANDSHAKE,
+ TLSProxy::Record::VERS_TLS_1_2,
+ length($hello_request),
+ length($hello_request),
+ length($hello_request),
+ $hello_request,
+ $hello_request
+ );
- $hello_request = pack("C4", TLSProxy::Message::MT_HELLO_REQUEST,
- 0, 0, 0);
- $record = TLSProxy::Record->new(
- 1,
- 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;
+ return;
+ }
+
+ # Insert a standalone server record into an existing DTLS flight, bumping
+ # later same-epoch record sequence numbers while preserving handshake message
+ # sequences for the expected DTLSv1.3 reject and DTLSv1.2 skip paths.
if ($hello_request_after_server_hello) {
+ return if ($proxy->flight & 1) == 0;
+
foreach my $message (@{$proxy->message_list}) {
next if $message->mt != TLSProxy::Message::MT_SERVER_HELLO
- || ${$message->records}[0]->flight != 1;
+ || !$message->server;
+ $server_hello = $message;
$server_hello_record = @{$message->records}[-1];
last;
}
@@ -555,12 +629,63 @@ sub inject_hello_request
last if ${$records}[$i] == $server_hello_record;
}
$i++;
+ $record_epoch = $server_hello_record->epoch;
+ $record_seq = $server_hello_record->seq + 1;
+ $record_version = $server_hello_record->version;
+ $msgseq = $server_hello->msgseq + 1;
} else {
- for ($i = 0; ${$records}[$i]->flight() < 1; $i++) {
- next;
+ return if $proxy->flight != 1;
+
+ foreach my $message (@{$proxy->message_list}) {
+ next if !$message->server
+ || ${$message->records}[0]->flight != $proxy->flight;
+
+ $target_message = $message;
+ $target_record = ${$message->records}[0];
+ last;
+ }
+
+ return if !defined $target_record;
+
+ for ($i = 0; $i < @{$records}; $i++) {
+ last if ${$records}[$i] == $target_record;
}
+
+ $record_epoch = $target_record->epoch;
+ $record_seq = $target_record->seq;
+ $record_version = $target_record->version;
+ $msgseq = $target_message->msgseq;
+ }
+
+ foreach my $existing_record (@{$records}) {
+ next if !$existing_record->serverissender;
+ next if $existing_record->epoch != $record_epoch;
+ next if $existing_record->seq < $record_seq;
+
+ $existing_record->seq($existing_record->seq + 1);
}
+ $hello_request = pack("C", TLSProxy::Message::MT_HELLO_REQUEST)
+ . pack("C3", 0, 0, 0)
+ . pack("n", $msgseq)
+ . pack("C3", 0, 0, 0)
+ . pack("C3", 0, 0, 0);
+ $record = TLSProxy::Record->new_dtls(
+ 1,
+ $proxy->flight,
+ TLSProxy::Record::RT_HANDSHAKE,
+ $record_version,
+ $record_epoch,
+ $record_seq,
+ length($hello_request),
+ length($hello_request),
+ length($hello_request),
+ $hello_request,
+ $hello_request
+ );
+
splice @{$records}, $i, 0, $record;
$hello_request_added = 1;
+ $hello_request_record_epoch = $record_epoch;
+ $hello_request_record_seq = $record_seq;
}