Hi,

as discussed on IRC here are the proposed updates for the CVE-2025-13086
fix in both Trixie and Bookworm.

For both versions it's a simple cherry-pick of two upstream commits
(order reversed here for explaination)

CVE-2025-13086.patch
https://github.com/OpenVPN/openvpn/commit/fa6a1824b0f37bff137204156a74ca28cf5b6f83

This is the actual CVE patch released with 2.6.16. It does NOT apply
cleanly to both versions in Bookworm and Trixie. I'm absolutely not
feeling confident to massage crypto related code into submission, but
fortunately all rejects are caused by another codefix that can be
cherry-picked before


check-message-id.patch
https://github.com/OpenVPN/openvpn/commit/68c01720eecc1772b3f648b9e043e396d943f632

This one fixes an annoying regression in all of 2.6 where a floating
client was not properly handled. It happens only in special
configurations and has been unreported a long time, but it happens.
This fix has only been released in 2.6.15, but it's worth noting that
the reporter made Ubuntu to release that patch into both Ubuntu 24.04
LTS and 25.04 two months ago

https://launchpad.net/ubuntu/+source/openvpn/2.6.14-0ubuntu0.24.04.2
https://launchpad.net/ubuntu/+source/openvpn/2.6.14-0ubuntu0.25.04.2
https://bugs.launchpad.net/ubuntu/+source/openvpn/+bug/2108860


The trixie diff also imports another fix for a FTBFS on newer upstream
kernels. I had queued that up before for a trixie-pu so it happens to be
included. I can revert it if you like, but it's easy enough (header
changes in 6.16+ caused by the import of the official ovpn module)

https://github.com/OpenVPN/openvpn/commit/1fbbe91d292fb925f5af73b512d7d1c83abfe714


As discussed, the bookworm update also includes changes filed for a
bookworm-pu update in
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1112645 . It includes
a regression fix for the earlier CVE-2024-5594 security update,
cherry-picked from upstream in 
https://github.com/OpenVPN/openvpn/commit/343573990135023d855d151fcd9248e5c26d9f8b
and released in 2.6.12 in July 2024 and would close Bug#1112516 . 
The regression did not cause vicious complaints so I asked for it to be
handled in a bookworm-pu and be kept there for a bit, however the pu was
not handled yet by the SRM.

The rest of the changes only affect autopkgtest and salsa-ci, they just
increase test coverage which is always a good thing to have.

The resulting binary builds fine, the testsuite runs and it has been
slightly tested. However I would like to test it on a few more
production systems before I finally upload.

Bernhard
diffstat for openvpn-2.6.14 openvpn-2.6.14

 changelog                           |   14 +
 gbp.conf                            |    1 
 patches/CVE-2025-13086.patch        |  155 +++++++++++++++++
 patches/check-message-id.patch      |  312 ++++++++++++++++++++++++++++++++++++
 patches/fix-ftbfs-kernel-6.16.patch |   78 +++++++++
 patches/series                      |    3 
 6 files changed, 563 insertions(+)

diff -Nru openvpn-2.6.14/debian/changelog openvpn-2.6.14/debian/changelog
--- openvpn-2.6.14/debian/changelog	2025-04-02 20:56:44.000000000 +0200
+++ openvpn-2.6.14/debian/changelog	2025-11-21 00:45:17.000000000 +0100
@@ -1,3 +1,17 @@
+openvpn (2.6.14-1+deb13u1) trixie-security; urgency=medium
+
+  * Cherry-pick patches for CVE-2025-13086
+    - check-message-id.patch: Check message id/acked ids too when doing
+      sessionid cookie checks - bugfix for floating client problem, code
+      prequesite for the CVE patch to apply
+    - CVE-2025-13086.patch: Fix memcmp check for the hmac verification in the
+      3way handshake being inverted (Closes: #1121086)
+  * fix-ftbfs-kernel-6.16.patch: Fix compilation against 6.16+ kernel
+    headers (Closes: #1114249)
+  * d/gbp.conf: set debian-branch for trixie
+
+ -- Bernhard Schmidt <[email protected]>  Fri, 21 Nov 2025 00:45:17 +0100
+
 openvpn (2.6.14-1) unstable; urgency=medium
 
   [ Aquila Macedo ]
diff -Nru openvpn-2.6.14/debian/gbp.conf openvpn-2.6.14/debian/gbp.conf
--- openvpn-2.6.14/debian/gbp.conf	2025-04-02 20:56:44.000000000 +0200
+++ openvpn-2.6.14/debian/gbp.conf	2025-11-21 00:45:17.000000000 +0100
@@ -1,2 +1,3 @@
 [DEFAULT]
 pristine-tar = True
+debian-branch = debian/trixie
diff -Nru openvpn-2.6.14/debian/patches/check-message-id.patch openvpn-2.6.14/debian/patches/check-message-id.patch
--- openvpn-2.6.14/debian/patches/check-message-id.patch	1970-01-01 01:00:00.000000000 +0100
+++ openvpn-2.6.14/debian/patches/check-message-id.patch	2025-11-21 00:45:17.000000000 +0100
@@ -0,0 +1,312 @@
+From 68c01720eecc1772b3f648b9e043e396d943f632 Mon Sep 17 00:00:00 2001
+From: Arne Schwabe <[email protected]>
+Date: Tue, 16 Sep 2025 17:52:50 +0200
+Subject: [PATCH] Check message id/acked ids too when doing sessionid cookie
+ checks
+
+This fixes that control packets on a floating client can trigger
+creating a new session in special circumstances:
+
+To trigger this circumstance a connection needs to
+
+- starts on IP A
+- successfully floats to IP B by data packet
+- then has a control packet from IP A before any
+  data packet can trigger the float back to IP A
+
+and all of this needs to happen in the 60s time
+that hmac cookie is valid in the default
+configuration.
+
+In this scenario we would trigger a new connection as the HMAC
+session id would be valid.
+
+This patch adds checking also of the message-id and acked ids to
+discern packet from the initial three-way handshake where these
+ids are 0 or 1 from any later packet.
+
+This will now trigger (at verb 4 or higher) a messaged like:
+
+   Packet (P_ACK_V1) with invalid or missing SID
+
+instead.
+
+Also remove a few duplicated free_tls_pre_decrypt_state in test_ssl.
+
+Reported-By: Walter Doekes <[email protected]>
+Tested-By: Walter Doekes <[email protected]>
+
+Change-Id: I6752dcd5aff3e5cea2b439366479e86751a1c403
+Signed-off-by: Arne Schwabe <[email protected]>
+Acked-by: MaxF <[email protected]>
+Gerrit URL: https://gerrit.openvpn.net/c/openvpn/+/1184
+Message-Id: <[email protected]>
+URL: https://www.mail-archive.com/[email protected]/msg32990.html
+Signed-off-by: Gert Doering <[email protected]>
+(backported from commit 518e122b42739b0dbb54e7169a8a3aadb4773125)
+---
+ src/openvpn/mudp.c                  |  6 ++-
+ src/openvpn/ssl_pkt.c               | 39 +++++++++++++--
+ src/openvpn/ssl_pkt.h               | 15 +++---
+ tests/unit_tests/openvpn/test_pkt.c | 77 ++++++++++++++++++++++++++---
+ 4 files changed, 117 insertions(+), 20 deletions(-)
+
+diff --git a/src/openvpn/mudp.c b/src/openvpn/mudp.c
+index cdde0b5c972..0492311669c 100644
+--- a/src/openvpn/mudp.c
++++ b/src/openvpn/mudp.c
+@@ -153,7 +153,8 @@ do_pre_decrypt_check(struct multi_context *m,
+          * need to contain the peer id */
+         struct gc_arena gc = gc_new();
+ 
+-        bool ret = check_session_id_hmac(state, from, hmac, handwindow);
++        bool pkt_is_ack = (verdict == VERDICT_VALID_ACK_V1);
++        bool ret = check_session_hmac_and_pkt_id(state, from, hmac, handwindow, pkt_is_ack);
+ 
+         const char *peer = print_link_socket_actual(&m->top.c2.from, &gc);
+         uint8_t pkt_firstbyte = *BPTR( &m->top.c2.buf);
+@@ -161,7 +162,8 @@ do_pre_decrypt_check(struct multi_context *m,
+ 
+         if (!ret)
+         {
+-            msg(D_MULTI_MEDIUM, "Packet (%s) with invalid or missing SID from %s",
++            msg(D_MULTI_MEDIUM, "Packet (%s) with invalid or missing SID from"
++                " %s or wrong packet id",
+                 packet_opcode_name(op), peer);
+         }
+         else
+diff --git a/src/openvpn/ssl_pkt.c b/src/openvpn/ssl_pkt.c
+index 41299f462db..432bb8b8181 100644
+--- a/src/openvpn/ssl_pkt.c
++++ b/src/openvpn/ssl_pkt.c
+@@ -527,10 +527,11 @@ calculate_session_id_hmac(struct session_id client_sid,
+ }
+ 
+ bool
+-check_session_id_hmac(struct tls_pre_decrypt_state *state,
+-                      const struct openvpn_sockaddr *from,
+-                      hmac_ctx_t *hmac,
+-                      int handwindow)
++check_session_hmac_and_pkt_id(struct tls_pre_decrypt_state *state,
++                              const struct openvpn_sockaddr *from,
++                              hmac_ctx_t *hmac,
++                              int handwindow,
++                              bool pkt_is_ack)
+ {
+     if (!from)
+     {
+@@ -545,6 +546,36 @@ check_session_id_hmac(struct tls_pre_decrypt_state *state,
+         return false;
+     }
+ 
++    /* Check if the packet ID of the packet or ACKED packet  is <= 1 */
++    for (int i = 0; i < ack.len; i++)
++    {
++        /* This packet ACKs a packet that has a higher packet id than the
++         * ones expected in the three-way handshake, consider it as invalid
++         * for the session */
++        if (ack.packet_id[i] > 1)
++        {
++            return false;
++        }
++    }
++
++    if (!pkt_is_ack)
++    {
++        packet_id_type message_id;
++        /* Extract the packet ID from the packet */
++        if (!reliable_ack_read_packet_id(&buf, &message_id))
++        {
++            return false;
++        }
++
++        /* similar check. Anything larger than 1 is not considered part of the
++         * three-way handshake */
++        if (message_id > 1)
++        {
++            return false;
++        }
++    }
++
++
+     /* check adjacent timestamps too */
+     for (int offset = -2; offset <= 1; offset++)
+     {
+diff --git a/src/openvpn/ssl_pkt.h b/src/openvpn/ssl_pkt.h
+index 2033da61ff7..1df691c369a 100644
+--- a/src/openvpn/ssl_pkt.h
++++ b/src/openvpn/ssl_pkt.h
+@@ -182,17 +182,20 @@ calculate_session_id_hmac(struct session_id client_sid,
+ /**
+  * Checks if a control packet has a correct HMAC server session id
+  *
+- * @param client_sid    session id of the client
++ * This will also consider packets that have a packet id higher
++ * than 1 or ack packets higher than 1 to be invalid as they are
++ * not part of the initial three way handshake of OpenVPN and should
++ * not create a new connection.
++ *
++ * @param state         session information
+  * @param from          link_socket from the client
+  * @param hmac          the hmac context to use for the calculation
+  * @param handwindow    the quantisation of the current time
++ * @param pkt_is_ack    the packet being checked is a P_ACK_V1
+  * @return              the expected server session id
+  */
+-bool
+-check_session_id_hmac(struct tls_pre_decrypt_state *state,
+-                      const struct openvpn_sockaddr *from,
+-                      hmac_ctx_t *hmac,
+-                      int handwindow);
++bool check_session_hmac_and_pkt_id(struct tls_pre_decrypt_state *state, const struct openvpn_sockaddr *from,
++                                   hmac_ctx_t *hmac, int handwindow, bool pkt_is_ack);
+ 
+ /*
+  * Write a control channel authentication record.
+diff --git a/tests/unit_tests/openvpn/test_pkt.c b/tests/unit_tests/openvpn/test_pkt.c
+index 74d7311f74d..27f52cf500d 100644
+--- a/tests/unit_tests/openvpn/test_pkt.c
++++ b/tests/unit_tests/openvpn/test_pkt.c
+@@ -174,6 +174,27 @@ const uint8_t client_ack_none_random_id[] = {
+     0x85, 0xdb, 0x53, 0x56, 0x23, 0xb0, 0x2e
+ };
+ 
++/* no tls-auth, P_ACK_V1, acks 0,1, and 2 */
++const uint8_t client_ack_123_none_random_id[] = {
++    0x28,
++    0xae, 0xb9, 0xaf, 0xe1, 0xf0, 0x1d, 0x79, 0xc8,
++    0x03,
++    0x00, 0x00, 0x00, 0x00,
++    0x00, 0x00, 0x00, 0x01,
++    0x00, 0x00, 0x00, 0x02,
++    0xdd, 0x85, 0xdb, 0x53, 0x56, 0x23, 0xb0, 0x2e
++};
++
++/* no tls-auth, P_CONTROL_V1, acks 0, msg-id 2 */
++const uint8_t client_control_none_random_id[] = {
++    0x20,
++    0xae, 0xb9, 0xaf, 0xe1, 0xf0, 0x1d, 0x79, 0xc8,
++    0x01,
++    0x00, 0x00, 0x00, 0x00,
++    0x02
++};
++
++
+ struct tls_auth_standalone
+ init_tas_auth(int key_direction)
+ {
+@@ -294,12 +315,10 @@ test_tls_decrypt_lite_auth(void **ut_state)
+     assert_int_equal(verdict, VERDICT_VALID_RESET_V2);
+     free_tls_pre_decrypt_state(&state);
+ 
+-    free_tls_pre_decrypt_state(&state);
+     /* The pre decrypt function should not modify the buffer, so calling it
+      * again should have the same result */
+     verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
+     assert_int_equal(verdict, VERDICT_VALID_RESET_V2);
+-    free_tls_pre_decrypt_state(&state);
+ 
+     /* and buf memory should be equal */
+     assert_memory_equal(BPTR(&buf), client_reset_v2_tls_auth, sizeof(client_reset_v2_tls_auth));
+@@ -317,7 +336,6 @@ test_tls_decrypt_lite_auth(void **ut_state)
+     assert_int_equal(verdict, VERDICT_INVALID);
+     free_tls_pre_decrypt_state(&state);
+ 
+-    free_tls_pre_decrypt_state(&state);
+     /* Wrong key direction gives a wrong hmac key and should not validate */
+     free_key_ctx_bi(&tas.tls_wrap.opt.key_ctx_bi);
+     free_tas(&tas);
+@@ -357,15 +375,12 @@ test_tls_decrypt_lite_none(void **ut_state)
+     assert_int_equal(verdict, VERDICT_VALID_RESET_V2);
+     free_tls_pre_decrypt_state(&state);
+ 
+-    free_tls_pre_decrypt_state(&state);
+     buf_reset_len(&buf);
+     buf_write(&buf, client_reset_v2_tls_crypt, sizeof(client_reset_v2_none));
+     verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
+     assert_int_equal(verdict, VERDICT_VALID_RESET_V2);
+     free_tls_pre_decrypt_state(&state);
+ 
+-    free_tls_pre_decrypt_state(&state);
+-
+     /* This is not a reset packet and should trigger the other response */
+     buf_reset_len(&buf);
+     buf_write(&buf, client_ack_tls_auth_randomid, sizeof(client_ack_tls_auth_randomid));
+@@ -443,7 +458,7 @@ test_verify_hmac_tls_auth(void **ut_state)
+     assert_int_equal(verdict, VERDICT_VALID_CONTROL_V1);
+ 
+     /* This is a valid packet but containing a random id instead of an HMAC id*/
+-    bool valid = check_session_id_hmac(&state, &from.dest, hmac, 30);
++    bool valid = check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, false);
+     assert_false(valid);
+ 
+     free_tls_pre_decrypt_state(&state);
+@@ -474,7 +489,7 @@ test_verify_hmac_none(void **ut_state)
+     verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
+     assert_int_equal(verdict, VERDICT_VALID_ACK_V1);
+ 
+-    bool valid = check_session_id_hmac(&state, &from.dest, hmac, 30);
++    bool valid = check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true);
+     assert_true(valid);
+ 
+     free_tls_pre_decrypt_state(&state);
+@@ -483,6 +498,51 @@ test_verify_hmac_none(void **ut_state)
+     hmac_ctx_free(hmac);
+ }
+ 
++static void
++test_verify_hmac_none_out_of_range_ack(void **ut_state)
++{
++    hmac_ctx_t *hmac = session_id_hmac_init();
++
++    struct link_socket_actual from = { 0 };
++    from.dest.addr.sa.sa_family = AF_INET;
++
++    struct tls_auth_standalone tas = { 0 };
++    struct tls_pre_decrypt_state state = { 0 };
++
++    struct buffer buf = alloc_buf(1024);
++    enum first_packet_verdict verdict;
++
++    tas.tls_wrap.mode = TLS_WRAP_NONE;
++
++    buf_reset_len(&buf);
++    buf_write(&buf, client_ack_123_none_random_id, sizeof(client_ack_123_none_random_id));
++
++
++    verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
++    assert_int_equal(verdict, VERDICT_VALID_ACK_V1);
++
++    /* should fail because it acks 2 */
++    bool valid = check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true);
++    assert_false(valid);
++    free_tls_pre_decrypt_state(&state);
++
++    /* Try test with the control with a too high message id now */
++    buf_reset_len(&buf);
++    buf_write(&buf, client_control_none_random_id, sizeof(client_control_none_random_id));
++
++    verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
++    assert_int_equal(verdict, VERDICT_VALID_CONTROL_V1);
++
++    /* should fail because it has message id 2 */
++    valid = check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true);
++    assert_false(valid);
++
++    free_tls_pre_decrypt_state(&state);
++    free_buf(&buf);
++    hmac_ctx_cleanup(hmac);
++    hmac_ctx_free(hmac);
++}
++
+ static hmac_ctx_t *
+ init_static_hmac(void)
+ {
+@@ -670,6 +730,7 @@ main(void)
+         cmocka_unit_test(test_calc_session_id_hmac_static),
+         cmocka_unit_test(test_verify_hmac_none),
+         cmocka_unit_test(test_verify_hmac_tls_auth),
++        cmocka_unit_test(test_verify_hmac_none_out_of_range_ack),
+         cmocka_unit_test(test_generate_reset_packet_plain),
+         cmocka_unit_test(test_generate_reset_packet_tls_auth),
+         cmocka_unit_test(test_extract_control_message)
diff -Nru openvpn-2.6.14/debian/patches/CVE-2025-13086.patch openvpn-2.6.14/debian/patches/CVE-2025-13086.patch
--- openvpn-2.6.14/debian/patches/CVE-2025-13086.patch	1970-01-01 01:00:00.000000000 +0100
+++ openvpn-2.6.14/debian/patches/CVE-2025-13086.patch	2025-11-21 00:45:17.000000000 +0100
@@ -0,0 +1,155 @@
+From fa6a1824b0f37bff137204156a74ca28cf5b6f83 Mon Sep 17 00:00:00 2001
+From: Arne Schwabe <[email protected]>
+Date: Mon, 27 Oct 2025 10:05:55 +0100
+Subject: [PATCH] Fix memcmp check for the hmac verification in the 3way
+ handshake being inverted
+
+This is a stupid mistake but causes all hmac cookies to be accepted,
+thus breaking source IP address validation.   As a consequence, TLS
+sessions can be openend and state can be consumed in the server from
+IP addresses that did not initiate an initial connection.
+
+While at it, fix check to only allow [t-2;t] timeslots, disallowing
+HMACs coming in from a future timeslot.
+
+Github: OpenVPN/openvpn-private-issues#56
+
+CVE: 2025-13086
+
+Reported-By: Joshua Rogers <[email protected]>
+Found-by: ZeroPath (https://zeropath.com/)
+Reported-By: [email protected]
+
+Change-Id: I9cbe2bf535575b47ddd7f34e985c5c1c6953a6fc
+Signed-off-by: Arne Schwabe <[email protected]>
+Acked-by: Max Fillinger <[email protected]>
+(cherry picked from commit 68ec931e7fb4af11d5ba0d4283df0350083fd373)
+---
+ src/openvpn/ssl_pkt.c               |  7 ++--
+ tests/unit_tests/openvpn/test_pkt.c | 58 ++++++++++++++++++++++++++++-
+ 2 files changed, 61 insertions(+), 4 deletions(-)
+
+diff --git a/src/openvpn/ssl_pkt.c b/src/openvpn/ssl_pkt.c
+index 432bb8b8181..cf1ce172732 100644
+--- a/src/openvpn/ssl_pkt.c
++++ b/src/openvpn/ssl_pkt.c
+@@ -576,13 +576,14 @@ check_session_hmac_and_pkt_id(struct tls_pre_decrypt_state *state,
+     }
+ 
+ 
+-    /* check adjacent timestamps too */
+-    for (int offset = -2; offset <= 1; offset++)
++    /* check adjacent timestamps too, the handwindow is split in 2 for the
++     * offset, so we check the current timeslot and the two before that */
++    for (int offset = -2; offset <= 0; offset++)
+     {
+         struct session_id expected_id =
+             calculate_session_id_hmac(state->peer_session_id, from, hmac, handwindow, offset);
+ 
+-        if (memcmp_constant_time(&expected_id, &state->server_session_id, SID_SIZE))
++        if (memcmp_constant_time(&expected_id, &state->server_session_id, SID_SIZE) == 0)
+         {
+             return true;
+         }
+diff --git a/tests/unit_tests/openvpn/test_pkt.c b/tests/unit_tests/openvpn/test_pkt.c
+index 27f52cf500d..62abafaac99 100644
+--- a/tests/unit_tests/openvpn/test_pkt.c
++++ b/tests/unit_tests/openvpn/test_pkt.c
+@@ -444,6 +444,8 @@ test_verify_hmac_tls_auth(void **ut_state)
+     hmac_ctx_t *hmac = session_id_hmac_init();
+ 
+     struct link_socket_actual from = { 0 };
++    from.dest.addr.sa.sa_family = AF_INET;
++    from.dest.addr.in4.sin_addr.s_addr = ntohl(0x01020304);
+     struct tls_auth_standalone tas = { 0 };
+     struct tls_pre_decrypt_state state = { 0 };
+ 
+@@ -471,10 +473,12 @@ test_verify_hmac_tls_auth(void **ut_state)
+ static void
+ test_verify_hmac_none(void **ut_state)
+ {
++    now = 1000;
+     hmac_ctx_t *hmac = session_id_hmac_init();
+ 
+     struct link_socket_actual from = { 0 };
+     from.dest.addr.sa.sa_family = AF_INET;
++    from.dest.addr.in4.sin_addr.s_addr = ntohl(0x01020304);
+ 
+     struct tls_auth_standalone tas = { 0 };
+     struct tls_pre_decrypt_state state = { 0 };
+@@ -489,9 +493,61 @@ test_verify_hmac_none(void **ut_state)
+     verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
+     assert_int_equal(verdict, VERDICT_VALID_ACK_V1);
+ 
++    /* This packet has a random hmac, so it should fail to validate */
+     bool valid = check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true);
++    assert_false(valid);
++
++    struct session_id client_id = { { 0xae, 0xb9, 0xaf, 0xe1, 0xf0, 0x1d, 0x79, 0xc8 } };
++    assert_memory_equal(&client_id, &state.peer_session_id, sizeof(struct session_id));
++
++    struct session_id expected_id = calculate_session_id_hmac(client_id, &from.dest, hmac, 30, 0);
++
++    free_tls_pre_decrypt_state(&state);
++    buf_reset_len(&buf);
++
++    /* Write the packet again into the buffer but this time, replacing the peer packet
++     * id with the expected one */
++    buf_write(&buf, client_ack_none_random_id, sizeof(client_ack_none_random_id) - 8);
++    buf_write(&buf, expected_id.id, 8);
++
++    verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
++    assert_int_equal(verdict, VERDICT_VALID_ACK_V1);
++    valid = check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true);
++
+     assert_true(valid);
+ 
++    /* Our handwindow is 30 so the slices are half of that, so they are
++     * (975,990), (990, 1005), (1005, 1020), (1020, 1035), (1035, 1050)
++     * So setting time to the two future ones should work
++     */
++    now = 980;
++    assert_false(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++    now = 1040;
++    assert_false(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++    now = 1002;
++    assert_true(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++    now = 1022;
++    assert_true(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++    now = 1010;
++    assert_true(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++
++    /* Changing the IP address should make this invalid */
++    from.dest.addr.in4.sin_addr.s_addr = ntohl(0x01020305);
++    assert_false(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++
++    /* Change to the correct one again */
++    from.dest.addr.in4.sin_addr.s_addr = ntohl(0x01020304);
++    assert_true(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++
++    /* Modify the peer id, should now fail hmac verification */
++    buf_inc_len(&buf, -4);
++    buf_write_u32(&buf, 0x12345678);
++
++    free_tls_pre_decrypt_state(&state);
++    verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
++    assert_int_equal(verdict, VERDICT_VALID_ACK_V1);
++    assert_false(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++
+     free_tls_pre_decrypt_state(&state);
+     free_buf(&buf);
+     hmac_ctx_cleanup(hmac);
+@@ -723,12 +779,12 @@ int
+ main(void)
+ {
+     const struct CMUnitTest tests[] = {
++        cmocka_unit_test(test_verify_hmac_none),
+         cmocka_unit_test(test_tls_decrypt_lite_none),
+         cmocka_unit_test(test_tls_decrypt_lite_auth),
+         cmocka_unit_test(test_tls_decrypt_lite_crypt),
+         cmocka_unit_test(test_parse_ack),
+         cmocka_unit_test(test_calc_session_id_hmac_static),
+-        cmocka_unit_test(test_verify_hmac_none),
+         cmocka_unit_test(test_verify_hmac_tls_auth),
+         cmocka_unit_test(test_verify_hmac_none_out_of_range_ack),
+         cmocka_unit_test(test_generate_reset_packet_plain),
diff -Nru openvpn-2.6.14/debian/patches/fix-ftbfs-kernel-6.16.patch openvpn-2.6.14/debian/patches/fix-ftbfs-kernel-6.16.patch
--- openvpn-2.6.14/debian/patches/fix-ftbfs-kernel-6.16.patch	1970-01-01 01:00:00.000000000 +0100
+++ openvpn-2.6.14/debian/patches/fix-ftbfs-kernel-6.16.patch	2025-11-21 00:45:17.000000000 +0100
@@ -0,0 +1,78 @@
+From 1fbbe91d292fb925f5af73b512d7d1c83abfe714 Mon Sep 17 00:00:00 2001
+From: Frank Lichtenheld <[email protected]>
+Date: Fri, 1 Aug 2025 15:03:02 +0200
+Subject: [PATCH] dco linux: avoid redefining ovpn enums (2.6)
+
+Starting with Linux kernel version 6.16, a couple of ovpn-related enum
+definitions were introduced in the `include/uapi/linux/if_link.h`
+header. Redefining them in openvpn when they are already present in the
+system headers can lead to conflicts or build issues.
+
+This commit ensures that enum redefinitions are avoided by conditionally
+using the existing definitions from the system header when available.
+
+This is the port to release/2.6 based on commit
+1d3c2b67a73a0aa011c13e62f876d24e49d41df0.
+
+Change-Id: I41c5dfc7489352a9534ff6b1585a5a81e0623ab1
+Signed-off-by: Frank Lichtenheld <[email protected]>
+Acked-by: Antonio Quartulli <[email protected]>
+Message-Id: <[email protected]>
+URL: https://www.mail-archive.com/[email protected]/msg32470.html
+Signed-off-by: Gert Doering <[email protected]>
+---
+ src/openvpn/dco_linux.h      | 6 ++++--
+ src/openvpn/ovpn_dco_linux.h | 9 +++++++--
+ 2 files changed, 11 insertions(+), 4 deletions(-)
+
+diff --git a/src/openvpn/dco_linux.h b/src/openvpn/dco_linux.h
+index 511519aece4..5179912b522 100644
+--- a/src/openvpn/dco_linux.h
++++ b/src/openvpn/dco_linux.h
+@@ -26,11 +26,13 @@
+ 
+ #include "event.h"
+ 
+-#include "ovpn_dco_linux.h"
+-
+ #include <netlink/socket.h>
+ #include <netlink/netlink.h>
+ 
++/* include last since we need to behave differently if the kernel headers
++ * are from 6.16+ */
++#include "ovpn_dco_linux.h"
++
+ typedef enum ovpn_key_slot dco_key_slot_t;
+ typedef enum ovpn_cipher_alg dco_cipher_t;
+ 
+diff --git a/src/openvpn/ovpn_dco_linux.h b/src/openvpn/ovpn_dco_linux.h
+index 73e19b591cb..34abc6ab79d 100644
+--- a/src/openvpn/ovpn_dco_linux.h
++++ b/src/openvpn/ovpn_dco_linux.h
+@@ -237,14 +237,17 @@ enum ovpn_netlink_packet_attrs {
+ 	OVPN_PACKET_ATTR_MAX = __OVPN_PACKET_ATTR_AFTER_LAST - 1,
+ };
+ 
++#ifndef IFLA_OVPN_MAX
++
+ enum ovpn_ifla_attrs {
+ 	IFLA_OVPN_UNSPEC = 0,
+ 	IFLA_OVPN_MODE,
+ 
+-	__IFLA_OVPN_AFTER_LAST,
+-	IFLA_OVPN_MAX = __IFLA_OVPN_AFTER_LAST - 1,
++	__IFLA_OVPN_MAX,
+ };
+ 
++#define IFLA_OVPN_MAX (__IFLA_OVPN_MAX - 1)
++
+ enum ovpn_mode {
+ 	__OVPN_MODE_FIRST = 0,
+ 	OVPN_MODE_P2P = __OVPN_MODE_FIRST,
+@@ -253,4 +256,6 @@ enum ovpn_mode {
+ 	__OVPN_MODE_AFTER_LAST,
+ };
+ 
++#endif /* ifndef IFLA_OVPN_MAX */
++
+ #endif /* _UAPI_LINUX_OVPN_DCO_H_ */
diff -Nru openvpn-2.6.14/debian/patches/series openvpn-2.6.14/debian/patches/series
--- openvpn-2.6.14/debian/patches/series	2025-04-02 20:56:44.000000000 +0200
+++ openvpn-2.6.14/debian/patches/series	2025-11-21 00:45:17.000000000 +0100
@@ -2,3 +2,6 @@
 auth-pam_libpam_so_filename.patch
 #debian_nogroup_for_sample_files.patch
 openvpn-pkcs11warn.patch
+fix-ftbfs-kernel-6.16.patch
+check-message-id.patch
+CVE-2025-13086.patch
diffstat for openvpn-2.6.3 openvpn-2.6.3

 changelog                                  |   25 ++
 patches/CVE-2024-5594-regression-fix.patch |  203 ++++++++++++++++++
 patches/CVE-2025-13086.patch               |  155 ++++++++++++++
 patches/check-message-id.patch             |  312 +++++++++++++++++++++++++++++
 patches/series                             |    4 
 salsa-ci.yml                               |    9 
 tests/control                              |    4 
 tests/unit-tests                           |   13 +
 8 files changed, 724 insertions(+), 1 deletion(-)

diff -Nru openvpn-2.6.3/debian/changelog openvpn-2.6.3/debian/changelog
--- openvpn-2.6.3/debian/changelog	2025-04-02 17:45:15.000000000 +0200
+++ openvpn-2.6.3/debian/changelog	2025-11-26 22:54:51.000000000 +0100
@@ -1,3 +1,28 @@
+openvpn (2.6.3-1+deb12u4) bookworm-security; urgency=medium
+
+  * Cherry-pick patches for CVE-2025-13086
+    - check-message-id.patch: Check message id/acked ids too when doing
+      sessionid cookie checks - bugfix for floating client problem, code
+      prequesite for the CVE patch to apply
+    - CVE-2025-13086.patch: Fix memcmp check for the hmac verification in the
+      3way handshake being inverted (Closes: #1121086)
+
+  [ Aquila Macedo ]
+  * Add new autopkgtest for unit tests.
+
+  [ Carlos Henrique Lima Melara ]
+  * debian/patches/CVE-2024-5594-regression-fix.patch: cherry-pick from
+    upstream to fix a regression introduced with CVE-2024-5594's fix. Namely,
+    "Allow trailing \r and \n in control channel message". (Closes: #1112516)
+  * debian/salsa-ci:
+      - Allow lintian job to fail. Sid's version dislikes things from bookworm.
+      - Disable gbp setup-gitattributes.
+      - Disable reprotest on bookworm. It can't run on bookworm, so the build
+        fails because of build dependencies problems.
+  * debian/tests/unit-tests: enable unit-tests in configure and be verbose.
+
+ -- Bernhard Schmidt <[email protected]>  Wed, 26 Nov 2025 22:54:51 +0100
+
 openvpn (2.6.3-1+deb12u3) bookworm; urgency=medium
 
   [ Bernhard Schmidt ]
diff -Nru openvpn-2.6.3/debian/patches/check-message-id.patch openvpn-2.6.3/debian/patches/check-message-id.patch
--- openvpn-2.6.3/debian/patches/check-message-id.patch	1970-01-01 01:00:00.000000000 +0100
+++ openvpn-2.6.3/debian/patches/check-message-id.patch	2025-11-26 22:54:51.000000000 +0100
@@ -0,0 +1,312 @@
+From 68c01720eecc1772b3f648b9e043e396d943f632 Mon Sep 17 00:00:00 2001
+From: Arne Schwabe <[email protected]>
+Date: Tue, 16 Sep 2025 17:52:50 +0200
+Subject: [PATCH] Check message id/acked ids too when doing sessionid cookie
+ checks
+
+This fixes that control packets on a floating client can trigger
+creating a new session in special circumstances:
+
+To trigger this circumstance a connection needs to
+
+- starts on IP A
+- successfully floats to IP B by data packet
+- then has a control packet from IP A before any
+  data packet can trigger the float back to IP A
+
+and all of this needs to happen in the 60s time
+that hmac cookie is valid in the default
+configuration.
+
+In this scenario we would trigger a new connection as the HMAC
+session id would be valid.
+
+This patch adds checking also of the message-id and acked ids to
+discern packet from the initial three-way handshake where these
+ids are 0 or 1 from any later packet.
+
+This will now trigger (at verb 4 or higher) a messaged like:
+
+   Packet (P_ACK_V1) with invalid or missing SID
+
+instead.
+
+Also remove a few duplicated free_tls_pre_decrypt_state in test_ssl.
+
+Reported-By: Walter Doekes <[email protected]>
+Tested-By: Walter Doekes <[email protected]>
+
+Change-Id: I6752dcd5aff3e5cea2b439366479e86751a1c403
+Signed-off-by: Arne Schwabe <[email protected]>
+Acked-by: MaxF <[email protected]>
+Gerrit URL: https://gerrit.openvpn.net/c/openvpn/+/1184
+Message-Id: <[email protected]>
+URL: https://www.mail-archive.com/[email protected]/msg32990.html
+Signed-off-by: Gert Doering <[email protected]>
+(backported from commit 518e122b42739b0dbb54e7169a8a3aadb4773125)
+---
+ src/openvpn/mudp.c                  |  6 ++-
+ src/openvpn/ssl_pkt.c               | 39 +++++++++++++--
+ src/openvpn/ssl_pkt.h               | 15 +++---
+ tests/unit_tests/openvpn/test_pkt.c | 77 ++++++++++++++++++++++++++---
+ 4 files changed, 117 insertions(+), 20 deletions(-)
+
+diff --git a/src/openvpn/mudp.c b/src/openvpn/mudp.c
+index cdde0b5c972..0492311669c 100644
+--- a/src/openvpn/mudp.c
++++ b/src/openvpn/mudp.c
+@@ -153,7 +153,8 @@ do_pre_decrypt_check(struct multi_context *m,
+          * need to contain the peer id */
+         struct gc_arena gc = gc_new();
+ 
+-        bool ret = check_session_id_hmac(state, from, hmac, handwindow);
++        bool pkt_is_ack = (verdict == VERDICT_VALID_ACK_V1);
++        bool ret = check_session_hmac_and_pkt_id(state, from, hmac, handwindow, pkt_is_ack);
+ 
+         const char *peer = print_link_socket_actual(&m->top.c2.from, &gc);
+         uint8_t pkt_firstbyte = *BPTR( &m->top.c2.buf);
+@@ -161,7 +162,8 @@ do_pre_decrypt_check(struct multi_context *m,
+ 
+         if (!ret)
+         {
+-            msg(D_MULTI_MEDIUM, "Packet (%s) with invalid or missing SID from %s",
++            msg(D_MULTI_MEDIUM, "Packet (%s) with invalid or missing SID from"
++                " %s or wrong packet id",
+                 packet_opcode_name(op), peer);
+         }
+         else
+diff --git a/src/openvpn/ssl_pkt.c b/src/openvpn/ssl_pkt.c
+index 41299f462db..432bb8b8181 100644
+--- a/src/openvpn/ssl_pkt.c
++++ b/src/openvpn/ssl_pkt.c
+@@ -527,10 +527,11 @@ calculate_session_id_hmac(struct session_id client_sid,
+ }
+ 
+ bool
+-check_session_id_hmac(struct tls_pre_decrypt_state *state,
+-                      const struct openvpn_sockaddr *from,
+-                      hmac_ctx_t *hmac,
+-                      int handwindow)
++check_session_hmac_and_pkt_id(struct tls_pre_decrypt_state *state,
++                              const struct openvpn_sockaddr *from,
++                              hmac_ctx_t *hmac,
++                              int handwindow,
++                              bool pkt_is_ack)
+ {
+     if (!from)
+     {
+@@ -545,6 +546,36 @@ check_session_id_hmac(struct tls_pre_decrypt_state *state,
+         return false;
+     }
+ 
++    /* Check if the packet ID of the packet or ACKED packet  is <= 1 */
++    for (int i = 0; i < ack.len; i++)
++    {
++        /* This packet ACKs a packet that has a higher packet id than the
++         * ones expected in the three-way handshake, consider it as invalid
++         * for the session */
++        if (ack.packet_id[i] > 1)
++        {
++            return false;
++        }
++    }
++
++    if (!pkt_is_ack)
++    {
++        packet_id_type message_id;
++        /* Extract the packet ID from the packet */
++        if (!reliable_ack_read_packet_id(&buf, &message_id))
++        {
++            return false;
++        }
++
++        /* similar check. Anything larger than 1 is not considered part of the
++         * three-way handshake */
++        if (message_id > 1)
++        {
++            return false;
++        }
++    }
++
++
+     /* check adjacent timestamps too */
+     for (int offset = -2; offset <= 1; offset++)
+     {
+diff --git a/src/openvpn/ssl_pkt.h b/src/openvpn/ssl_pkt.h
+index 2033da61ff7..1df691c369a 100644
+--- a/src/openvpn/ssl_pkt.h
++++ b/src/openvpn/ssl_pkt.h
+@@ -182,17 +182,20 @@ calculate_session_id_hmac(struct session_id client_sid,
+ /**
+  * Checks if a control packet has a correct HMAC server session id
+  *
+- * @param client_sid    session id of the client
++ * This will also consider packets that have a packet id higher
++ * than 1 or ack packets higher than 1 to be invalid as they are
++ * not part of the initial three way handshake of OpenVPN and should
++ * not create a new connection.
++ *
++ * @param state         session information
+  * @param from          link_socket from the client
+  * @param hmac          the hmac context to use for the calculation
+  * @param handwindow    the quantisation of the current time
++ * @param pkt_is_ack    the packet being checked is a P_ACK_V1
+  * @return              the expected server session id
+  */
+-bool
+-check_session_id_hmac(struct tls_pre_decrypt_state *state,
+-                      const struct openvpn_sockaddr *from,
+-                      hmac_ctx_t *hmac,
+-                      int handwindow);
++bool check_session_hmac_and_pkt_id(struct tls_pre_decrypt_state *state, const struct openvpn_sockaddr *from,
++                                   hmac_ctx_t *hmac, int handwindow, bool pkt_is_ack);
+ 
+ /*
+  * Write a control channel authentication record.
+diff --git a/tests/unit_tests/openvpn/test_pkt.c b/tests/unit_tests/openvpn/test_pkt.c
+index 74d7311f74d..27f52cf500d 100644
+--- a/tests/unit_tests/openvpn/test_pkt.c
++++ b/tests/unit_tests/openvpn/test_pkt.c
+@@ -174,6 +174,27 @@ const uint8_t client_ack_none_random_id[] = {
+     0x85, 0xdb, 0x53, 0x56, 0x23, 0xb0, 0x2e
+ };
+ 
++/* no tls-auth, P_ACK_V1, acks 0,1, and 2 */
++const uint8_t client_ack_123_none_random_id[] = {
++    0x28,
++    0xae, 0xb9, 0xaf, 0xe1, 0xf0, 0x1d, 0x79, 0xc8,
++    0x03,
++    0x00, 0x00, 0x00, 0x00,
++    0x00, 0x00, 0x00, 0x01,
++    0x00, 0x00, 0x00, 0x02,
++    0xdd, 0x85, 0xdb, 0x53, 0x56, 0x23, 0xb0, 0x2e
++};
++
++/* no tls-auth, P_CONTROL_V1, acks 0, msg-id 2 */
++const uint8_t client_control_none_random_id[] = {
++    0x20,
++    0xae, 0xb9, 0xaf, 0xe1, 0xf0, 0x1d, 0x79, 0xc8,
++    0x01,
++    0x00, 0x00, 0x00, 0x00,
++    0x02
++};
++
++
+ struct tls_auth_standalone
+ init_tas_auth(int key_direction)
+ {
+@@ -294,12 +315,10 @@ test_tls_decrypt_lite_auth(void **ut_state)
+     assert_int_equal(verdict, VERDICT_VALID_RESET_V2);
+     free_tls_pre_decrypt_state(&state);
+ 
+-    free_tls_pre_decrypt_state(&state);
+     /* The pre decrypt function should not modify the buffer, so calling it
+      * again should have the same result */
+     verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
+     assert_int_equal(verdict, VERDICT_VALID_RESET_V2);
+-    free_tls_pre_decrypt_state(&state);
+ 
+     /* and buf memory should be equal */
+     assert_memory_equal(BPTR(&buf), client_reset_v2_tls_auth, sizeof(client_reset_v2_tls_auth));
+@@ -317,7 +336,6 @@ test_tls_decrypt_lite_auth(void **ut_state)
+     assert_int_equal(verdict, VERDICT_INVALID);
+     free_tls_pre_decrypt_state(&state);
+ 
+-    free_tls_pre_decrypt_state(&state);
+     /* Wrong key direction gives a wrong hmac key and should not validate */
+     free_key_ctx_bi(&tas.tls_wrap.opt.key_ctx_bi);
+     free_tas(&tas);
+@@ -357,15 +375,12 @@ test_tls_decrypt_lite_none(void **ut_state)
+     assert_int_equal(verdict, VERDICT_VALID_RESET_V2);
+     free_tls_pre_decrypt_state(&state);
+ 
+-    free_tls_pre_decrypt_state(&state);
+     buf_reset_len(&buf);
+     buf_write(&buf, client_reset_v2_tls_crypt, sizeof(client_reset_v2_none));
+     verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
+     assert_int_equal(verdict, VERDICT_VALID_RESET_V2);
+     free_tls_pre_decrypt_state(&state);
+ 
+-    free_tls_pre_decrypt_state(&state);
+-
+     /* This is not a reset packet and should trigger the other response */
+     buf_reset_len(&buf);
+     buf_write(&buf, client_ack_tls_auth_randomid, sizeof(client_ack_tls_auth_randomid));
+@@ -443,7 +458,7 @@ test_verify_hmac_tls_auth(void **ut_state)
+     assert_int_equal(verdict, VERDICT_VALID_CONTROL_V1);
+ 
+     /* This is a valid packet but containing a random id instead of an HMAC id*/
+-    bool valid = check_session_id_hmac(&state, &from.dest, hmac, 30);
++    bool valid = check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, false);
+     assert_false(valid);
+ 
+     free_tls_pre_decrypt_state(&state);
+@@ -474,7 +489,7 @@ test_verify_hmac_none(void **ut_state)
+     verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
+     assert_int_equal(verdict, VERDICT_VALID_ACK_V1);
+ 
+-    bool valid = check_session_id_hmac(&state, &from.dest, hmac, 30);
++    bool valid = check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true);
+     assert_true(valid);
+ 
+     free_tls_pre_decrypt_state(&state);
+@@ -483,6 +498,51 @@ test_verify_hmac_none(void **ut_state)
+     hmac_ctx_free(hmac);
+ }
+ 
++static void
++test_verify_hmac_none_out_of_range_ack(void **ut_state)
++{
++    hmac_ctx_t *hmac = session_id_hmac_init();
++
++    struct link_socket_actual from = { 0 };
++    from.dest.addr.sa.sa_family = AF_INET;
++
++    struct tls_auth_standalone tas = { 0 };
++    struct tls_pre_decrypt_state state = { 0 };
++
++    struct buffer buf = alloc_buf(1024);
++    enum first_packet_verdict verdict;
++
++    tas.tls_wrap.mode = TLS_WRAP_NONE;
++
++    buf_reset_len(&buf);
++    buf_write(&buf, client_ack_123_none_random_id, sizeof(client_ack_123_none_random_id));
++
++
++    verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
++    assert_int_equal(verdict, VERDICT_VALID_ACK_V1);
++
++    /* should fail because it acks 2 */
++    bool valid = check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true);
++    assert_false(valid);
++    free_tls_pre_decrypt_state(&state);
++
++    /* Try test with the control with a too high message id now */
++    buf_reset_len(&buf);
++    buf_write(&buf, client_control_none_random_id, sizeof(client_control_none_random_id));
++
++    verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
++    assert_int_equal(verdict, VERDICT_VALID_CONTROL_V1);
++
++    /* should fail because it has message id 2 */
++    valid = check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true);
++    assert_false(valid);
++
++    free_tls_pre_decrypt_state(&state);
++    free_buf(&buf);
++    hmac_ctx_cleanup(hmac);
++    hmac_ctx_free(hmac);
++}
++
+ static hmac_ctx_t *
+ init_static_hmac(void)
+ {
+@@ -670,6 +730,7 @@ main(void)
+         cmocka_unit_test(test_calc_session_id_hmac_static),
+         cmocka_unit_test(test_verify_hmac_none),
+         cmocka_unit_test(test_verify_hmac_tls_auth),
++        cmocka_unit_test(test_verify_hmac_none_out_of_range_ack),
+         cmocka_unit_test(test_generate_reset_packet_plain),
+         cmocka_unit_test(test_generate_reset_packet_tls_auth),
+         cmocka_unit_test(test_extract_control_message)
diff -Nru openvpn-2.6.3/debian/patches/CVE-2024-5594-regression-fix.patch openvpn-2.6.3/debian/patches/CVE-2024-5594-regression-fix.patch
--- openvpn-2.6.3/debian/patches/CVE-2024-5594-regression-fix.patch	1970-01-01 01:00:00.000000000 +0100
+++ openvpn-2.6.3/debian/patches/CVE-2024-5594-regression-fix.patch	2025-11-26 22:54:51.000000000 +0100
@@ -0,0 +1,203 @@
+From: Arne Schwabe <[email protected]>
+Date: Wed, 10 Jul 2024 16:06:23 +0200
+Subject: Allow trailing \r and \n in control channel message
+
+Writing a reason from a script will easily end up adding extra \r\n characters
+at the end of the reason. Our current code pushes this to the peer. So be more
+liberal in accepting these message.
+
+Github: closes OpenVPN/openvpn#568
+
+Change-Id: I47c992b6b73b1475cbff8a28f720cf50dc1fbe3e
+Signed-off-by: Arne Schwabe <[email protected]>
+Acked-by: Frank Lichtenheld <[email protected]>
+Message-Id: <[email protected]>
+URL: https://www.mail-archive.com/[email protected]/msg28910.html
+Signed-off-by: Gert Doering <[email protected]>
+(cherry picked from commit be31325e1dfdffbb152374985c2ae7b6644e3519)
+
+Origin: upstream, https://github.com/OpenVPN/openvpn/commit/343573990135023d855d151fcd9248e5c26d9f8b
+Bug: https://github.com/OpenVPN/openvpn/issues/568
+Last-Update: 2025-08-24
+---
+ src/openvpn/forward.c               | 33 +++---------------------------
+ src/openvpn/ssl_pkt.c               | 40 +++++++++++++++++++++++++++++++++++++
+ src/openvpn/ssl_pkt.h               | 14 +++++++++++++
+ tests/unit_tests/openvpn/test_pkt.c | 35 ++++++++++++++++++++++++++++++++
+ 4 files changed, 92 insertions(+), 30 deletions(-)
+
+diff --git a/src/openvpn/forward.c b/src/openvpn/forward.c
+index b565bfa..48c4276 100644
+--- a/src/openvpn/forward.c
++++ b/src/openvpn/forward.c
+@@ -292,41 +292,14 @@ check_incoming_control_channel(struct context *c)
+     struct buffer buf = alloc_buf_gc(len, &gc);
+     if (tls_rec_payload(c->c2.tls_multi, &buf))
+     {
+-
+         while (BLEN(&buf) > 1)
+         {
+-            /* commands on the control channel are seperated by 0x00 bytes.
+-             * cmdlen does not include the 0 byte of the string */
+-            int cmdlen = (int)strnlen(BSTR(&buf), BLEN(&buf));
+-
+-            if (cmdlen < BLEN(&buf))
+-            {
+-                /* include the NUL byte and ensure NUL termination */
+-                int cmdlen = (int)strlen(BSTR(&buf)) + 1;
++            struct buffer cmdbuf = extract_command_buffer(&buf, &gc);
+ 
+-                /* Construct a buffer that only holds the current command and
+-                 * its closing NUL byte */
+-                struct buffer cmdbuf = alloc_buf_gc(cmdlen, &gc);
+-                buf_write(&cmdbuf, BPTR(&buf), cmdlen);
+-
+-                /* check we have only printable characters or null byte in the
+-                 * command string and no newlines */
+-                if (!string_check_buf(&buf, CC_PRINT | CC_NULL, CC_CRLF))
+-                {
+-                    msg(D_PUSH_ERRORS, "WARNING: Received control with invalid characters: %s",
+-                        format_hex(BPTR(&buf), BLEN(&buf), 256, &gc));
+-                }
+-                else
+-                {
+-                    parse_incoming_control_channel_command(c, &cmdbuf);
+-                }
+-            }
+-            else
++            if (cmdbuf.len > 0)
+             {
+-                msg(D_PUSH_ERRORS, "WARNING: Ignoring control channel "
+-                    "message command without NUL termination");
++                parse_incoming_control_channel_command(c, &cmdbuf);
+             }
+-            buf_advance(&buf, cmdlen);
+         }
+     }
+     else
+diff --git a/src/openvpn/ssl_pkt.c b/src/openvpn/ssl_pkt.c
+index 7229f55..42cb130 100644
+--- a/src/openvpn/ssl_pkt.c
++++ b/src/openvpn/ssl_pkt.c
+@@ -560,3 +560,43 @@ check_session_id_hmac(struct tls_pre_decrypt_state *state,
+     }
+     return false;
+ }
++
++struct buffer
++extract_command_buffer(struct buffer *buf, struct gc_arena *gc)
++{
++    /* commands on the control channel are seperated by 0x00 bytes.
++     * cmdlen does not include the 0 byte of the string */
++    int cmdlen = (int)strnlen(BSTR(buf), BLEN(buf));
++
++    if (cmdlen >= BLEN(buf))
++    {
++        buf_advance(buf, cmdlen);
++        /* Return empty buffer */
++        struct buffer empty = { 0 };
++        return empty;
++    }
++
++    /* include the NUL byte and ensure NUL termination */
++    cmdlen +=  1;
++
++    /* Construct a buffer that only holds the current command and
++     * its closing NUL byte */
++    struct buffer cmdbuf = alloc_buf_gc(cmdlen, gc);
++    buf_write(&cmdbuf, BPTR(buf), cmdlen);
++
++    /* Remove \r and \n at the end of the buffer to avoid
++     * problems with scripts and other that add extra \r and \n */
++    buf_chomp(&cmdbuf);
++
++    /* check we have only printable characters or null byte in the
++     * command string and no newlines */
++    if (!string_check_buf(&cmdbuf, CC_PRINT | CC_NULL, CC_CRLF))
++    {
++        msg(D_PUSH_ERRORS, "WARNING: Received control with invalid characters: %s",
++            format_hex(BPTR(&cmdbuf), BLEN(&cmdbuf), 256, gc));
++        cmdbuf.len = 0;
++    }
++
++    buf_advance(buf, cmdlen);
++    return cmdbuf;
++}
+diff --git a/src/openvpn/ssl_pkt.h b/src/openvpn/ssl_pkt.h
+index 43c303f..f92eacc 100644
+--- a/src/openvpn/ssl_pkt.h
++++ b/src/openvpn/ssl_pkt.h
+@@ -238,6 +238,20 @@ tls_reset_standalone(struct tls_wrap_ctx *ctx,
+                      uint8_t header,
+                      bool request_resend_wkc);
+ 
++
++/**
++ * Extracts a control channel message from buf and adjusts the size of
++ * buf after the message has been extracted
++ * @param buf   The buffer the message should be extracted from
++ * @param gc    gc_arena to be used for the returned buffer and displaying
++ *              diagnostic messages
++ * @return      A buffer with a control channel message or a buffer with
++ *              with length 0 if there is no message or the message has
++ *              invalid characters.
++ */
++struct buffer
++extract_command_buffer(struct buffer *buf, struct gc_arena *gc);
++
+ static inline const char *
+ packet_opcode_name(int op)
+ {
+diff --git a/tests/unit_tests/openvpn/test_pkt.c b/tests/unit_tests/openvpn/test_pkt.c
+index 736f131..f3dc855 100644
+--- a/tests/unit_tests/openvpn/test_pkt.c
++++ b/tests/unit_tests/openvpn/test_pkt.c
+@@ -636,6 +636,40 @@ test_generate_reset_packet_tls_auth(void **ut_state)
+     free_tas(&tas_server);
+ }
+ 
++static void
++test_extract_control_message(void **ut_state)
++{
++    struct gc_arena gc = gc_new();
++    struct buffer input_buf = alloc_buf_gc(1024, &gc);
++
++    /* This message will have a \0x00 at the end since it is a C string */
++    const char input[] = "valid control message\r\n\0\0Invalid\r\none\0valid one again";
++
++    buf_write(&input_buf, input, sizeof(input));
++    struct buffer cmd1 = extract_command_buffer(&input_buf, &gc);
++    struct buffer cmd2 = extract_command_buffer(&input_buf, &gc);
++    struct buffer cmd3 = extract_command_buffer(&input_buf, &gc);
++    struct buffer cmd4 = extract_command_buffer(&input_buf, &gc);
++    struct buffer cmd5 = extract_command_buffer(&input_buf, &gc);
++
++    assert_string_equal(BSTR(&cmd1), "valid control message");
++    /* empty message with just a \0x00 */
++    assert_int_equal(cmd2.len, 1);
++    assert_string_equal(BSTR(&cmd2), "");
++    assert_int_equal(cmd3.len, 0);
++    assert_string_equal(BSTR(&cmd4), "valid one again");
++    assert_int_equal(cmd5.len, 0);
++
++    const uint8_t nonull[6] = { 'n', 'o', ' ', 'N', 'U', 'L'};
++    struct buffer nonull_buf = alloc_buf_gc(1024, &gc);
++
++    buf_write(&nonull_buf, nonull, sizeof(nonull));
++    struct buffer nonullcmd = extract_command_buffer(&nonull_buf, &gc);
++    assert_int_equal(nonullcmd.len, 0);
++
++    gc_free(&gc);
++}
++
+ int
+ main(void)
+ {
+@@ -649,6 +683,7 @@ main(void)
+         cmocka_unit_test(test_verify_hmac_tls_auth),
+         cmocka_unit_test(test_generate_reset_packet_plain),
+         cmocka_unit_test(test_generate_reset_packet_tls_auth),
++        cmocka_unit_test(test_extract_control_message)
+     };
+ 
+ #if defined(ENABLE_CRYPTO_OPENSSL)
diff -Nru openvpn-2.6.3/debian/patches/CVE-2025-13086.patch openvpn-2.6.3/debian/patches/CVE-2025-13086.patch
--- openvpn-2.6.3/debian/patches/CVE-2025-13086.patch	1970-01-01 01:00:00.000000000 +0100
+++ openvpn-2.6.3/debian/patches/CVE-2025-13086.patch	2025-11-26 22:54:51.000000000 +0100
@@ -0,0 +1,155 @@
+From fa6a1824b0f37bff137204156a74ca28cf5b6f83 Mon Sep 17 00:00:00 2001
+From: Arne Schwabe <[email protected]>
+Date: Mon, 27 Oct 2025 10:05:55 +0100
+Subject: [PATCH] Fix memcmp check for the hmac verification in the 3way
+ handshake being inverted
+
+This is a stupid mistake but causes all hmac cookies to be accepted,
+thus breaking source IP address validation.   As a consequence, TLS
+sessions can be openend and state can be consumed in the server from
+IP addresses that did not initiate an initial connection.
+
+While at it, fix check to only allow [t-2;t] timeslots, disallowing
+HMACs coming in from a future timeslot.
+
+Github: OpenVPN/openvpn-private-issues#56
+
+CVE: 2025-13086
+
+Reported-By: Joshua Rogers <[email protected]>
+Found-by: ZeroPath (https://zeropath.com/)
+Reported-By: [email protected]
+
+Change-Id: I9cbe2bf535575b47ddd7f34e985c5c1c6953a6fc
+Signed-off-by: Arne Schwabe <[email protected]>
+Acked-by: Max Fillinger <[email protected]>
+(cherry picked from commit 68ec931e7fb4af11d5ba0d4283df0350083fd373)
+---
+ src/openvpn/ssl_pkt.c               |  7 ++--
+ tests/unit_tests/openvpn/test_pkt.c | 58 ++++++++++++++++++++++++++++-
+ 2 files changed, 61 insertions(+), 4 deletions(-)
+
+diff --git a/src/openvpn/ssl_pkt.c b/src/openvpn/ssl_pkt.c
+index 432bb8b8181..cf1ce172732 100644
+--- a/src/openvpn/ssl_pkt.c
++++ b/src/openvpn/ssl_pkt.c
+@@ -576,13 +576,14 @@ check_session_hmac_and_pkt_id(struct tls_pre_decrypt_state *state,
+     }
+ 
+ 
+-    /* check adjacent timestamps too */
+-    for (int offset = -2; offset <= 1; offset++)
++    /* check adjacent timestamps too, the handwindow is split in 2 for the
++     * offset, so we check the current timeslot and the two before that */
++    for (int offset = -2; offset <= 0; offset++)
+     {
+         struct session_id expected_id =
+             calculate_session_id_hmac(state->peer_session_id, from, hmac, handwindow, offset);
+ 
+-        if (memcmp_constant_time(&expected_id, &state->server_session_id, SID_SIZE))
++        if (memcmp_constant_time(&expected_id, &state->server_session_id, SID_SIZE) == 0)
+         {
+             return true;
+         }
+diff --git a/tests/unit_tests/openvpn/test_pkt.c b/tests/unit_tests/openvpn/test_pkt.c
+index 27f52cf500d..62abafaac99 100644
+--- a/tests/unit_tests/openvpn/test_pkt.c
++++ b/tests/unit_tests/openvpn/test_pkt.c
+@@ -444,6 +444,8 @@ test_verify_hmac_tls_auth(void **ut_state)
+     hmac_ctx_t *hmac = session_id_hmac_init();
+ 
+     struct link_socket_actual from = { 0 };
++    from.dest.addr.sa.sa_family = AF_INET;
++    from.dest.addr.in4.sin_addr.s_addr = ntohl(0x01020304);
+     struct tls_auth_standalone tas = { 0 };
+     struct tls_pre_decrypt_state state = { 0 };
+ 
+@@ -471,10 +473,12 @@ test_verify_hmac_tls_auth(void **ut_state)
+ static void
+ test_verify_hmac_none(void **ut_state)
+ {
++    now = 1000;
+     hmac_ctx_t *hmac = session_id_hmac_init();
+ 
+     struct link_socket_actual from = { 0 };
+     from.dest.addr.sa.sa_family = AF_INET;
++    from.dest.addr.in4.sin_addr.s_addr = ntohl(0x01020304);
+ 
+     struct tls_auth_standalone tas = { 0 };
+     struct tls_pre_decrypt_state state = { 0 };
+@@ -489,9 +493,61 @@ test_verify_hmac_none(void **ut_state)
+     verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
+     assert_int_equal(verdict, VERDICT_VALID_ACK_V1);
+ 
++    /* This packet has a random hmac, so it should fail to validate */
+     bool valid = check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true);
++    assert_false(valid);
++
++    struct session_id client_id = { { 0xae, 0xb9, 0xaf, 0xe1, 0xf0, 0x1d, 0x79, 0xc8 } };
++    assert_memory_equal(&client_id, &state.peer_session_id, sizeof(struct session_id));
++
++    struct session_id expected_id = calculate_session_id_hmac(client_id, &from.dest, hmac, 30, 0);
++
++    free_tls_pre_decrypt_state(&state);
++    buf_reset_len(&buf);
++
++    /* Write the packet again into the buffer but this time, replacing the peer packet
++     * id with the expected one */
++    buf_write(&buf, client_ack_none_random_id, sizeof(client_ack_none_random_id) - 8);
++    buf_write(&buf, expected_id.id, 8);
++
++    verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
++    assert_int_equal(verdict, VERDICT_VALID_ACK_V1);
++    valid = check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true);
++
+     assert_true(valid);
+ 
++    /* Our handwindow is 30 so the slices are half of that, so they are
++     * (975,990), (990, 1005), (1005, 1020), (1020, 1035), (1035, 1050)
++     * So setting time to the two future ones should work
++     */
++    now = 980;
++    assert_false(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++    now = 1040;
++    assert_false(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++    now = 1002;
++    assert_true(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++    now = 1022;
++    assert_true(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++    now = 1010;
++    assert_true(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++
++    /* Changing the IP address should make this invalid */
++    from.dest.addr.in4.sin_addr.s_addr = ntohl(0x01020305);
++    assert_false(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++
++    /* Change to the correct one again */
++    from.dest.addr.in4.sin_addr.s_addr = ntohl(0x01020304);
++    assert_true(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++
++    /* Modify the peer id, should now fail hmac verification */
++    buf_inc_len(&buf, -4);
++    buf_write_u32(&buf, 0x12345678);
++
++    free_tls_pre_decrypt_state(&state);
++    verdict = tls_pre_decrypt_lite(&tas, &state, &from, &buf);
++    assert_int_equal(verdict, VERDICT_VALID_ACK_V1);
++    assert_false(check_session_hmac_and_pkt_id(&state, &from.dest, hmac, 30, true));
++
+     free_tls_pre_decrypt_state(&state);
+     free_buf(&buf);
+     hmac_ctx_cleanup(hmac);
+@@ -723,12 +779,12 @@ int
+ main(void)
+ {
+     const struct CMUnitTest tests[] = {
++        cmocka_unit_test(test_verify_hmac_none),
+         cmocka_unit_test(test_tls_decrypt_lite_none),
+         cmocka_unit_test(test_tls_decrypt_lite_auth),
+         cmocka_unit_test(test_tls_decrypt_lite_crypt),
+         cmocka_unit_test(test_parse_ack),
+         cmocka_unit_test(test_calc_session_id_hmac_static),
+-        cmocka_unit_test(test_verify_hmac_none),
+         cmocka_unit_test(test_verify_hmac_tls_auth),
+         cmocka_unit_test(test_verify_hmac_none_out_of_range_ack),
+         cmocka_unit_test(test_generate_reset_packet_plain),
diff -Nru openvpn-2.6.3/debian/patches/series openvpn-2.6.3/debian/patches/series
--- openvpn-2.6.3/debian/patches/series	2025-04-02 17:45:15.000000000 +0200
+++ openvpn-2.6.3/debian/patches/series	2025-11-26 22:54:51.000000000 +0100
@@ -1,6 +1,5 @@
 move_log_dir.patch
 auth-pam_libpam_so_filename.patch
-#debian_nogroup_for_sample_files.patch
 openvpn-pkcs11warn.patch
 systemd.patch
 fix-dangling-pointer-in-pkcs11.patch
@@ -11,3 +10,6 @@
 CVE-2024-5594.patch
 sample-keys-renew-10-years.patch
 CVE-2025-2704.patch
+CVE-2024-5594-regression-fix.patch
+check-message-id.patch
+CVE-2025-13086.patch
diff -Nru openvpn-2.6.3/debian/salsa-ci.yml openvpn-2.6.3/debian/salsa-ci.yml
--- openvpn-2.6.3/debian/salsa-ci.yml	2025-04-02 17:45:15.000000000 +0200
+++ openvpn-2.6.3/debian/salsa-ci.yml	2025-11-26 22:54:51.000000000 +0100
@@ -5,3 +5,12 @@
 
 variables:
   RELEASE: 'bookworm'
+  SALSA_CI_DISABLE_GBP_SETUP_GITATTRIBUTES: 1
+  # reprotest can only run on sid and doesn't respect $RELEASE so it's
+  # completely broken for openvpn and we gain nothing by running it
+  SALSA_CI_DISABLE_REPROTEST: 1
+
+# salsa-ci runs on lintian from unstable, it dislikes lots of things from the
+# bookworm era :-)
+lintian:
+  allow_failure: true
diff -Nru openvpn-2.6.3/debian/tests/control openvpn-2.6.3/debian/tests/control
--- openvpn-2.6.3/debian/tests/control	2025-04-02 17:45:15.000000000 +0200
+++ openvpn-2.6.3/debian/tests/control	2025-11-26 22:54:51.000000000 +0100
@@ -4,3 +4,7 @@
 
 Tests: server-setup-with-static-key
 Restrictions: needs-root, isolation-machine, allow-stderr
+
+Tests: unit-tests
+Depends: libcmocka-dev, @builddeps@
+Restrictions: allow-stderr
diff -Nru openvpn-2.6.3/debian/tests/unit-tests openvpn-2.6.3/debian/tests/unit-tests
--- openvpn-2.6.3/debian/tests/unit-tests	1970-01-01 01:00:00.000000000 +0100
+++ openvpn-2.6.3/debian/tests/unit-tests	2025-11-26 22:54:51.000000000 +0100
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -ex
+
+# This test sets up and builds the openvpn, then runs the unit tests.
+
+sed -i -e 's/--disable-unit-tests/--enable-unit-tests/' debian/rules
+
+dh_update_autotools_config
+dh_autoreconf
+debian/rules override_dh_auto_configure
+debian/rules override_dh_auto_build
+
+make -C tests/unit_tests check

Attachment: signature.asc
Description: PGP signature

Reply via email to