changed debian/source/format to native

This commit is contained in:
luoyaoming 2024-05-07 11:30:07 +08:00
parent 719ef1093a
commit 08e1f0fffb
15 changed files with 1 additions and 1196 deletions

View File

@ -1,46 +0,0 @@
Backport of:
From be3ffc18cc466e0b0a877d716721353c12561bcc Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
Date: Fri, 15 Dec 2023 22:14:48 -0500
Subject: [PATCH] Make ext-info faux-KexAlgorithm detection more robust
---
paramiko/transport.py | 5 +++--
sites/www/changelog.rst | 3 +++
tests/test_transport.py | 8 ++++++--
3 files changed, 12 insertions(+), 4 deletions(-)
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -2429,8 +2429,9 @@ class Transport(threading.Thread, Closin
# Strip out ext-info "kex algo"
self._remote_ext_info = None
- if kex_algo_list[-1].startswith("ext-info-"):
- self._remote_ext_info = kex_algo_list.pop()
+ for i, algo in enumerate(kex_algo_list):
+ if algo.startswith("ext-info-"):
+ self._remote_ext_info = kex_algo_list.pop(i)
# as a server, we pick the first item in the client's list that we
# support.
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -1350,10 +1350,14 @@ class TestSHA2SignatureKeyExchange(unitt
class TestExtInfo(unittest.TestCase):
- def test_ext_info_handshake(self):
+ def test_ext_info_handshake_exposed_in_client_kexinit(self):
with server() as (tc, _):
+ # NOTE: this is latest KEXINIT /sent by us/ (Transport retains it)
kex = tc._get_latest_kex_init()
- assert kex["kex_algo_list"][-1] == "ext-info-c"
+ # flag in KexAlgorithms list
+ assert "ext-info-c" in kex["kex_algo_list"]
+ # data stored on Transport after hearing back from a compatible
+ # server (such as ourselves in server mode)
assert tc.server_extensions == {
"server-sig-algs": b"ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-512,rsa-sha2-256,ssh-rsa,ssh-dss" # noqa
}

View File

@ -1,162 +0,0 @@
Backport of:
From 773a174fb1e40e1d18dbe2625e16337ea401119e Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
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
@@ -329,6 +329,7 @@ class Transport(threading.Thread, Closin
gss_deleg_creds=True,
disabled_algorithms=None,
server_sig_algs=True,
+ strict_kex=True,
):
"""
Create a new SSH session over an existing socket, or socket-like
@@ -395,6 +396,10 @@ class Transport(threading.Thread, Closin
Whether to send an extra message to compatible clients, in server
mode, with a list of supported pubkey algorithms. Default:
``True``.
+ :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``
@@ -405,10 +410,14 @@ class Transport(threading.Thread, Closin
Added the ``disabled_algorithms`` kwarg.
.. versionchanged:: 2.9
Added the ``server_sig_algs`` kwarg.
+ .. versionchanged:: 3.4
+ Added the ``strict_kex`` kwarg.
"""
self.active = False
self.hostname = None
self.server_extensions = {}
+ self.advertise_strict_kex = strict_kex
+ self.agreed_on_strict_kex = False
if isinstance(sock, string_types):
# convert "host:port" into (host, port)
@@ -2342,12 +2351,18 @@ class Transport(threading.Thread, Closin
)
else:
available_server_keys = self.preferred_keys
- # Signal support for MSG_EXT_INFO.
+ # Signal support for MSG_EXT_INFO so server will send it to us.
# NOTE: doing this here handily means we don't even consider this
# value when agreeing on real kex algo to use (which is a common
# pitfall when adding this apparently).
kex_algos.append("ext-info-c")
+ # 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))
@@ -2427,11 +2442,28 @@ class Transport(threading.Thread, Closin
self._log(DEBUG, "kex follows: {}".format(kex_follows))
self._log(DEBUG, "=== Key exchange agreements ===")
- # Strip out ext-info "kex algo"
+ # Record, and strip out, ext-info and/or strict-kex non-algorithms
self._remote_ext_info = None
+ self._remote_strict_kex = None
+ to_pop = []
for i, algo in enumerate(kex_algo_list):
if algo.startswith("ext-info-"):
- self._remote_ext_info = kex_algo_list.pop(i)
+ self._remote_ext_info = algo
+ to_pop.insert(0, i)
+ elif 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.
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -24,6 +24,7 @@ from __future__ import with_statement
from binascii import hexlify
from contextlib import contextmanager
+import itertools
import select
import socket
import time
@@ -63,6 +64,7 @@ from paramiko.message import Message
from .util import needs_builtin, _support, requires_sha1_signing, slow
from .loop import LoopSocket
+from pytest import skip, mark
LONG_BANNER = """\
@@ -1463,3 +1465,44 @@ class TestSHA2SignaturePubkeys(unittest.
) as (tc, ts):
assert tc.is_authenticated()
assert tc._agreed_pubkey_algorithm == "rsa-sha2-256"
+
+
+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()

View File

@ -1,105 +0,0 @@
Backport of:
From f4dedacb9040d27d9844f51c81c28e0247d3e4a3 Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
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 (
ConfigParseError,
CouldNotCanonicalize,
IncompatiblePeer,
+ MessageOrderError,
PasswordRequiredException,
ProxyCommandFailure,
SSHException,
--- a/paramiko/ssh_exception.py
+++ b/paramiko/ssh_exception.py
@@ -235,3 +235,13 @@ class ConfigParseError(SSHException):
"""
pass
+
+
+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
@@ -110,6 +110,7 @@ from paramiko.ssh_exception import (
BadAuthenticationType,
ChannelException,
IncompatiblePeer,
+ MessageOrderError,
ProxyCommandFailure,
)
from paramiko.util import retry_on_signal, ClosingContextManager, clamp_value
@@ -2129,7 +2130,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
@@ -42,6 +42,7 @@ from paramiko import (
SSHException,
AuthenticationException,
IncompatiblePeer,
+ MessageOrderError,
SecurityOptions,
ServerInterface,
Transport,
@@ -64,7 +65,7 @@ from paramiko.message import Message
from .util import needs_builtin, _support, requires_sha1_signing, slow
from .loop import LoopSocket
-from pytest import skip, mark
+from pytest import skip, mark, raises
LONG_BANNER = """\
@@ -1504,5 +1505,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!

View File

@ -1,170 +0,0 @@
Backport of:
From 75e311d3c0845a316b6e7b3fae2488d86ad5a270 Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
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
@@ -331,6 +331,7 @@ class Transport(threading.Thread, Closin
disabled_algorithms=None,
server_sig_algs=True,
strict_kex=True,
+ packetizer_class=None,
):
"""
Create a new SSH session over an existing socket, or socket-like
@@ -401,6 +402,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``
@@ -413,6 +417,8 @@ class Transport(threading.Thread, Closin
Added the ``server_sig_algs`` kwarg.
.. versionchanged:: 3.4
Added the ``strict_kex`` kwarg.
+ .. versionchanged:: 3.4
+ Added the ``packetizer_class`` kwarg.
"""
self.active = False
self.hostname = None
@@ -460,7 +466,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 = ""
@@ -2407,7 +2413,8 @@ class Transport(threading.Thread, Closin
def _get_latest_kex_init(self):
return self._really_parse_kex_init(
- Message(self._latest_kex_init), ignore_first_byte=True
+ Message(self._latest_kex_init),
+ ignore_first_byte=True,
)
def _parse_kex_init(self, m):
@@ -2469,6 +2476,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
@@ -1118,6 +1118,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):
@@ -1468,6 +1478,20 @@ class TestSHA2SignaturePubkeys(unittest.
assert tc._agreed_pubkey_algorithm == "rsa-sha2-256"
+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, _):
@@ -1502,9 +1526,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, _):
@@ -1513,12 +1534,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()

View File

@ -1,136 +0,0 @@
Backport of:
From fa46de7feeeb8a01dc471581a0258252ce4f2db6 Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
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
@@ -2478,9 +2478,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
@@ -2682,6 +2686,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
@@ -2689,6 +2700,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
@@ -1211,6 +1211,7 @@ def server(
connect=None,
pubkeys=None,
catch_error=False,
+ defer=False,
):
"""
SSH server contextmanager for testing.
@@ -1231,6 +1232,8 @@ def server(
: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.
"""
if init is None:
init = {}
@@ -1239,7 +1242,12 @@ def server(
if client_init is None:
client_init = {}
if connect is None:
- connect = dict(username="slowdive", password="pygmalion")
+ # 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)
@@ -1570,5 +1578,26 @@ 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)
+ assert tc.packetizer._Packetizer__sequence_number_in == 1
+ assert ts.packetizer._Packetizer__sequence_number_out == 1
+ # 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

View File

@ -1,113 +0,0 @@
Backport of:
From 96db1e2be856eac66631761bae41167a1ebd2b4e Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
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
@@ -2797,7 +2797,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
@@ -1601,3 +1602,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...

View File

@ -1,167 +0,0 @@
Backport of:
From 33508c920309860c4a775be70f209c2a400e18ec Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
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
@@ -2097,6 +2097,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
@@ -2141,11 +2155,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
@@ -56,7 +56,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,
@@ -81,6 +85,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
@@ -1213,6 +1221,8 @@ def server(
pubkeys=None,
catch_error=False,
defer=False,
+ transport_factory=None,
+ server_transport_factory=None,
):
"""
SSH server contextmanager for testing.
@@ -1235,6 +1245,10 @@ def server(
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 = {}
@@ -1252,8 +1266,12 @@ def server(
socks = LoopSocket()
sockc = LoopSocket()
sockc.link(socks)
- tc = Transport(sockc, **dict(init, **client_init))
- ts = Transport(socks, **dict(init, **server_init))
+ 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"))
@@ -1535,13 +1553,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,

View File

@ -1,58 +0,0 @@
Backport of:
From 30b447b911c39460bbef5e7834e339c43a251316 Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
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
@@ -2720,7 +2720,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()
@@ -2734,7 +2734,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
@@ -70,7 +70,7 @@ from paramiko.message import Message
from .util import needs_builtin, _support, requires_sha1_signing, slow
from .loop import LoopSocket
-from pytest import skip, mark, raises
+from pytest import mark, raises
LONG_BANNER = """\
@@ -1665,12 +1665,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,
)

View File

@ -1,81 +0,0 @@
Partial backport of:
From 7700c7e033652ed98c0c385b0da936f12b35aabf Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
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
@@ -540,6 +540,20 @@ 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_EXT_INFO: self._parse_ext_info,
+ 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(
@@ -2154,7 +2168,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)
@@ -3053,18 +3067,6 @@ class Transport(threading.Thread, Closin
finally:
self.lock.release()
- _handler_table = {
- MSG_EXT_INFO: _parse_ext_info,
- 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,

View File

@ -1,15 +0,0 @@
Description: disable flaky test
Author: Marc Deslauriers <marc.deslauriers@canonical.com>
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -1633,7 +1633,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).

View File

@ -1,20 +0,0 @@
Description: fix test on armhf
Author: Marc Deslauriers <marc.deslauriers@canonical.com>
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -1666,12 +1666,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(

View File

@ -1,72 +0,0 @@
From: Debian Python Modules Team
<python-modules-team@lists.alioth.debian.org>
Date: Wed, 2 Sep 2020 10:30:06 +0200
Subject: remove_pytest_relaxed
---
setup.cfg | 1 -
tests/test_client.py | 23 ++++++++++++-----------
2 files changed, 12 insertions(+), 12 deletions(-)
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -34,7 +34,7 @@ import weakref
from tempfile import mkstemp
import pytest
-from pytest_relaxed import raises
+from pytest import raises
from mock import patch, Mock
import paramiko
@@ -785,13 +785,13 @@ class PasswordPassphraseTests(ClientTest
# Straightforward / duplicate of earlier basic password test.
self._test_connection(password="pygmalion")
- # TODO: more granular exception pending #387; should be signaling "no auth
- # methods available" because no key and no password
- @raises(SSHException)
@requires_sha1_signing
def test_passphrase_kwarg_not_used_for_password_auth(self):
# Using the "right" password in the "wrong" field shouldn't work.
- self._test_connection(passphrase="pygmalion")
+ # TODO: more granular exception pending #387; should be signaling "no auth
+ # methods available" because no key and no password
+ with raises(SSHException):
+ self._test_connection(passphrase='pygmalion')
@requires_sha1_signing
def test_passphrase_kwarg_used_for_key_passphrase(self):
@@ -811,15 +811,16 @@ class PasswordPassphraseTests(ClientTest
password="television",
)
- @raises(AuthenticationException) # TODO: more granular
@requires_sha1_signing
def test_password_kwarg_not_used_for_passphrase_when_passphrase_kwarg_given( # noqa
self
):
# Sanity: if we're given both fields, the password field is NOT used as
# a passphrase.
- self._test_connection(
- key_filename=_support("test_rsa_password.key"),
- password="television",
- passphrase="wat? lol no",
- )
+ with raises(AuthenticationException): # TODO: more granular
+ self._test_connection(
+ key_filename=_support("test_rsa_password.key"),
+ password="television",
+ passphrase="wat? lol no",
+ )
+
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,7 +1,4 @@
[pytest]
-# We use pytest-relaxed just for its utils at the moment, so disable it at the
-# plugin level until we adapt test organization to really use it.
-addopts = -p no:relaxed
# Loop on failure
looponfailroots = tests paramiko
# Ignore some warnings we cannot easily handle.

View File

@ -1,37 +0,0 @@
Backport of:
From 880a9b57f972adb755c638777d002ed7bdd68c2a Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
Date: Thu, 12 Jan 2023 16:15:44 -0500
Subject: [PATCH] Last spot of six removal
---
paramiko/pkey.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
--- a/paramiko/pkey.py
+++ b/paramiko/pkey.py
@@ -27,7 +27,6 @@ from hashlib import md5
import re
import struct
-import six
import bcrypt
from cryptography.hazmat.backends import default_backend
@@ -48,13 +47,13 @@ def _unpad_openssh(data):
# At the moment, this is only used for unpadding private keys on disk. This
# really ought to be made constant time (possibly by upstreaming this logic
# into pyca/cryptography).
- padding_length = six.indexbytes(data, -1)
+ padding_length = data[-1]
if 0x20 <= padding_length < 0x7f:
return data # no padding, last byte part comment (printable ascii)
if padding_length > 15:
raise SSHException("Invalid key")
for i in range(padding_length):
- if six.indexbytes(data, i - padding_length) != i + 1:
+ if data[i - padding_length] != i + 1:
raise SSHException("Invalid key")
return data[:-padding_length]

13
debian/patches/series vendored
View File

@ -1,13 +0,0 @@
remove_pytest_relaxed.patch
CVE-2023-48795-1.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
remove_six.patch
fix_test_on_armhf.patch
disable_flaky_test.patch

View File

@ -1 +1 @@
3.0 (quilt)
3.0 (native)