diff -Nru paramiko-2.6.0/debian/changelog paramiko-2.6.0/debian/changelog --- paramiko-2.6.0/debian/changelog 2022-03-24 13:25:44.000000000 +0000 +++ paramiko-2.6.0/debian/changelog 2024-01-12 12:30:05.000000000 +0000 @@ -1,3 +1,18 @@ +paramiko (2.6.0-2ubuntu0.3) focal-security; urgency=medium + + * SECURITY UPDATE: Prefix truncation attack on BPP + - debian/patches/CVE-2023-48795-*.patch: implement strict key + exchange. + - debian/patches/fix_test_on_armhf.patch: fix test failing on armhf. + - debian/patches/disable_flaky_test.patch: disable flaky + test_sequence_numbers_reset_on_newkeys_when_strict test. + - CVE-2023-48795 + * Enable test suite + - debian/rules: re-enable tests. + - debian/control: added python3-mock and python3-pytest to B-D. + + -- Marc Deslauriers Fri, 12 Jan 2024 07:30:05 -0500 + paramiko (2.6.0-2ubuntu0.1) focal-security; urgency=medium * SECURITY UPDATE: race condition in write_private_key_file diff -Nru paramiko-2.6.0/debian/control paramiko-2.6.0/debian/control --- paramiko-2.6.0/debian/control 2022-03-24 13:25:44.000000000 +0000 +++ paramiko-2.6.0/debian/control 2024-01-12 12:30:05.000000000 +0000 @@ -11,8 +11,10 @@ python3-cryptography (>= 2.5), python3-ecdsa (>= 0.11), python3-bcrypt (>= 3.1.3), + python3-mock , python3-nacl (>= 1.0.1), python3-pyasn1 (>= 0.1.7), + python3-pytest , python3-setuptools Standards-Version: 4.4.1 Homepage: https://github.com/paramiko/paramiko/ diff -Nru paramiko-2.6.0/debian/patches/CVE-2023-48795-2.patch paramiko-2.6.0/debian/patches/CVE-2023-48795-2.patch --- paramiko-2.6.0/debian/patches/CVE-2023-48795-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-2.6.0/debian/patches/CVE-2023-48795-2.patch 2024-01-12 12:30:05.000000000 +0000 @@ -0,0 +1,341 @@ +Backport of: + +From 773a174fb1e40e1d18dbe2625e16337ea401119e Mon Sep 17 00:00:00 2001 +From: Jeff Forcier +Date: Fri, 15 Dec 2023 23:59:12 -0500 +Subject: [PATCH] Basic strict-kex-mode agreement mechanics work + +--- + paramiko/transport.py | 38 +++++++++++++++++++++++++++++++++--- + tests/test_transport.py | 43 +++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 78 insertions(+), 3 deletions(-) + +--- a/paramiko/transport.py ++++ b/paramiko/transport.py +@@ -310,6 +310,7 @@ class Transport(threading.Thread, Closin + gss_kex=False, + gss_deleg_creds=True, + disabled_algorithms=None, ++ strict_kex=True, + ): + """ + Create a new SSH session over an existing socket, or socket-like +@@ -372,6 +373,10 @@ class Transport(threading.Thread, Closin + your code talks to a server which implements it differently from + Paramiko), specify ``disabled_algorithms={"kex": + ["diffie-hellman-group16-sha512"]}``. ++ :param bool strict_kex: ++ Whether to advertise (and implement, if client also advertises ++ support for) a "strict kex" mode for safer handshaking. Default: ++ ``True``. + + .. versionchanged:: 1.15 + Added the ``default_window_size`` and ``default_max_packet_size`` +@@ -383,6 +388,8 @@ class Transport(threading.Thread, Closin + """ + self.active = False + self.hostname = None ++ self.advertise_strict_kex = strict_kex ++ self.agreed_on_strict_kex = False + + if isinstance(sock, string_types): + # convert "host:port" into (host, port) +@@ -2253,6 +2260,7 @@ class Transport(threading.Thread, Closin + self.clear_to_send_lock.release() + self.gss_kex_used = False + self.in_kex = True ++ kex_algos = list(self.preferred_kex) + if self.server_mode: + mp_required_prefix = "diffie-hellman-group-exchange-sha" + kex_mp = [ +@@ -2278,10 +2286,16 @@ class Transport(threading.Thread, Closin + else: + available_server_keys = self.preferred_keys + ++ # Similar to ext-info, but used in both server modes, so done outside ++ # of above if/else. ++ if self.advertise_strict_kex: ++ which = "s" if self.server_mode else "c" ++ kex_algos.append(f"kex-strict-{which}-v00@openssh.com") ++ + m = Message() + m.add_byte(cMSG_KEXINIT) + m.add_bytes(os.urandom(16)) +- m.add_list(self.preferred_kex) ++ m.add_list(kex_algos) + m.add_list(available_server_keys) + m.add_list(self.preferred_ciphers) + m.add_list(self.preferred_ciphers) +@@ -2294,23 +2308,47 @@ class Transport(threading.Thread, Closin + m.add_boolean(False) + m.add_int(0) + # save a copy for later (needed to compute a hash) +- self.local_kex_init = m.asbytes() ++ self.local_kex_init = self._latest_kex_init = m.asbytes() + self._send_message(m) + +- def _parse_kex_init(self, m): ++ def _really_parse_kex_init(self, m, ignore_first_byte=False): ++ parsed = {} ++ if ignore_first_byte: ++ m.get_byte() + m.get_bytes(16) # cookie, discarded +- kex_algo_list = m.get_list() +- server_key_algo_list = m.get_list() +- client_encrypt_algo_list = m.get_list() +- server_encrypt_algo_list = m.get_list() +- client_mac_algo_list = m.get_list() +- server_mac_algo_list = m.get_list() +- client_compress_algo_list = m.get_list() +- server_compress_algo_list = m.get_list() +- client_lang_list = m.get_list() +- server_lang_list = m.get_list() +- kex_follows = m.get_boolean() ++ parsed["kex_algo_list"] = m.get_list() ++ parsed["server_key_algo_list"] = m.get_list() ++ parsed["client_encrypt_algo_list"] = m.get_list() ++ parsed["server_encrypt_algo_list"] = m.get_list() ++ parsed["client_mac_algo_list"] = m.get_list() ++ parsed["server_mac_algo_list"] = m.get_list() ++ parsed["client_compress_algo_list"] = m.get_list() ++ parsed["server_compress_algo_list"] = m.get_list() ++ parsed["client_lang_list"] = m.get_list() ++ parsed["server_lang_list"] = m.get_list() ++ parsed["kex_follows"] = m.get_boolean() + m.get_int() # unused ++ return parsed ++ ++ def _get_latest_kex_init(self): ++ return self._really_parse_kex_init( ++ Message(self._latest_kex_init), ++ ignore_first_byte=True, ++ ) ++ ++ def _parse_kex_init(self, m): ++ parsed = self._really_parse_kex_init(m) ++ kex_algo_list = parsed["kex_algo_list"] ++ server_key_algo_list = parsed["server_key_algo_list"] ++ client_encrypt_algo_list = parsed["client_encrypt_algo_list"] ++ server_encrypt_algo_list = parsed["server_encrypt_algo_list"] ++ client_mac_algo_list = parsed["client_mac_algo_list"] ++ server_mac_algo_list = parsed["server_mac_algo_list"] ++ client_compress_algo_list = parsed["client_compress_algo_list"] ++ server_compress_algo_list = parsed["server_compress_algo_list"] ++ client_lang_list = parsed["client_lang_list"] ++ server_lang_list = parsed["server_lang_list"] ++ kex_follows = parsed["kex_follows"] + + self._log( + DEBUG, +@@ -2338,6 +2376,25 @@ class Transport(threading.Thread, Closin + + str(kex_follows), + ) + ++ # Record, and strip out, strict-kex non-algorithms ++ self._remote_strict_kex = None ++ to_pop = [] ++ for i, algo in enumerate(kex_algo_list): ++ if algo.startswith("kex-strict-"): ++ # NOTE: this is what we are expecting from the /remote/ end. ++ which = "c" if self.server_mode else "s" ++ expected = f"kex-strict-{which}-v00@openssh.com" ++ # Set strict mode if agreed. ++ self.agreed_on_strict_kex = ( ++ algo == expected and self.advertise_strict_kex ++ ) ++ self._log( ++ DEBUG, f"Strict kex mode: {self.agreed_on_strict_kex}" ++ ) ++ to_pop.insert(0, i) ++ for i in to_pop: ++ kex_algo_list.pop(i) ++ + # as a server, we pick the first item in the client's list that we + # support. + # as a client, we pick the first item in our list that the server +--- a/tests/test_transport.py ++++ b/tests/test_transport.py +@@ -23,6 +23,8 @@ Some unit tests for the ssh2 protocol in + from __future__ import with_statement + + from binascii import hexlify ++from contextlib import contextmanager ++import itertools + import select + import socket + import time +@@ -60,6 +62,7 @@ from paramiko.message import Message + + from .util import needs_builtin, _support, slow + from .loop import LoopSocket ++from pytest import skip, mark + + + LONG_BANNER = """\ +@@ -80,6 +83,9 @@ class NullServer(ServerInterface): + paranoid_did_public_key = False + paranoid_key = DSSKey.from_private_key_file(_support("test_dss.key")) + ++ def __init__(self, allowed_keys=None): ++ self.allowed_keys = allowed_keys if allowed_keys is not None else [] ++ + def get_allowed_auths(self, username): + if username == "slowdive": + return "publickey,password" +@@ -90,6 +96,11 @@ class NullServer(ServerInterface): + return AUTH_SUCCESSFUL + return AUTH_FAILED + ++ def check_auth_publickey(self, username, key): ++ if key in self.allowed_keys: ++ return AUTH_SUCCESSFUL ++ return AUTH_FAILED ++ + def check_channel_request(self, kind, chanid): + if kind == "bogus": + return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED +@@ -1169,3 +1180,144 @@ class AlgorithmDisablingTests(unittest.T + assert "ssh-dss" not in server_keys + assert "diffie-hellman-group14-sha256" not in kexen + assert "zlib" not in compressions ++ ++ ++@contextmanager ++def server( ++ hostkey=None, ++ init=None, ++ server_init=None, ++ client_init=None, ++ connect=None, ++ pubkeys=None, ++ catch_error=False, ++ defer=False, ++ transport_factory=None, ++ server_transport_factory=None, ++): ++ """ ++ SSH server contextmanager for testing. ++ ++ :param hostkey: ++ Host key to use for the server; if None, loads ++ ``test_rsa.key``. ++ :param init: ++ Default `Transport` constructor kwargs to use for both sides. ++ :param server_init: ++ Extends and/or overrides ``init`` for server transport only. ++ :param client_init: ++ Extends and/or overrides ``init`` for client transport only. ++ :param connect: ++ Kwargs to use for ``connect()`` on the client. ++ :param pubkeys: ++ List of public keys for auth. ++ :param catch_error: ++ Whether to capture connection errors & yield from contextmanager. ++ Necessary for connection_time exception testing. ++ :param bool defer: ++ Whether to defer authentication during connecting. ++ :param transport_factory: ++ Like the same-named param in SSHClient: which Transport class to use. ++ :param server_transport_factory: ++ Like ``transport_factory``, but only impacts the server transport. ++ """ ++ if init is None: ++ init = {} ++ if server_init is None: ++ server_init = {} ++ if client_init is None: ++ client_init = {} ++ if connect is None: ++ # No auth at all please ++ if defer: ++ connect = dict() ++ # Default username based auth ++ else: ++ connect = dict(username="slowdive", password="pygmalion") ++ socks = LoopSocket() ++ sockc = LoopSocket() ++ sockc.link(socks) ++ if transport_factory is None: ++ transport_factory = Transport ++ if server_transport_factory is None: ++ server_transport_factory = transport_factory ++ tc = transport_factory(sockc, **dict(init, **client_init)) ++ ts = server_transport_factory(socks, **dict(init, **server_init)) ++ ++ if hostkey is None: ++ hostkey = RSAKey.from_private_key_file(_support("test_rsa.key")) ++ ts.add_server_key(hostkey) ++ event = threading.Event() ++ server = NullServer(allowed_keys=pubkeys) ++ assert not event.is_set() ++ assert not ts.is_active() ++ assert tc.get_username() is None ++ assert ts.get_username() is None ++ assert not tc.is_authenticated() ++ assert not ts.is_authenticated() ++ ++ err = None ++ # Trap errors and yield instead of raising right away; otherwise callers ++ # cannot usefully deal with problems at connect time which stem from errors ++ # in the server side. ++ try: ++ ts.start_server(event, server) ++ tc.connect(**connect) ++ ++ event.wait(1.0) ++ assert event.is_set() ++ assert ts.is_active() ++ assert tc.is_active() ++ ++ except Exception as e: ++ if not catch_error: ++ raise ++ err = e ++ ++ yield (tc, ts, err) if catch_error else (tc, ts) ++ ++ tc.close() ++ ts.close() ++ socks.close() ++ sockc.close() ++ ++ ++class TestStrictKex: ++ def test_kex_algos_includes_kex_strict_c(self): ++ with server() as (tc, _): ++ kex = tc._get_latest_kex_init() ++ assert "kex-strict-c-v00@openssh.com" in kex["kex_algo_list"] ++ ++ @mark.parametrize( ++ "server_active,client_active", ++ itertools.product([True, False], repeat=2), ++ ) ++ def test_mode_agreement(self, server_active, client_active): ++ with server( ++ server_init=dict(strict_kex=server_active), ++ client_init=dict(strict_kex=client_active), ++ ) as (tc, ts): ++ if server_active and client_active: ++ assert tc.agreed_on_strict_kex is True ++ assert ts.agreed_on_strict_kex is True ++ else: ++ assert tc.agreed_on_strict_kex is False ++ assert ts.agreed_on_strict_kex is False ++ ++ def test_mode_advertised_by_default(self): ++ # NOTE: no explicit strict_kex overrides... ++ with server() as (tc, ts): ++ assert all( ++ ( ++ tc.advertise_strict_kex, ++ tc.agreed_on_strict_kex, ++ ts.advertise_strict_kex, ++ ts.agreed_on_strict_kex, ++ ) ++ ) ++ ++ def test_sequence_numbers_reset_on_newkeys(self): ++ skip() ++ ++ def test_error_raised_on_out_of_order_handshakes(self): ++ skip() diff -Nru paramiko-2.6.0/debian/patches/CVE-2023-48795-3.patch paramiko-2.6.0/debian/patches/CVE-2023-48795-3.patch --- paramiko-2.6.0/debian/patches/CVE-2023-48795-3.patch 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-2.6.0/debian/patches/CVE-2023-48795-3.patch 2024-01-12 12:30:05.000000000 +0000 @@ -0,0 +1,105 @@ +Backport of: + +From f4dedacb9040d27d9844f51c81c28e0247d3e4a3 Mon Sep 17 00:00:00 2001 +From: Jeff Forcier +Date: Sat, 16 Dec 2023 13:02:05 -0500 +Subject: [PATCH] Raise new exception type when unexpected messages appear + +--- + paramiko/__init__.py | 1 + + paramiko/ssh_exception.py | 10 ++++++++++ + paramiko/transport.py | 6 +++++- + sites/www/changelog.rst | 8 +++++--- + tests/test_transport.py | 22 +++++++++++++++++++--- + 5 files changed, 40 insertions(+), 7 deletions(-) + +--- a/paramiko/__init__.py ++++ b/paramiko/__init__.py +@@ -43,6 +43,7 @@ from paramiko.ssh_exception import ( + BadHostKeyException, + AuthenticationException, + ProxyCommandFailure, ++ MessageOrderError, + ) + from paramiko.server import ServerInterface, SubsystemHandler, InteractiveQuery + from paramiko.rsakey import RSAKey +--- a/paramiko/ssh_exception.py ++++ b/paramiko/ssh_exception.py +@@ -196,3 +196,13 @@ class NoValidConnectionsError(socket.err + + def __reduce__(self): + return (self.__class__, (self.errors,)) ++ ++ ++class MessageOrderError(SSHException): ++ """ ++ Out-of-order protocol messages were received, violating "strict kex" mode. ++ ++ .. versionadded:: 3.4 ++ """ ++ ++ pass +--- a/paramiko/transport.py ++++ b/paramiko/transport.py +@@ -108,6 +108,7 @@ from paramiko.ssh_exception import ( + BadAuthenticationType, + ChannelException, + ProxyCommandFailure, ++ MessageOrderError, + ) + from paramiko.util import retry_on_signal, ClosingContextManager, clamp_value + +@@ -2072,7 +2073,10 @@ class Transport(threading.Thread, Closin + continue + if len(self._expected_packet) > 0: + if ptype not in self._expected_packet: +- raise SSHException( ++ exc_class = SSHException ++ if self.agreed_on_strict_kex: ++ exc_class = MessageOrderError ++ raise exc_class( + "Expecting packet from {!r}, got {:d}".format( + self._expected_packet, ptype + ) +--- a/tests/test_transport.py ++++ b/tests/test_transport.py +@@ -43,6 +43,7 @@ from paramiko import ( + SecurityOptions, + ServerInterface, + Transport, ++ MessageOrderError, + ) + from paramiko import AUTH_FAILED, AUTH_SUCCESSFUL + from paramiko import OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED +@@ -62,7 +63,7 @@ from paramiko.message import Message + + from .util import needs_builtin, _support, slow + from .loop import LoopSocket +-from pytest import skip, mark ++from pytest import skip, mark, raises + + + LONG_BANNER = """\ +@@ -1319,5 +1428,20 @@ class TestStrictKex: + def test_sequence_numbers_reset_on_newkeys(self): + skip() + +- def test_error_raised_on_out_of_order_handshakes(self): +- skip() ++ def test_MessageOrderError_raised_on_out_of_order_messages(self): ++ with raises(MessageOrderError): ++ with server() as (tc, _): ++ # A bit artificial as it's outside kexinit/handshake, but much ++ # easier to trigger and still in line with behavior under test ++ tc._expect_packet(MSG_KEXINIT) ++ tc.open_session() ++ ++ def test_SSHException_raised_on_out_of_order_messages_when_not_strict(self): ++ # This is kind of dumb (either situation is still fatal!) but whatever, ++ # may as well be strict with our new strict flag... ++ with raises(SSHException) as info: # would be true either way, but ++ with server(client_init=dict(strict_kex=False), ++ ) as (tc, _): ++ tc._expect_packet(MSG_KEXINIT) ++ tc.open_session() ++ assert info.type is SSHException # NOT MessageOrderError! diff -Nru paramiko-2.6.0/debian/patches/CVE-2023-48795-4.patch paramiko-2.6.0/debian/patches/CVE-2023-48795-4.patch --- paramiko-2.6.0/debian/patches/CVE-2023-48795-4.patch 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-2.6.0/debian/patches/CVE-2023-48795-4.patch 2024-01-12 12:30:05.000000000 +0000 @@ -0,0 +1,151 @@ +Backport of: + +From 75e311d3c0845a316b6e7b3fae2488d86ad5a270 Mon Sep 17 00:00:00 2001 +From: Jeff Forcier +Date: Sat, 16 Dec 2023 16:17:58 -0500 +Subject: [PATCH] Enforce zero seqno on kexinit + +--- + paramiko/transport.py | 18 ++++++++++-- + sites/www/changelog.rst | 3 ++ + tests/test_transport.py | 62 +++++++++++++++++++++++++++++++++++++---- + 3 files changed, 75 insertions(+), 8 deletions(-) + +--- a/paramiko/transport.py ++++ b/paramiko/transport.py +@@ -312,6 +312,7 @@ class Transport(threading.Thread, Closin + gss_deleg_creds=True, + disabled_algorithms=None, + strict_kex=True, ++ packetizer_class=None, + ): + """ + Create a new SSH session over an existing socket, or socket-like +@@ -378,6 +379,9 @@ class Transport(threading.Thread, Closin + Whether to advertise (and implement, if client also advertises + support for) a "strict kex" mode for safer handshaking. Default: + ``True``. ++ :param packetizer_class: ++ Which class to use for instantiating the internal packet handler. ++ Default: ``None`` (i.e.: use `Packetizer` as normal). + + .. versionchanged:: 1.15 + Added the ``default_window_size`` and ``default_max_packet_size`` +@@ -432,7 +436,7 @@ class Transport(threading.Thread, Closin + self.sock.settimeout(self._active_check_timeout) + + # negotiated crypto parameters +- self.packetizer = Packetizer(sock) ++ self.packetizer = (packetizer_class or Packetizer)(sock) + self.local_version = "SSH-" + self._PROTO_ID + "-" + self._CLIENT_ID + self.remote_version = "" + self.local_cipher = self.remote_cipher = "" +@@ -2399,6 +2403,13 @@ class Transport(threading.Thread, Closin + for i in to_pop: + kex_algo_list.pop(i) + ++ # CVE mitigation: expect zeroed-out seqno anytime we are performing kex ++ # init phase, if strict mode was negotiated. ++ if self.agreed_on_strict_kex and m.seqno != 0: ++ raise MessageOrderError( ++ f"Got nonzero seqno ({m.seqno}) during strict KEXINIT!" ++ ) ++ + # as a server, we pick the first item in the client's list that we + # support. + # as a client, we pick the first item in our list that the server +--- a/tests/test_transport.py ++++ b/tests/test_transport.py +@@ -1115,6 +1115,16 @@ class TransportTest(unittest.TestCase): + # Real fix's behavior + self._expect_unimplemented() + ++ def test_can_override_packetizer_used(self): ++ class MyPacketizer(Packetizer): ++ pass ++ ++ # control case ++ assert Transport(sock=LoopSocket()).packetizer.__class__ is Packetizer ++ # overridden case ++ tweaked = Transport(sock=LoopSocket(), packetizer_class=MyPacketizer) ++ assert tweaked.packetizer.__class__ is MyPacketizer ++ + + class AlgorithmDisablingTests(unittest.TestCase): + def test_preferred_lists_default_to_private_attribute_contents(self): +@@ -1283,6 +1293,20 @@ def server( + sockc.close() + + ++class BadSeqPacketizer(Packetizer): ++ def read_message(self): ++ cmd, msg = super().read_message() ++ # Only mess w/ seqno if kexinit. ++ if cmd is MSG_KEXINIT: ++ # NOTE: this is /only/ the copy of the seqno which gets ++ # transmitted up from Packetizer; it's not modifying ++ # Packetizer's own internal seqno. For these tests, ++ # modifying the latter isn't required, and is also harder ++ # to do w/o triggering MAC mismatches. ++ msg.seqno = 17 # arbitrary nonzero int ++ return cmd, msg ++ ++ + class TestStrictKex: + def test_kex_algos_includes_kex_strict_c(self): + with server() as (tc, _): +@@ -1317,9 +1341,6 @@ class TestStrictKex: + ) + ) + +- def test_sequence_numbers_reset_on_newkeys(self): +- skip() +- + def test_MessageOrderError_raised_on_out_of_order_messages(self): + with raises(MessageOrderError): + with server() as (tc, _): +@@ -1328,12 +1349,41 @@ class TestStrictKex: + tc._expect_packet(MSG_KEXINIT) + tc.open_session() + +- def test_SSHException_raised_on_out_of_order_messages_when_not_strict(self): ++ def test_SSHException_raised_on_out_of_order_messages_when_not_strict( ++ self, ++ ): + # This is kind of dumb (either situation is still fatal!) but whatever, + # may as well be strict with our new strict flag... + with raises(SSHException) as info: # would be true either way, but +- with server(client_init=dict(strict_kex=False), +- ) as (tc, _): ++ with server( ++ client_init=dict(strict_kex=False), ++ ) as (tc, _): + tc._expect_packet(MSG_KEXINIT) + tc.open_session() + assert info.type is SSHException # NOT MessageOrderError! ++ ++ def test_error_not_raised_when_kexinit_not_seq_0_but_unstrict(self): ++ with server( ++ client_init=dict( ++ # Disable strict kex ++ strict_kex=False, ++ # Give our clientside a packetizer that sets all kexinit ++ # Message objects to have .seqno==17, which would trigger the ++ # new logic if we'd forgotten to wrap it in strict-kex check ++ packetizer_class=BadSeqPacketizer, ++ ), ++ ): ++ pass # kexinit happens at connect... ++ ++ def test_MessageOrderError_raised_when_kexinit_not_seq_0_and_strict(self): ++ with raises(MessageOrderError): ++ with server( ++ # Give our clientside a packetizer that sets all kexinit ++ # Message objects to have .seqno==17, which should trigger the ++ # new logic (given we are NOT disabling strict-mode) ++ client_init=dict(packetizer_class=BadSeqPacketizer), ++ ): ++ pass # kexinit happens at connect... ++ ++ def test_sequence_numbers_reset_on_newkeys(self): ++ skip() diff -Nru paramiko-2.6.0/debian/patches/CVE-2023-48795-5.patch paramiko-2.6.0/debian/patches/CVE-2023-48795-5.patch --- paramiko-2.6.0/debian/patches/CVE-2023-48795-5.patch 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-2.6.0/debian/patches/CVE-2023-48795-5.patch 2024-01-12 12:30:05.000000000 +0000 @@ -0,0 +1,107 @@ +Backport of: + +From fa46de7feeeb8a01dc471581a0258252ce4f2db6 Mon Sep 17 00:00:00 2001 +From: Jeff Forcier +Date: Sat, 16 Dec 2023 17:12:42 -0500 +Subject: [PATCH] Reset sequence numbers on rekey + +--- + paramiko/packet.py | 6 ++++++ + paramiko/transport.py | 22 ++++++++++++++++++++-- + tests/test_transport.py | 25 +++++++++++++++++++++++-- + 3 files changed, 49 insertions(+), 4 deletions(-) + +--- a/paramiko/packet.py ++++ b/paramiko/packet.py +@@ -130,6 +130,12 @@ class Packetizer(object): + def closed(self): + return self.__closed + ++ def reset_seqno_out(self): ++ self.__sequence_number_out = 0 ++ ++ def reset_seqno_in(self): ++ self.__sequence_number_in = 0 ++ + def set_log(self, log): + """ + Set the Python log object to use for logging. +--- a/paramiko/transport.py ++++ b/paramiko/transport.py +@@ -2405,9 +2405,13 @@ class Transport(threading.Thread, Closin + + # CVE mitigation: expect zeroed-out seqno anytime we are performing kex + # init phase, if strict mode was negotiated. +- if self.agreed_on_strict_kex and m.seqno != 0: ++ if ( ++ self.agreed_on_strict_kex ++ and not self.initial_kex_done ++ and m.seqno != 0 ++ ): + raise MessageOrderError( +- f"Got nonzero seqno ({m.seqno}) during strict KEXINIT!" ++ "In strict-kex mode, but KEXINIT was not the first packet!" + ) + + # as a server, we pick the first item in the client's list that we +@@ -2603,6 +2607,13 @@ class Transport(threading.Thread, Closin + ): + self._log(DEBUG, "Switching on inbound compression ...") + self.packetizer.set_inbound_compressor(compress_in()) ++ # Reset inbound sequence number if strict mode. ++ if self.agreed_on_strict_kex: ++ self._log( ++ DEBUG, ++ f"Resetting inbound seqno after NEWKEYS due to strict mode", ++ ) ++ self.packetizer.reset_seqno_in() + + def _activate_outbound(self): + """switch on newly negotiated encryption parameters for +@@ -2610,6 +2621,13 @@ class Transport(threading.Thread, Closin + m = Message() + m.add_byte(cMSG_NEWKEYS) + self._send_message(m) ++ # Reset outbound sequence number if strict mode. ++ if self.agreed_on_strict_kex: ++ self._log( ++ DEBUG, ++ f"Resetting outbound sequence number after NEWKEYS due to strict mode", ++ ) ++ self.packetizer.reset_seqno_out() + block_size = self._cipher_info[self.local_cipher]["block-size"] + if self.server_mode: + IV_out = self._compute_key("B", block_size) +--- a/tests/test_transport.py ++++ b/tests/test_transport.py +@@ -1385,5 +1385,28 @@ class TestStrictKex: + ): + pass # kexinit happens at connect... + +- def test_sequence_numbers_reset_on_newkeys(self): +- skip() ++ def test_sequence_numbers_reset_on_newkeys_when_strict(self): ++ with server(defer=True) as (tc, ts): ++ # When in strict mode, these should all be zero or close to it ++ # (post-kexinit, pre-auth). ++ # Server->client will be 1 (EXT_INFO got sent after NEWKEYS) ++ # Ubuntu Backporting notes: since this old version doesn't have ++ # EXT_INFO, the sequence number will actually be 0 ++ assert tc.packetizer._Packetizer__sequence_number_in == 0 ++ assert ts.packetizer._Packetizer__sequence_number_out == 0 ++ # Client->server will be 0 ++ assert tc.packetizer._Packetizer__sequence_number_out == 0 ++ assert ts.packetizer._Packetizer__sequence_number_in == 0 ++ ++ def test_sequence_numbers_not_reset_on_newkeys_when_not_strict(self): ++ with server(defer=True, client_init=dict(strict_kex=False)) as ( ++ tc, ++ ts, ++ ): ++ # When not in strict mode, these will all be ~3-4 or so ++ # (post-kexinit, pre-auth). Not encoding exact values as it will ++ # change anytime we mess with the test harness... ++ assert tc.packetizer._Packetizer__sequence_number_in != 0 ++ assert tc.packetizer._Packetizer__sequence_number_out != 0 ++ assert ts.packetizer._Packetizer__sequence_number_in != 0 ++ assert ts.packetizer._Packetizer__sequence_number_out != 0 diff -Nru paramiko-2.6.0/debian/patches/CVE-2023-48795-6.patch paramiko-2.6.0/debian/patches/CVE-2023-48795-6.patch --- paramiko-2.6.0/debian/patches/CVE-2023-48795-6.patch 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-2.6.0/debian/patches/CVE-2023-48795-6.patch 2024-01-12 12:30:05.000000000 +0000 @@ -0,0 +1,113 @@ +Backport of: + +From 96db1e2be856eac66631761bae41167a1ebd2b4e Mon Sep 17 00:00:00 2001 +From: Jeff Forcier +Date: Sun, 17 Dec 2023 17:13:53 -0500 +Subject: [PATCH] Raise exception when sequence numbers rollover during initial + kex + +--- + paramiko/packet.py | 17 +++++++++++++---- + paramiko/transport.py | 4 +++- + sites/www/changelog.rst | 2 ++ + tests/test_transport.py | 32 ++++++++++++++++++++++++++++++++ + 4 files changed, 50 insertions(+), 5 deletions(-) + +--- a/paramiko/packet.py ++++ b/paramiko/packet.py +@@ -86,6 +86,7 @@ class Packetizer(object): + self.__need_rekey = False + self.__init_count = 0 + self.__remainder = bytes() ++ self._initial_kex_done = False + + # used for noticing when to re-key: + self.__sent_bytes = 0 +@@ -431,9 +432,12 @@ class Packetizer(object): + out += compute_hmac( + self.__mac_key_out, payload, self.__mac_engine_out + )[: self.__mac_size_out] +- self.__sequence_number_out = ( +- self.__sequence_number_out + 1 +- ) & xffffffff ++ next_seq = (self.__sequence_number_out + 1) & xffffffff ++ if next_seq == 0 and not self._initial_kex_done: ++ raise SSHException( ++ "Sequence number rolled over during initial kex!" ++ ) ++ self.__sequence_number_out = next_seq + self.write_all(out) + + self.__sent_bytes += len(out) +@@ -537,7 +541,12 @@ class Packetizer(object): + + msg = Message(payload[1:]) + msg.seqno = self.__sequence_number_in +- self.__sequence_number_in = (self.__sequence_number_in + 1) & xffffffff ++ next_seq = (self.__sequence_number_in + 1) & xffffffff ++ if next_seq == 0 and not self._initial_kex_done: ++ raise SSHException( ++ "Sequence number rolled over during initial kex!" ++ ) ++ self.__sequence_number_in = next_seq + + # check for rekey + raw_packet_size = packet_size + self.__mac_size_in + 4 +--- a/paramiko/transport.py ++++ b/paramiko/transport.py +@@ -2690,7 +2690,9 @@ class Transport(threading.Thread, Closin + self.auth_handler = AuthHandler(self) + if not self.initial_kex_done: + # this was the first key exchange +- self.initial_kex_done = True ++ # (also signal to packetizer as it sometimes wants to know this ++ # staus as well, eg when seqnos rollover) ++ self.initial_kex_done = self.packetizer._initial_kex_done = True + # send an event? + if self.completion_event is not None: + self.completion_event.set() +--- a/tests/test_transport.py ++++ b/tests/test_transport.py +@@ -30,6 +30,7 @@ import socket + import time + import threading + import random ++import sys + import unittest + from mock import Mock + +@@ -1410,3 +1411,34 @@ class TestStrictKex: + assert tc.packetizer._Packetizer__sequence_number_out != 0 + assert ts.packetizer._Packetizer__sequence_number_in != 0 + assert ts.packetizer._Packetizer__sequence_number_out != 0 ++ ++ def test_sequence_number_rollover_detected(self): ++ class RolloverTransport(Transport): ++ def __init__(self, *args, **kwargs): ++ super().__init__(*args, **kwargs) ++ # Induce an about-to-rollover seqno, such that it rolls over ++ # during initial kex. ++ setattr( ++ self.packetizer, ++ f"_Packetizer__sequence_number_in", ++ sys.maxsize, ++ ) ++ setattr( ++ self.packetizer, ++ f"_Packetizer__sequence_number_out", ++ sys.maxsize, ++ ) ++ ++ with raises( ++ SSHException, ++ match=r"Sequence number rolled over during initial kex!", ++ ): ++ with server( ++ client_init=dict( ++ # Disable strict kex - this should happen always ++ strict_kex=False, ++ ), ++ # Transport which tickles its packetizer seqno's ++ transport_factory=RolloverTransport, ++ ): ++ pass # kexinit happens at connect... diff -Nru paramiko-2.6.0/debian/patches/CVE-2023-48795-7.patch paramiko-2.6.0/debian/patches/CVE-2023-48795-7.patch --- paramiko-2.6.0/debian/patches/CVE-2023-48795-7.patch 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-2.6.0/debian/patches/CVE-2023-48795-7.patch 2024-01-12 12:30:05.000000000 +0000 @@ -0,0 +1,132 @@ +Backport of: + +From 33508c920309860c4a775be70f209c2a400e18ec Mon Sep 17 00:00:00 2001 +From: Jeff Forcier +Date: Sun, 17 Dec 2023 18:47:33 -0500 +Subject: [PATCH] Expand MessageOrderError use to handle more packet types + +--- + paramiko/transport.py | 16 ++++++++++++ + sites/www/changelog.rst | 6 ++--- + tests/_util.py | 7 ++++- + tests/test_transport.py | 58 ++++++++++++++++++++++++++++++++++++----- + 4 files changed, 76 insertions(+), 11 deletions(-) + +--- a/paramiko/transport.py ++++ b/paramiko/transport.py +@@ -2037,6 +2037,20 @@ class Transport(threading.Thread, Closin + # be empty.) + return reply + ++ def _enforce_strict_kex(self, ptype): ++ """ ++ Conditionally raise `MessageOrderError` during strict initial kex. ++ ++ This method should only be called inside code that handles non-KEXINIT ++ messages; it does not interrogate ``ptype`` besides using it to log ++ more accurately. ++ """ ++ if self.agreed_on_strict_kex and not self.initial_kex_done: ++ name = MSG_NAMES.get(ptype, f"msg {ptype}") ++ raise MessageOrderError( ++ f"In strict-kex mode, but was sent {name!r}!" ++ ) ++ + def run(self): + # (use the exposed "run" method, because if we specify a thread target + # of a private method, threading.Thread will keep a reference to it +@@ -2081,11 +2095,13 @@ class Transport(threading.Thread, Closin + except NeedRekeyException: + continue + if ptype == MSG_IGNORE: ++ self._enforce_strict_kex(ptype) + continue + elif ptype == MSG_DISCONNECT: + self._parse_disconnect(m) + break + elif ptype == MSG_DEBUG: ++ self._enforce_strict_kex(ptype) + self._parse_debug(m) + continue + if len(self._expected_packet) > 0: +--- a/tests/test_transport.py ++++ b/tests/test_transport.py +@@ -54,7 +54,11 @@ from paramiko.common import ( + MAX_WINDOW_SIZE, + MIN_PACKET_SIZE, + MIN_WINDOW_SIZE, ++ MSG_CHANNEL_OPEN, ++ MSG_DEBUG, ++ MSG_IGNORE, + MSG_KEXINIT, ++ MSG_UNIMPLEMENTED, + MSG_USERAUTH_SUCCESS, + cMSG_CHANNEL_WINDOW_ADJUST, + cMSG_UNIMPLEMENTED, +@@ -79,6 +83,10 @@ Note: An SSH banner may eventually appea + Maybe. + """ + ++# Faux 'packet type' we do not implement and are unlikely ever to (but which is ++# technically "within spec" re RFC 4251 ++MSG_FUGGEDABOUTIT = 253 ++ + + class NullServer(ServerInterface): + paranoid_did_password = False +@@ -1342,13 +1350,49 @@ class TestStrictKex: + ) + ) + +- def test_MessageOrderError_raised_on_out_of_order_messages(self): ++ @mark.parametrize( ++ "ptype", ++ ( ++ # "normal" but definitely out-of-order message ++ MSG_CHANNEL_OPEN, ++ # Normally ignored, but not in this case ++ MSG_IGNORE, ++ # Normally triggers debug parsing, but not in this case ++ MSG_DEBUG, ++ # Normally ignored, but...you get the idea ++ MSG_UNIMPLEMENTED, ++ # Not real, so would normally trigger us /sending/ ++ # MSG_UNIMPLEMENTED, but... ++ MSG_FUGGEDABOUTIT, ++ ), ++ ) ++ def test_MessageOrderError_non_kex_messages_in_initial_kex(self, ptype): ++ class AttackTransport(Transport): ++ # Easiest apparent spot on server side which is: ++ # - late enough for both ends to have handshook on strict mode ++ # - early enough to be in the window of opportunity for Terrapin ++ # attack; essentially during actual kex, when the engine is ++ # waiting for things like MSG_KEXECDH_REPLY (for eg curve25519). ++ def _negotiate_keys(self, m): ++ self.clear_to_send_lock.acquire() ++ try: ++ self.clear_to_send.clear() ++ finally: ++ self.clear_to_send_lock.release() ++ if self.local_kex_init is None: ++ # remote side wants to renegotiate ++ self._send_kex_init() ++ self._parse_kex_init(m) ++ # Here, we would normally kick over to kex_engine, but instead ++ # we want the server to send the OOO message. ++ m = Message() ++ m.add_byte(byte_chr(ptype)) ++ # rest of packet unnecessary... ++ self._send_message(m) ++ + with raises(MessageOrderError): +- with server() as (tc, _): +- # A bit artificial as it's outside kexinit/handshake, but much +- # easier to trigger and still in line with behavior under test +- tc._expect_packet(MSG_KEXINIT) +- tc.open_session() ++ with server(server_transport_factory=AttackTransport) as (tc, _): ++ pass # above should run and except during connect() + + def test_SSHException_raised_on_out_of_order_messages_when_not_strict( + self, diff -Nru paramiko-2.6.0/debian/patches/CVE-2023-48795-8.patch paramiko-2.6.0/debian/patches/CVE-2023-48795-8.patch --- paramiko-2.6.0/debian/patches/CVE-2023-48795-8.patch 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-2.6.0/debian/patches/CVE-2023-48795-8.patch 2024-01-12 12:30:05.000000000 +0000 @@ -0,0 +1,58 @@ +Backport of: + +From 30b447b911c39460bbef5e7834e339c43a251316 Mon Sep 17 00:00:00 2001 +From: Jeff Forcier +Date: Sun, 17 Dec 2023 18:47:49 -0500 +Subject: [PATCH] Linting + +--- + paramiko/transport.py | 4 ++-- + tests/test_transport.py | 6 +++--- + 2 files changed, 5 insertions(+), 5 deletions(-) + +--- a/paramiko/transport.py ++++ b/paramiko/transport.py +@@ -2640,7 +2640,7 @@ class Transport(threading.Thread, Closin + if self.agreed_on_strict_kex: + self._log( + DEBUG, +- f"Resetting inbound seqno after NEWKEYS due to strict mode", ++ "Resetting inbound seqno after NEWKEYS due to strict mode", + ) + self.packetizer.reset_seqno_in() + +@@ -2654,7 +2654,7 @@ class Transport(threading.Thread, Closin + if self.agreed_on_strict_kex: + self._log( + DEBUG, +- f"Resetting outbound sequence number after NEWKEYS due to strict mode", ++ "Resetting outbound seqno after NEWKEYS due to strict mode", + ) + self.packetizer.reset_seqno_out() + block_size = self._cipher_info[self.local_cipher]["block-size"] +--- a/tests/test_transport.py ++++ b/tests/test_transport.py +@@ -68,7 +68,7 @@ from paramiko.message import Message + + from .util import needs_builtin, _support, slow + from .loop import LoopSocket +-from pytest import skip, mark, raises ++from pytest import mark, raises + + + LONG_BANNER = """\ +@@ -1464,12 +1464,12 @@ class TestStrictKex: + # during initial kex. + setattr( + self.packetizer, +- f"_Packetizer__sequence_number_in", ++ "_Packetizer__sequence_number_in", + sys.maxsize, + ) + setattr( + self.packetizer, +- f"_Packetizer__sequence_number_out", ++ "_Packetizer__sequence_number_out", + sys.maxsize, + ) + diff -Nru paramiko-2.6.0/debian/patches/CVE-2023-48795-pre7.patch paramiko-2.6.0/debian/patches/CVE-2023-48795-pre7.patch --- paramiko-2.6.0/debian/patches/CVE-2023-48795-pre7.patch 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-2.6.0/debian/patches/CVE-2023-48795-pre7.patch 2024-01-12 12:30:05.000000000 +0000 @@ -0,0 +1,79 @@ +Partial backport of: + +From 7700c7e033652ed98c0c385b0da936f12b35aabf Mon Sep 17 00:00:00 2001 +From: Jeff Forcier +Date: Thu, 20 Apr 2023 17:45:08 -0400 +Subject: [PATCH] Opt-in overhaul to how MSG_SERVICE_REQUEST is done + +- New subclass(es) for opt-in use. Most below messages refer to them, + not parent classes. +- In parent classes, make handler tables instance attributes for easier + subclass twiddling. +- Refactor Transport-level session check +- Refactor Transport-level auth handler instantiation (but keep behavior + the same, for now) +- Add service-request handler to Transport subclass, and remove from + AuthHandler subclass +- Remove manual event injection from the handful of Transport auth + methods which supported it. Suspect unused, don't need the extra + complexity, and wasn't consistent anyways - can add back smarter later + if anyone needs it. +- Not bothering with gssapi at all for now as I cannot easily test it +- Primarily tested against the new AuthStrategy architecture +--- + paramiko/__init__.py | 6 +- + paramiko/auth_handler.py | 156 +++++++++++++++++++++++++++---- + paramiko/transport.py | 192 ++++++++++++++++++++++++++++++++++++--- + sites/www/changelog.rst | 53 +++++++++++ + tests/test_transport.py | 3 +- + 5 files changed, 376 insertions(+), 34 deletions(-) + +--- a/paramiko/transport.py ++++ b/paramiko/transport.py +@@ -509,6 +509,19 @@ class Transport(threading.Thread, Closin + self.server_accept_cv = threading.Condition(self.lock) + self.subsystem_table = {} + ++ # Handler table, now set at init time for easier per-instance ++ # manipulation and subclass twiddling. ++ self._handler_table = { ++ MSG_NEWKEYS: self._parse_newkeys, ++ MSG_GLOBAL_REQUEST: self._parse_global_request, ++ MSG_REQUEST_SUCCESS: self._parse_request_success, ++ MSG_REQUEST_FAILURE: self._parse_request_failure, ++ MSG_CHANNEL_OPEN_SUCCESS: self._parse_channel_open_success, ++ MSG_CHANNEL_OPEN_FAILURE: self._parse_channel_open_failure, ++ MSG_CHANNEL_OPEN: self._parse_channel_open, ++ MSG_KEXINIT: self._negotiate_keys, ++ } ++ + def _filter_algorithm(self, type_): + default = getattr(self, "_preferred_{}".format(type_)) + return tuple( +@@ -2095,7 +2108,7 @@ class Transport(threading.Thread, Closin + if error_msg: + self._send_message(error_msg) + else: +- self._handler_table[ptype](self, m) ++ self._handler_table[ptype](m) + elif ptype in self._channel_handler_table: + chanid = m.get_int() + chan = self._channels.get(chanid) +@@ -2946,17 +2959,6 @@ class Transport(threading.Thread, Closin + finally: + self.lock.release() + +- _handler_table = { +- MSG_NEWKEYS: _parse_newkeys, +- MSG_GLOBAL_REQUEST: _parse_global_request, +- MSG_REQUEST_SUCCESS: _parse_request_success, +- MSG_REQUEST_FAILURE: _parse_request_failure, +- MSG_CHANNEL_OPEN_SUCCESS: _parse_channel_open_success, +- MSG_CHANNEL_OPEN_FAILURE: _parse_channel_open_failure, +- MSG_CHANNEL_OPEN: _parse_channel_open, +- MSG_KEXINIT: _negotiate_keys, +- } +- + _channel_handler_table = { + MSG_CHANNEL_SUCCESS: Channel._request_success, + MSG_CHANNEL_FAILURE: Channel._request_failed, diff -Nru paramiko-2.6.0/debian/patches/disable_flaky_test.patch paramiko-2.6.0/debian/patches/disable_flaky_test.patch --- paramiko-2.6.0/debian/patches/disable_flaky_test.patch 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-2.6.0/debian/patches/disable_flaky_test.patch 2024-01-12 12:30:05.000000000 +0000 @@ -0,0 +1,15 @@ +Description: disable flaky test +Author: Marc Deslauriers + +--- a/tests/test_transport.py ++++ b/tests/test_transport.py +@@ -1430,7 +1430,8 @@ class TestStrictKex: + ): + pass # kexinit happens at connect... + +- def test_sequence_numbers_reset_on_newkeys_when_strict(self): ++ # This test is disabled as it is flaky ++ def disabled_test_sequence_numbers_reset_on_newkeys_when_strict(self): + with server(defer=True) as (tc, ts): + # When in strict mode, these should all be zero or close to it + # (post-kexinit, pre-auth). diff -Nru paramiko-2.6.0/debian/patches/fix_test_on_armhf.patch paramiko-2.6.0/debian/patches/fix_test_on_armhf.patch --- paramiko-2.6.0/debian/patches/fix_test_on_armhf.patch 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-2.6.0/debian/patches/fix_test_on_armhf.patch 2024-01-12 12:30:05.000000000 +0000 @@ -0,0 +1,20 @@ +Description: fix test on armhf +Author: Marc Deslauriers + +--- a/tests/test_transport.py ++++ b/tests/test_transport.py +@@ -1465,12 +1465,12 @@ class TestStrictKex: + setattr( + self.packetizer, + "_Packetizer__sequence_number_in", +- sys.maxsize, ++ 0xffffffff, + ) + setattr( + self.packetizer, + "_Packetizer__sequence_number_out", +- sys.maxsize, ++ 0xffffffff, + ) + + with raises( diff -Nru paramiko-2.6.0/debian/patches/series paramiko-2.6.0/debian/patches/series --- paramiko-2.6.0/debian/patches/series 2022-03-24 13:24:40.000000000 +0000 +++ paramiko-2.6.0/debian/patches/series 2024-01-12 12:30:05.000000000 +0000 @@ -1,2 +1,12 @@ remove_pytest_relaxed.patch CVE-2022-24302.patch +CVE-2023-48795-2.patch +CVE-2023-48795-3.patch +CVE-2023-48795-4.patch +CVE-2023-48795-5.patch +CVE-2023-48795-6.patch +CVE-2023-48795-pre7.patch +CVE-2023-48795-7.patch +CVE-2023-48795-8.patch +fix_test_on_armhf.patch +disable_flaky_test.patch diff -Nru paramiko-2.6.0/debian/rules paramiko-2.6.0/debian/rules --- paramiko-2.6.0/debian/rules 2020-01-10 00:39:19.000000000 +0000 +++ paramiko-2.6.0/debian/rules 2024-01-12 12:30:05.000000000 +0000 @@ -1,7 +1,6 @@ #!/usr/bin/make -f export PYBUILD_NAME=paramiko -export PYBUILD_DISABLE=test export LC_ALL=C.UTF-8 %: