Hi! On Tue, Dec 19, 2023 at 09:31:00AM +0100, Salvatore Bonaccorso wrote: >Source: python-asyncssh >Version: 2.10.1-2 >Severity: important >Tags: security upstream >X-Debbugs-Cc: car...@debian.org, Debian Security Team ><t...@security.debian.org> > >Hi, > >The following vulnerability was published for python-asyncssh. > >CVE-2023-48795[0]: >| The SSH transport protocol with certain OpenSSH extensions, found in >| OpenSSH before 9.6 and other products, allows remote attackers to >| bypass integrity checks such that some packets are omitted (from the >| extension negotiation message), and a client and server may >| consequently end up with a connection for which some security >| features have been downgraded or disabled, aka a Terrapin attack. >| This occurs because the SSH Binary Packet Protocol (BPP), >| implemented by these extensions, mishandles the handshake phase and >| mishandles use of sequence numbers. For example, there is an >| effective attack against SSH's use of ChaCha20-Poly1305 (and CBC >| with Encrypt-then-MAC). The bypass occurs in >| chacha20-poly1...@openssh.com and (if CBC is used) the >| -e...@openssh.com MAC algorithms. This also affects Maverick Synergy >| Java SSH API before 3.1.0-SNAPSHOT, Dropbear through 2022.83, Ssh >| before 5.1.1 in Erlang/OTP, PuTTY before 0.80, AsyncSSH before >| 2.14.2, golang.org/x/crypto before 0.17.0, libssh before 0.10.6, and >| libssh2 through 1.11.0; and there could be effects on Bitvise SSH >| through 9.31.
We wanted this fixed in Pexip, so I've taken a look at this bug. The upstream bugfix just needs a small rework so it applies cleanly to the version in bookworm. Here's a debdiff for that that in case it's useful. -- Steve McIntyre, Cambridge, UK. st...@einval.com Into the distance, a ribbon of black Stretched to the point of no turning back
diff -Nru python-asyncssh-2.10.1/debian/changelog python-asyncssh-2.10.1/debian/changelog --- python-asyncssh-2.10.1/debian/changelog 2022-12-22 03:54:16.000000000 +0000 +++ python-asyncssh-2.10.1/debian/changelog 2024-04-29 11:45:47.000000000 +0100 @@ -1,3 +1,11 @@ +python-asyncssh (2.10.1-2+deb12u1) bookworm; urgency=medium + + * Apply and tweak upstream security fix for CVE-2023-48795 + Implement "strict kex" support to harden AsyncSSH against Terrapin + Attack. Closes: #1059007 + + -- Steve McIntyre <steve.mcint...@pexip.com> Mon, 29 Apr 2024 11:45:47 +0100 + python-asyncssh (2.10.1-2) unstable; urgency=medium * Team Upload. diff -Nru python-asyncssh-2.10.1/debian/patches/CVE-2023-48795.patch python-asyncssh-2.10.1/debian/patches/CVE-2023-48795.patch --- python-asyncssh-2.10.1/debian/patches/CVE-2023-48795.patch 1970-01-01 01:00:00.000000000 +0100 +++ python-asyncssh-2.10.1/debian/patches/CVE-2023-48795.patch 2024-04-29 11:45:47.000000000 +0100 @@ -0,0 +1,382 @@ +commit 0bc73254f41acb140187e0c89606311f88de5b7b +Author: Ron Frederick <r...@timeheart.net> +Date: Mon Dec 18 07:41:57 2023 -0800 + + Implement "strict kex" support to harden AsyncSSH against Terrapin Attack + + 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. + +Index: b/asyncssh/connection.py +=================================================================== +--- a/asyncssh/connection.py ++++ b/asyncssh/connection.py +@@ -810,6 +810,7 @@ class SSHConnection(SSHPacketHandler, as + 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 +@@ -1343,10 +1344,13 @@ class SSHConnection(SSHPacketHandler, as + (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-...@openssh.com'] ++ else: ++ return [b'ext-info-s', b'kex-strict-s-...@openssh.com'] + + def _send(self, data: bytes) -> None: + """Send data to the SSH connection""" +@@ -1487,6 +1491,11 @@ class SSHConnection(SSHPacketHandler, as + self._ignore_first_kex = False + else: + handler = self._kex ++ 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 (self._auth and + MSG_USERAUTH_FIRST <= pkttype <= MSG_USERAUTH_LAST): + handler = self._auth +@@ -1516,15 +1525,26 @@ class SSHConnection(SSHPacketHandler, as + 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) + + 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 + +@@ -1579,7 +1599,15 @@ class SSHConnection(SSHPacketHandler, as + 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 +@@ -1623,7 +1651,7 @@ class SSHConnection(SSHPacketHandler, as + + 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'] + +@@ -2106,13 +2134,26 @@ class SSHConnection(SSHPacketHandler, as + 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-...@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-...@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 +Index: b/tests/test_connection.py +=================================================================== +--- a/tests/test_connection.py ++++ b/tests/test_connection.py +@@ -31,9 +31,10 @@ import unittest + from unittest.mock import patch + + import asyncssh +-from asyncssh.constants import MSG_UNIMPLEMENTED, MSG_DEBUG ++from asyncssh.constants import MSG_UNIMPLEMENTED, MSG_DEBUG, MSG_IGNORE + from asyncssh.constants import MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT + 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_GLOBAL_REQUEST +@@ -43,6 +44,7 @@ from asyncssh.compression import get_com + 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_defa + + from .server import Server, ServerTestCase + +-from .util import asynctest, gss_available, patch_gss, run +-from .util import patch_getnameinfo, x509_available ++from .util import asynctest, patch_extra_kex, patch_getnameinfo, patch_gss, run ++from .util import gss_available, x509_available + + + try: +@@ -901,22 +903,6 @@ class _TestConnection(ServerTestCase): + 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""" + +@@ -941,6 +927,54 @@ class _TestConnection(ServerTestCase): + pass + + @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""" + +@@ -1468,6 +1502,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 _TestConnectionAsyncAcceptor(ServerTestCase): + """Unit test for async acceptor""" + +Index: b/tests/test_connection_auth.py +=================================================================== +--- a/tests/test_connection_auth.py ++++ b/tests/test_connection_auth.py +@@ -710,7 +710,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): + async with self.connect(username='user', client_host_keys='skey', + client_username='user'): +@@ -1209,7 +1209,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): + async with self.connect(username='ckey', client_keys='ckey', + agent_path=None): +Index: b/tests/util.py +=================================================================== +--- a/tests/util.py ++++ b/tests/util.py +@@ -96,6 +96,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""" + diff -Nru python-asyncssh-2.10.1/debian/patches/series python-asyncssh-2.10.1/debian/patches/series --- python-asyncssh-2.10.1/debian/patches/series 2022-12-22 03:52:46.000000000 +0000 +++ python-asyncssh-2.10.1/debian/patches/series 2024-04-29 11:45:47.000000000 +0100 @@ -3,3 +3,4 @@ 0003-Revert-fido-0.9.2-support.patch 0004-Handle-ConnectionRefusedError-when-connecting-to-223.patch mock-pathlib-expanduser.patch +CVE-2023-48795.patch