#7 epel9: Backport upstream fix for CVE-2023-48795
Merged 2 months ago by gsauthof. Opened 3 months ago by ktdreyer.
rpms/ ktdreyer/python-asyncssh rhbz2255043  into  epel9

standardize patches
Ken Dreyer • 3 months ago  
@@ -0,0 +1,22 @@ 

+ From 69beb38067e13a3770b35012b71b10d67a35f50e Mon Sep 17 00:00:00 2001

+ From: Georg Sauthoff <mail@gms.tf>

+ Date: Mon, 19 Feb 2024 16:35:30 -0500

+ Subject: [PATCH] fix stdout asynio test case

Can you fix the subject?

+ 

+ ---

+  tests/test_process.py | 2 +-

+  1 file changed, 1 insertion(+), 1 deletion(-)

+ 

+ diff --git a/tests/test_process.py b/tests/test_process.py

+ index 11cde06..4026c0e 100644

+ --- a/tests/test_process.py

+ +++ b/tests/test_process.py

+ @@ -902,7 +902,7 @@ class _TestProcessRedirection(_TestProcess):

+              proc1.stdin.write(data)

+              proc1.stdin.write_eof()

+  

+ -            stdout_data, _ = await proc2.communicate()

+ +            stdout_data = await proc2.stdout.read()

+  

+          self.assertEqual(stdout_data, data.encode('ascii'))

+  

0002-Harden-AsyncSSH-state-machine-against-message-inject.patch 0001-Harden-AsyncSSH-state-machine-against-message-injection-during-handshake.patch
file renamed
+3 -6
@@ -1,4 +1,4 @@ 

- From 3f60aece86ff691f6b49557288cd424e0ba386b7 Mon Sep 17 00:00:00 2001

+ From fa128656ffb60331fc74ef714385f792b75e7c1f Mon Sep 17 00:00:00 2001

  From: Ron Frederick <ronf@timeheart.net>

  Date: Wed, 8 Nov 2023 18:06:33 -0800

  Subject: [PATCH] Harden AsyncSSH state machine against message injection
@@ -21,8 +21,8 @@ 

  

  (cherry picked from commit 83e43f5ea3470a8617fc388c72b062c7136efd7e)

  ---

-  asyncssh/connection.py   | 132 +++++++++++++++++++++-------------

-  tests/test_connection.py | 151 ++++++++++++++++++++++++++++++++-------

+  asyncssh/connection.py   | 132 ++++++++++++++++++++++++++---------------

+  tests/test_connection.py | 151 ++++++++++++++++++++++++++++++++++++++---------

   2 files changed, 207 insertions(+), 76 deletions(-)

  

  diff --git a/asyncssh/connection.py b/asyncssh/connection.py
@@ -504,6 +504,3 @@ 

   

       @asynctest

       async def test_unexpected_userauth_banner(self):

- -- 

- 2.43.0

- 

@@ -0,0 +1,401 @@ 

+ From b390b11fec2e4904e2aef1c171e132cc4602a959 Mon Sep 17 00:00:00 2001

+ From: Ron Frederick <ronf@timeheart.net>

+ Date: Mon, 18 Dec 2023 07:41:57 -0800

+ Subject: [PATCH] Implement "strict kex" support to harden AsyncSSH against

+  Terrapin Attack

+ MIME-Version: 1.0

+ Content-Type: text/plain; charset=UTF-8

+ Content-Transfer-Encoding: 8bit

+ 

+ This commit implements "strict kex" support and other countermeasures to

+ protect against the Terrapin Attack described in CVE-2023-48795. Thanks

+ once again go to Fabian Bäumer, Marcus Brinkmann, and Jörg Schwenk for

+ identifying and reporting this vulnerability and providing detailed

+ analysis and suggestions about proposed fixes.

+ 

+ (cherry picked from commit 0bc73254f41acb140187e0c89606311f88de5b7b)

+ 

+ Resolves: rhbz#2255043

+ ---

+  asyncssh/connection.py        |  67 +++++++++++++++----

+  tests/test_connection.py      | 149 ++++++++++++++++++++++++++++++++++++------

+  tests/test_connection_auth.py |   4 +-

+  tests/util.py                 |  14 ++++

+  4 files changed, 200 insertions(+), 34 deletions(-)

+ 

+ diff --git a/asyncssh/connection.py b/asyncssh/connection.py

+ index 3012f26..e74020b 100644

+ --- a/asyncssh/connection.py

+ +++ b/asyncssh/connection.py

+ @@ -817,6 +817,7 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):

+          self._kexinit_sent = False

+          self._kex_complete = False

+          self._ignore_first_kex = False

+ +        self._strict_kex = False

+  

+          self._gss: Optional[GSSBase] = None

+          self._gss_kex = False

+ @@ -1354,10 +1355,13 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):

+              (alg_type, b','.join(local_algs).decode('ascii'),

+               b','.join(remote_algs).decode('ascii')))

+  

+ -    def _get_ext_info_kex_alg(self) -> List[bytes]:

+ -        """Return the kex alg to add if any to request extension info"""

+ +    def _get_extra_kex_algs(self) -> List[bytes]:

+ +        """Return the extra kex algs to add"""

+  

+ -        return [b'ext-info-c' if self.is_client() else b'ext-info-s']

+ +        if self.is_client():

+ +            return [b'ext-info-c', b'kex-strict-c-v00@openssh.com']

+ +        else:

+ +            return [b'ext-info-s', b'kex-strict-s-v00@openssh.com']

+  

+      def _send(self, data: bytes) -> None:

+          """Send data to the SSH connection"""

+ @@ -1502,6 +1506,11 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):

+              else:

+                  skip_reason = 'kex not in progress'

+                  exc_reason = 'Key exchange not in progress'

+ +        elif self._strict_kex and not self._recv_encryption and \

+ +                MSG_IGNORE <= pkttype <= MSG_DEBUG:

+ +            skip_reason = 'strict kex violation'

+ +            exc_reason = 'Strict key exchange violation: ' \

+ +                         'unexpected packet type %d received' % pkttype

+          elif MSG_USERAUTH_FIRST <= pkttype <= MSG_USERAUTH_LAST:

+              if self._auth:

+                  handler = self._auth

+ @@ -1537,8 +1546,13 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):

+                  raise ProtocolError(str(exc)) from None

+  

+              if not processed:

+ -                self.logger.debug1('Unknown packet type %d received', pkttype)

+ -                self.send_packet(MSG_UNIMPLEMENTED, UInt32(seq))

+ +                if self._strict_kex and not self._recv_encryption:

+ +                    exc_reason = 'Strict key exchange violation: ' \

+ +                                 'unexpected packet type %d received' % pkttype

+ +                else:

+ +                    self.logger.debug1('Unknown packet type %d received',

+ +                                       pkttype)

+ +                    self.send_packet(MSG_UNIMPLEMENTED, UInt32(seq))

+  

+          if exc_reason:

+              raise ProtocolError(exc_reason)

+ @@ -1547,9 +1561,16 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):

+              self._auth_final = True

+  

+          if self._transport:

+ -            self._recv_seq = (seq + 1) & 0xffffffff

+              self._recv_handler = self._recv_pkthdr

+  

+ +            if self._recv_seq == 0xffffffff and not self._recv_encryption:

+ +                raise ProtocolError('Sequence rollover before kex complete')

+ +

+ +            if pkttype == MSG_NEWKEYS and self._strict_kex:

+ +                self._recv_seq = 0

+ +            else:

+ +                self._recv_seq = (seq + 1) & 0xffffffff

+ +

+          return True

+  

+      def send_packet(self, pkttype: int, *args: bytes,

+ @@ -1601,7 +1622,15 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):

+              mac = b''

+  

+          self._send(packet + mac)

+ -        self._send_seq = (seq + 1) & 0xffffffff

+ +

+ +        if self._send_seq == 0xffffffff and not self._send_encryption:

+ +            self._send_seq = 0

+ +            raise ProtocolError('Sequence rollover before kex complete')

+ +

+ +        if pkttype == MSG_NEWKEYS and self._strict_kex:

+ +            self._send_seq = 0

+ +        else:

+ +            self._send_seq = (seq + 1) & 0xffffffff

+  

+          if self._kex_complete:

+              self._rekey_bytes_sent += pktlen

+ @@ -1645,7 +1674,7 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):

+  

+          kex_algs = expand_kex_algs(self._kex_algs, gss_mechs,

+                                     bool(self._server_host_key_algs)) + \

+ -                   self._get_ext_info_kex_alg()

+ +                   self._get_extra_kex_algs()

+  

+          host_key_algs = self._server_host_key_algs or [b'null']

+  

+ @@ -2147,13 +2176,27 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):

+          if self.is_server():

+              self._client_kexinit = packet.get_consumed_payload()

+  

+ -            if b'ext-info-c' in peer_kex_algs and not self._session_id:

+ -                self._can_send_ext_info = True

+ +            if not self._session_id:

+ +                if b'ext-info-c' in peer_kex_algs:

+ +                    self._can_send_ext_info = True

+ +

+ +                if b'kex-strict-c-v00@openssh.com' in peer_kex_algs:

+ +                    self._strict_kex = True

+          else:

+              self._server_kexinit = packet.get_consumed_payload()

+  

+ -            if b'ext-info-s' in peer_kex_algs and not self._session_id:

+ -                self._can_send_ext_info = True

+ +            if not self._session_id:

+ +                if b'ext-info-s' in peer_kex_algs:

+ +                    self._can_send_ext_info = True

+ +

+ +                if b'kex-strict-s-v00@openssh.com' in peer_kex_algs:

+ +                    self._strict_kex = True

+ +

+ +        if self._strict_kex and not self._recv_encryption and \

+ +                self._recv_seq != 0:

+ +            raise ProtocolError('Strict key exchange violation: '

+ +                                'KEXINIT was not the first packet')

+ +

+  

+          if self._kexinit_sent:

+              self._kexinit_sent = False

+ diff --git a/tests/test_connection.py b/tests/test_connection.py

+ index 1b3a945..75e02bc 100644

+ --- a/tests/test_connection.py

+ +++ b/tests/test_connection.py

+ @@ -30,9 +30,10 @@ import unittest

+  from unittest.mock import patch

+  

+  import asyncssh

+ -from asyncssh.constants import MSG_DEBUG

+ +from asyncssh.constants import MSG_IGNORE, MSG_DEBUG

+  from asyncssh.constants import MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT

+ -from asyncssh.constants import MSG_KEXINIT, MSG_NEWKEYS, MSG_KEX_FIRST

+ +from asyncssh.constants import MSG_KEXINIT, MSG_NEWKEYS

+ +from asyncssh.constants import MSG_KEX_FIRST, MSG_KEX_LAST

+  from asyncssh.constants import MSG_USERAUTH_REQUEST, MSG_USERAUTH_SUCCESS

+  from asyncssh.constants import MSG_USERAUTH_FAILURE, MSG_USERAUTH_BANNER

+  from asyncssh.constants import MSG_USERAUTH_FIRST

+ @@ -43,6 +44,7 @@ from asyncssh.compression import get_compression_algs

+  from asyncssh.crypto.cipher import GCMCipher

+  from asyncssh.encryption import get_encryption_algs

+  from asyncssh.kex import get_kex_algs

+ +from asyncssh.kex_dh import MSG_KEX_ECDH_REPLY

+  from asyncssh.mac import _HMAC, _mac_handler, get_mac_algs

+  from asyncssh.packet import Boolean, NameList, String, UInt32

+  from asyncssh.public_key import get_default_public_key_algs

+ @@ -51,8 +53,8 @@ from asyncssh.public_key import get_default_x509_certificate_algs

+  

+  from .server import Server, ServerTestCase

+  

+ -from .util import asynctest, gss_available, nc_available, patch_gss

+ -from .util import patch_getnameinfo, x509_available

+ +from .util import asynctest, patch_extra_kex, patch_getnameinfo, patch_gss

+ +from .util import gss_available, nc_available, x509_available

+  

+  

+  class _CheckAlgsClientConnection(asyncssh.SSHClientConnection):

+ @@ -930,22 +932,6 @@ class _TestConnection(ServerTestCase):

+              with self.assertRaises(asyncssh.KeyExchangeFailed):

+                  await self.connect(kex_algs=['fail'])

+  

+ -    @asynctest

+ -    async def test_skip_ext_info(self):

+ -        """Test not requesting extension info from the server"""

+ -

+ -        def skip_ext_info(self):

+ -            """Don't request extension information"""

+ -

+ -            # pylint: disable=unused-argument

+ -

+ -            return []

+ -

+ -        with patch('asyncssh.connection.SSHConnection._get_ext_info_kex_alg',

+ -                   skip_ext_info):

+ -            async with self.connect():

+ -                pass

+ -

+      @asynctest

+      async def test_unknown_ext_info(self):

+          """Test receiving unknown extension information"""

+ @@ -970,6 +956,54 @@ class _TestConnection(ServerTestCase):

+              with self.assertRaises(asyncssh.ProtocolError):

+                  await self.connect()

+  

+ +    @asynctest

+ +    async def test_message_before_kexinit_strict_kex(self):

+ +        """Test receiving a message before KEXINIT with strict_kex enabled"""

+ +

+ +        def send_packet(self, pkttype, *args, **kwargs):

+ +            if pkttype == MSG_KEXINIT:

+ +                self.send_packet(MSG_IGNORE, String(b''))

+ +

+ +            asyncssh.connection.SSHConnection.send_packet(

+ +                self, pkttype, *args, **kwargs)

+ +

+ +        with patch('asyncssh.connection.SSHClientConnection.send_packet',

+ +                   send_packet):

+ +            with self.assertRaises(asyncssh.ProtocolError):

+ +                await self.connect()

+ +

+ +    @asynctest

+ +    async def test_message_during_kex_strict_kex(self):

+ +        """Test receiving an unexpected message with strict_kex enabled"""

+ +

+ +        def send_packet(self, pkttype, *args, **kwargs):

+ +            if pkttype == MSG_KEX_ECDH_REPLY:

+ +                self.send_packet(MSG_IGNORE, String(b''))

+ +

+ +            asyncssh.connection.SSHConnection.send_packet(

+ +                self, pkttype, *args, **kwargs)

+ +

+ +        with patch('asyncssh.connection.SSHServerConnection.send_packet',

+ +                   send_packet):

+ +            with self.assertRaises(asyncssh.ProtocolError):

+ +                await self.connect()

+ +

+ +    @asynctest

+ +    async def test_unknown_message_during_kex_strict_kex(self):

+ +        """Test receiving an unknown message with strict_kex enabled"""

+ +

+ +        def send_packet(self, pkttype, *args, **kwargs):

+ +            if pkttype == MSG_KEX_ECDH_REPLY:

+ +                self.send_packet(MSG_KEX_LAST)

+ +

+ +            asyncssh.connection.SSHConnection.send_packet(

+ +                self, pkttype, *args, **kwargs)

+ +

+ +        with patch('asyncssh.connection.SSHServerConnection.send_packet',

+ +                   send_packet):

+ +            with self.assertRaises(asyncssh.ProtocolError):

+ +                await self.connect()

+ +

+      @asynctest

+      async def test_encryption_algs(self):

+          """Test connecting with different encryption algorithms"""

+ @@ -1602,6 +1636,81 @@ class _TestConnection(ServerTestCase):

+              await self.create_connection(_InternalErrorClient)

+  

+  

+ +@patch_extra_kex

+ +class _TestConnectionNoStrictKex(ServerTestCase):

+ +    """Unit tests for connection API with ext info and strict kex disabled"""

+ +

+ +    @classmethod

+ +    async def start_server(cls):

+ +        """Start an SSH server to connect to"""

+ +

+ +        return (await cls.create_server(_TunnelServer, gss_host=(),

+ +                                        compression_algs='*',

+ +                                        encryption_algs='*',

+ +                                        kex_algs='*', mac_algs='*'))

+ +

+ +    @asynctest

+ +    async def test_skip_ext_info(self):

+ +        """Test not requesting extension info from the server"""

+ +

+ +        async with self.connect():

+ +            pass

+ +

+ +    @asynctest

+ +    async def test_message_before_kexinit(self):

+ +        """Test receiving a message before KEXINIT"""

+ +

+ +        def send_packet(self, pkttype, *args, **kwargs):

+ +            if pkttype == MSG_KEXINIT:

+ +                self.send_packet(MSG_IGNORE, String(b''))

+ +

+ +            asyncssh.connection.SSHConnection.send_packet(

+ +                self, pkttype, *args, **kwargs)

+ +

+ +        with patch('asyncssh.connection.SSHClientConnection.send_packet',

+ +                   send_packet):

+ +            async with self.connect():

+ +                pass

+ +

+ +    @asynctest

+ +    async def test_message_during_kex(self):

+ +        """Test receiving an unexpected message in key exchange"""

+ +

+ +        def send_packet(self, pkttype, *args, **kwargs):

+ +            if pkttype == MSG_KEX_ECDH_REPLY:

+ +                self.send_packet(MSG_IGNORE, String(b''))

+ +

+ +            asyncssh.connection.SSHConnection.send_packet(

+ +                self, pkttype, *args, **kwargs)

+ +

+ +        with patch('asyncssh.connection.SSHServerConnection.send_packet',

+ +                   send_packet):

+ +            async with self.connect():

+ +                pass

+ +

+ +    @asynctest

+ +    async def test_sequence_wrap_during_kex(self):

+ +        """Test sequence wrap during initial key exchange"""

+ +

+ +        def send_packet(self, pkttype, *args, **kwargs):

+ +            if pkttype == MSG_KEXINIT:

+ +                if self._options.command == 'send':

+ +                    self._send_seq = 0xfffffffe

+ +                else:

+ +                    self._recv_seq = 0xfffffffe

+ +

+ +            asyncssh.connection.SSHConnection.send_packet(

+ +                self, pkttype, *args, **kwargs)

+ +

+ +        with patch('asyncssh.connection.SSHClientConnection.send_packet',

+ +                   send_packet):

+ +            with self.assertRaises(asyncssh.ProtocolError):

+ +                await self.connect(command='send')

+ +

+ +            with self.assertRaises(asyncssh.ProtocolError):

+ +                await self.connect(command='recv')

+ +

+ +

+  class _TestConnectionListenSock(ServerTestCase):

+      """Unit test for specifying a listen socket"""

+  

+ diff --git a/tests/test_connection_auth.py b/tests/test_connection_auth.py

+ index 822c418..75c8c47 100644

+ --- a/tests/test_connection_auth.py

+ +++ b/tests/test_connection_auth.py

+ @@ -739,7 +739,7 @@ class _TestHostBasedAuth(ServerTestCase):

+  

+              return []

+  

+ -        with patch('asyncssh.connection.SSHConnection._get_ext_info_kex_alg',

+ +        with patch('asyncssh.connection.SSHConnection._get_extra_kex_algs',

+                     skip_ext_info):

+              try:

+                  async with self.connect(username='user',

+ @@ -1245,7 +1245,7 @@ class _TestPublicKeyAuth(ServerTestCase):

+  

+              return []

+  

+ -        with patch('asyncssh.connection.SSHConnection._get_ext_info_kex_alg',

+ +        with patch('asyncssh.connection.SSHConnection._get_extra_kex_algs',

+                     skip_ext_info):

+              try:

+                  async with self.connect(username='ckey', client_keys='ckey',

+ diff --git a/tests/util.py b/tests/util.py

+ index a869944..503aa63 100644

+ --- a/tests/util.py

+ +++ b/tests/util.py

+ @@ -112,6 +112,20 @@ def patch_getnameinfo(cls):

+      return patch('socket.getnameinfo', getnameinfo)(cls)

+  

+  

+ +def patch_extra_kex(cls):

+ +    """Decorator for skipping extra kex algs"""

+ +

+ +    def skip_extra_kex_algs(self):

+ +        """Don't send extra key exchange algorithms"""

+ +

+ +        # pylint: disable=unused-argument

+ +

+ +        return []

+ +

+ +    return patch('asyncssh.connection.SSHConnection._get_extra_kex_algs',

+ +                 skip_extra_kex_algs)(cls)

+ +

+ +

+  def patch_gss(cls):

+      """Decorator for patching GSSAPI classes"""

+  

file modified
+9 -3
@@ -6,15 +6,18 @@ 

  

  Name:           python-%{srcname}

  Version:        2.13.2

- Release:        4%{?dist}

+ Release:        5%{?dist}

  Summary:        Asynchronous SSH for Python

  

  License:        EPL-2.0 or GPLv2+

  URL:            https://github.com/ronf/asyncssh

  Source0:        %pypi_source

- Patch0:         test_stdout_stream.diff

+ 

+ Patch0001: 0001-fix-stdout-asynio-test-case.patch

+ Patch0002: 0002-Harden-AsyncSSH-state-machine-against-message-inject.patch

+ Patch0003: 0003-Implement-strict-kex-support-to-harden-AsyncSSH-agai.patch

+ 

  # https://github.com/ronf/asyncssh/commit/83e43f5ea3470a8617fc388c72b062c7136efd7e

- Patch1:         0001-Harden-AsyncSSH-state-machine-against-message-injection-during-handshake.patch

  

  

  BuildArch:      noarch
@@ -83,6 +86,9 @@ 

  

  

  %changelog

+ * Mon Feb 19 2024 Ken Dreyer <kdreyer@ibm.com> 2.13.2-5

+ - Backport upstream fix for CVE-2023-48795 (rhbz#2255043)

+ 

  * Mon Jan 22 2024 Carl George <carlwgeorge@fedoraproject.org> - 2.13.2-4

  - Backport upstream fix for CVE-2023-46445 and CVE-2023-46446

  - Resolves: rhbz#2250327 rhbz#2250330

file removed
-11
@@ -1,11 +0,0 @@ 

- --- a/tests/test_process.py	2023-07-11 19:34:30.406758533 +0200

- +++ b/tests/test_process.py	2023-07-11 19:34:54.702860332 +0200

- @@ -902,7 +902,7 @@

-              proc1.stdin.write(data)

-              proc1.stdin.write_eof()

-  

- -            stdout_data, _ = await proc2.communicate()

- +            stdout_data = await proc2.stdout.read()

-  

-          self.assertEqual(stdout_data, data.encode('ascii'))

-  

Resolves: rhbz#2255043

(I also pushed a prerequisite commit to update the other two preexisting patch files. I used rdopkg update-patches to standardize the patch file formats and numbering. This makes it easier to add more patches with rdopkg.)

Since we are increasing the number of cherry-picks, I've pushed the full patch series to a new epel9-patches branch, here: https://github.com/ktdreyer/asyncssh/commits/epel9-patches

I used the subject line you wrote in the %changelog entry.

What subject line would you would prefer for that patch?

Well, I mean this typo:

Subject: [PATCH] fix stdout asynio test case

which should read:

Subject: [PATCH] fix stdout asyncio test case

I introduced it in the git commit message here:

commit 34629a2a7558fa54e8399abfd2956cccb6498b9f
Author: Georg Sauthoff <mail@gms.tf>
Date:   Tue Jul 11 20:00:19 2023 +0200

    fix stdout asynio test case

I thought that there is no need to keep it when you cherry pick it.

However, not a big deal either. One could argue for consistency when grepping for it - but on the other hand the commit hash also changed due to rebasing.

Pull-Request has been merged by gsauthof

2 months ago