Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected], Simon McVittie <[email protected]>
Control: affects -1 + src:gnutls28
User: [email protected]
Usertags: pu


Hello,

I would like to fix #1130152 for trixie. Simon has done all the heavy
and not so heavy lifting on this, let's quote #1130152:

8X-------------------
A regression in GnuTLS 3.8.5, which started shuffling the extensions
order, causes an interoperability issue leading to handshake failures
with some SSL/TLS servers. I'm reporting this at important severity since
it's an interop regression affecting an unknown number of remote services.

From the linked regression report https://github.com/luakit/luakit/issues/1101,
it seems that at the time of writing, search.dismail.de is a good test-case,
for example:
[...]
    # gnutls-cli search.dismail.de
    Processed 150 CA certificate(s).
    Resolving 'search.dismail.de:443'...
    Connecting to '128.140.68.142:443'...
    *** Fatal error: A TLS fatal alert has been received.
    *** Received alert [47]: Illegal parameter
[...]
I've confirmed that 3.8.12-2 in forky and 3.7.9-2+deb12u6 in bookworm
are both unaffected by this: they successfully connect to that server,
with gnutls-cli output that includes "Handshake was completed". (Press
Ctrl+D to exit after seeing this.)

This appears to have been fixed by
https://gitlab.com/gnutls/gnutls/-/merge_requests/1930
after the 3.8.9 release, commit
[...]
8X-------------------


I have verified the proposed change and that it fixes the issue.

TIA, cu Andreas

-- 
"You people are noisy," Nia said.
I made the gesture of agreement.
diff --git a/debian/changelog b/debian/changelog
index ee6981c..867d2f8 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,15 @@
+gnutls28 (3.8.9-3+deb13u3) trixie; urgency=medium
+
+  [ Simon McVittie ]
+  * d/p/51_handshake-only-shuffle-extensions-in-the-first-Client-Hel.patch:
+    Preserve extension order across client Hello retry.
+    This resolves an interop regression in 3.8.5 with servers that enforce
+    the RFC requirement that the Client Hello after a Hello Retry Request
+    has the same extensions as the original Client Hello, in the same order
+    (Closes: #1130152)
+
+ -- Andreas Metzler <[email protected]>  Sat, 14 Mar 2026 07:19:14 +0100
+
 gnutls28 (3.8.9-3+deb13u2) trixie-security; urgency=high
 
   * libgnutls: Fix name constraint processing performance issue
diff --git a/debian/patches/51_handshake-only-shuffle-extensions-in-the-first-Client-Hel.patch b/debian/patches/51_handshake-only-shuffle-extensions-in-the-first-Client-Hel.patch
new file mode 100644
index 0000000..dd48dfa
--- /dev/null
+++ b/debian/patches/51_handshake-only-shuffle-extensions-in-the-first-Client-Hel.patch
@@ -0,0 +1,213 @@
+From: Daiki Ueno <[email protected]>
+Date: Sun, 9 Feb 2025 10:31:20 +0900
+Subject: handshake: only shuffle extensions in the first Client Hello
+
+RFC 8446 section 4.1.2 states that the second Client Hello after HRR
+should preserve the same content as the first Client Hello with
+limited exceptions.  Since GnuTLS 3.8.5, however, the library started
+shuffling the order of extensions for privacy reasons and that didn't
+comply with the RFC, leading to a connectivity issue against the
+server configuration with a stricter check on that.
+
+Signed-off-by: Daiki Ueno <[email protected]>
+Origin: upstream, 3.8.10, commit:dc5ee80c3a28577e9de0f82fb08164e4c02b96af
+Bug: https://gitlab.com/gnutls/gnutls/-/work_items/1660
+Bug-Debian: https://bugs.debian.org/1130152
+Bug-SteamRT: steamrt/tasks#938
+---
+ lib/gnutls_int.h                  |  4 +++
+ lib/hello_ext.c                   | 41 +++++++++++++++++++------------
+ lib/state.c                       |  2 ++
+ tests/tls13/hello_retry_request.c | 51 ++++++++++++++++++++++++++++++++++++---
+ 4 files changed, 79 insertions(+), 19 deletions(-)
+
+diff --git a/lib/gnutls_int.h b/lib/gnutls_int.h
+index d10a028..572de5a 100644
+--- a/lib/gnutls_int.h
++++ b/lib/gnutls_int.h
+@@ -1666,6 +1666,10 @@ typedef struct {
+ 	/* Compression method for certificate compression */
+ 	gnutls_compression_method_t compress_certificate_method;
+ 
++	/* To shuffle extension sending order */
++	extensions_t client_hello_exts[MAX_EXT_TYPES];
++	bool client_hello_exts_set;
++
+ 	/* If you add anything here, check _gnutls_handshake_internal_state_clear().
+ 	 */
+ } internals_st;
+diff --git a/lib/hello_ext.c b/lib/hello_ext.c
+index 45237b8..94e6d86 100644
+--- a/lib/hello_ext.c
++++ b/lib/hello_ext.c
+@@ -438,8 +438,6 @@ int _gnutls_gen_hello_extensions(gnutls_session_t session,
+ 	int pos, ret;
+ 	size_t i;
+ 	hello_ext_ctx_st ctx;
+-	/* To shuffle extension sending order */
+-	extensions_t indices[MAX_EXT_TYPES];
+ 
+ 	msg &= GNUTLS_EXT_FLAG_SET_ONLY_FLAGS_MASK;
+ 
+@@ -469,26 +467,39 @@ int _gnutls_gen_hello_extensions(gnutls_session_t session,
+ 				ret - 4);
+ 	}
+ 
+-	/* Initializing extensions array */
+-	for (i = 0; i < MAX_EXT_TYPES; i++) {
+-		indices[i] = i;
+-	}
++	if (msg & GNUTLS_EXT_FLAG_CLIENT_HELLO &&
++	    !session->internals.client_hello_exts_set) {
++		/* Initializing extensions array */
++		for (i = 0; i < MAX_EXT_TYPES; i++) {
++			session->internals.client_hello_exts[i] = i;
++		}
+ 
+-	if (!session->internals.priorities->no_shuffle_extensions) {
+-		/* Ordering padding and pre_shared_key as last extensions */
+-		swap_exts(indices, MAX_EXT_TYPES - 2, GNUTLS_EXTENSION_DUMBFW);
+-		swap_exts(indices, MAX_EXT_TYPES - 1,
+-			  GNUTLS_EXTENSION_PRE_SHARED_KEY);
++		if (!session->internals.priorities->no_shuffle_extensions) {
++			/* Ordering padding and pre_shared_key as last extensions */
++			swap_exts(session->internals.client_hello_exts,
++				  MAX_EXT_TYPES - 2, GNUTLS_EXTENSION_DUMBFW);
++			swap_exts(session->internals.client_hello_exts,
++				  MAX_EXT_TYPES - 1,
++				  GNUTLS_EXTENSION_PRE_SHARED_KEY);
+ 
+-		ret = shuffle_exts(indices, MAX_EXT_TYPES - 2);
+-		if (ret < 0)
+-			return gnutls_assert_val(ret);
++			ret = shuffle_exts(session->internals.client_hello_exts,
++					   MAX_EXT_TYPES - 2);
++			if (ret < 0)
++				return gnutls_assert_val(ret);
++		}
++		session->internals.client_hello_exts_set = true;
+ 	}
+ 
+ 	/* hello_ext_send() ensures we don't send duplicates, in case
+ 	 * of overridden extensions */
+ 	for (i = 0; i < MAX_EXT_TYPES; i++) {
+-		size_t ii = indices[i];
++		size_t ii;
++
++		if (msg & GNUTLS_EXT_FLAG_CLIENT_HELLO)
++			ii = session->internals.client_hello_exts[i];
++		else
++			ii = i;
++
+ 		if (!extfunc[ii])
+ 			continue;
+ 
+diff --git a/lib/state.c b/lib/state.c
+index 020f212..8090686 100644
+--- a/lib/state.c
++++ b/lib/state.c
+@@ -518,6 +518,8 @@ static void handshake_internal_state_clear1(gnutls_session_t session)
+ 
+ 	session->internals.hrr_cs[0] = CS_INVALID_MAJOR;
+ 	session->internals.hrr_cs[1] = CS_INVALID_MINOR;
++
++	session->internals.client_hello_exts_set = false;
+ }
+ 
+ /* This function will clear all the variables in internals
+diff --git a/tests/tls13/hello_retry_request.c b/tests/tls13/hello_retry_request.c
+index f407b64..6c5f698 100644
+--- a/tests/tls13/hello_retry_request.c
++++ b/tests/tls13/hello_retry_request.c
+@@ -51,14 +51,37 @@ static void tls_log_func(int level, const char *str)
+ }
+ 
+ #define HANDSHAKE_SESSION_ID_POS 34
++#define MAX_EXT_TYPES 64
+ 
+ struct ctx_st {
+ 	unsigned hrr_seen;
+ 	unsigned hello_counter;
+ 	uint8_t session_id[32];
+ 	size_t session_id_len;
++	unsigned extensions[MAX_EXT_TYPES];
++	size_t extensions_size1;
++	size_t extensions_size2;
+ };
+ 
++static int ext_callback(void *_ctx, unsigned tls_id, const unsigned char *data,
++			unsigned size)
++{
++	struct ctx_st *ctx = _ctx;
++	if (ctx->hello_counter == 0) {
++		assert(ctx->extensions_size1 < MAX_EXT_TYPES);
++		ctx->extensions[ctx->extensions_size1++] = tls_id;
++	} else {
++		assert(ctx->extensions_size2 < MAX_EXT_TYPES);
++		if (tls_id != ctx->extensions[ctx->extensions_size2]) {
++			fail("extension doesn't match at position %zu, %u != %u\n",
++			     ctx->extensions_size2, tls_id,
++			     ctx->extensions[ctx->extensions_size2]);
++		}
++		ctx->extensions_size2++;
++	}
++	return 0;
++}
++
+ static int hello_callback(gnutls_session_t session, unsigned int htype,
+ 			  unsigned post, unsigned int incoming,
+ 			  const gnutls_datum_t *msg)
+@@ -73,15 +96,25 @@ static int hello_callback(gnutls_session_t session, unsigned int htype,
+ 	    post == GNUTLS_HOOK_POST) {
+ 		size_t session_id_len;
+ 		uint8_t *session_id;
++		unsigned pos = HANDSHAKE_SESSION_ID_POS;
++		gnutls_datum_t mmsg;
++		int ret;
+ 
+-		assert(msg->size > HANDSHAKE_SESSION_ID_POS + 1);
+-		session_id_len = msg->data[HANDSHAKE_SESSION_ID_POS];
+-		session_id = &msg->data[HANDSHAKE_SESSION_ID_POS + 1];
++		assert(msg->size > pos + 1);
++		session_id_len = msg->data[pos];
++		session_id = &msg->data[pos + 1];
++
++		SKIP8(pos, msg->size);
++		SKIP16(pos, msg->size);
++		SKIP8(pos, msg->size);
++
++		mmsg.data = &msg->data[pos];
++		mmsg.size = msg->size - pos;
+ 
+ 		if (ctx->hello_counter > 0) {
+ 			assert(msg->size > 4);
+ 			if (msg->data[0] != 0x03 || msg->data[1] != 0x03) {
+-				fail("version is %d.%d expected 3,3\n",
++				fail("version is %d.%d expected 3.3\n",
+ 				     (int)msg->data[0], (int)msg->data[1]);
+ 			}
+ 
+@@ -95,6 +128,12 @@ static int hello_callback(gnutls_session_t session, unsigned int htype,
+ 		ctx->session_id_len = session_id_len;
+ 		memcpy(ctx->session_id, session_id, session_id_len);
+ 
++		ret = gnutls_ext_raw_parse(ctx, ext_callback, &mmsg, 0);
++		if (ret < 0) {
++			fail("unable to parse extensions: %s\n",
++			     gnutls_strerror(ret));
++		}
++
+ 		ctx->hello_counter++;
+ 	}
+ 
+@@ -164,6 +203,10 @@ void doit(void)
+ 		myfail("group doesn't match the expected: %s\n",
+ 		       gnutls_group_get_name(gnutls_group_get(server)));
+ 
++	if (ctx.extensions_size1 != ctx.extensions_size2)
++		myfail("the number of extensions don't match in second Client Hello: %zu != %zu\n",
++		       ctx.extensions_size1, ctx.extensions_size2);
++
+ 	gnutls_bye(client, GNUTLS_SHUT_WR);
+ 	gnutls_bye(server, GNUTLS_SHUT_WR);
+ 
diff --git a/debian/patches/series b/debian/patches/series
index 8270734..6d55279 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -21,3 +21,4 @@
 50_0007-x509-name_constraints-implement-name_constraints_nod.patch
 50_0008-x509-name_constraints-make-types_with_empty_intersec.patch
 50_0009-x509-name_constraints-name_constraints_node_list_int.patch
+51_handshake-only-shuffle-extensions-in-the-first-Client-Hel.patch

Attachment: signature.asc
Description: PGP signature

Reply via email to