!48 [sync] PR-47: fix CVE-2023-48795
From: @openeuler-sync-bot Reviewed-by: @lyn1001 Signed-off-by: @lyn1001
This commit is contained in:
commit
664e911b8d
67
backport-0001-CVE-2023-48795.patch
Normal file
67
backport-0001-CVE-2023-48795.patch
Normal file
@ -0,0 +1,67 @@
|
||||
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
|
||||
|
||||
Reference:https://github.com/paramiko/paramiko/commit/be3ffc18cc466e0b0a877d716721353c12561bcc
|
||||
Conflict:The context of the changelog is adapted due to different versions
|
||||
|
||||
---
|
||||
paramiko/transport.py | 5 +++--
|
||||
sites/www/changelog.rst | 3 +++
|
||||
tests/test_transport.py | 8 ++++++--
|
||||
3 files changed, 12 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/paramiko/transport.py b/paramiko/transport.py
|
||||
index 68cc195..fd26371 100644
|
||||
--- a/paramiko/transport.py
|
||||
+++ b/paramiko/transport.py
|
||||
@@ -2429,8 +2429,9 @@ class Transport(threading.Thread, ClosingContextManager):
|
||||
|
||||
# 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.
|
||||
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
|
||||
index 29754bc..f180e77 100644
|
||||
--- a/sites/www/changelog.rst
|
||||
+++ b/sites/www/changelog.rst
|
||||
@@ -2,6 +2,9 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
+- :bug:`-` Tweak ``ext-info-(c|s)`` detection during KEXINIT protocol phase;
|
||||
+ the original implementation made assumptions based on an OpenSSH
|
||||
+ implementation detail.
|
||||
- :release:`2.11.0 <2022-05-16>`
|
||||
- :release:`2.10.5 <2022-05-16>`
|
||||
- :release:`2.9.5 <2022-05-16>`
|
||||
diff --git a/tests/test_transport.py b/tests/test_transport.py
|
||||
index 98a7d30..6bc0be8 100644
|
||||
--- a/tests/test_transport.py
|
||||
+++ b/tests/test_transport.py
|
||||
@@ -1350,10 +1350,14 @@ class TestSHA2SignatureKeyExchange(unittest.TestCase):
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
--
|
||||
2.33.0
|
||||
171
backport-0002-CVE-2023-48795.patch
Normal file
171
backport-0002-CVE-2023-48795.patch
Normal file
@ -0,0 +1,171 @@
|
||||
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
|
||||
|
||||
Reference:https://github.com/paramiko/paramiko/commit/773a174fb1e40e1d18dbe2625e16337ea401119e
|
||||
Conflict:The comments are different. Therefore, the transport.py file is adapted
|
||||
Due to different versions, some test cases do not exist. Therefore, context adaptation is required when new test cases are added
|
||||
|
||||
---
|
||||
paramiko/transport.py | 38 +++++++++++++++++++++++++++++++++---
|
||||
tests/test_transport.py | 43 +++++++++++++++++++++++++++++++++++++++++
|
||||
2 files changed, 78 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/paramiko/transport.py b/paramiko/transport.py
|
||||
index fd26371..2d6d581 100644
|
||||
--- a/paramiko/transport.py
|
||||
+++ b/paramiko/transport.py
|
||||
@@ -329,6 +329,7 @@ class Transport(threading.Thread, ClosingContextManager):
|
||||
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, ClosingContextManager):
|
||||
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, ClosingContextManager):
|
||||
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, ClosingContextManager):
|
||||
)
|
||||
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, ClosingContextManager):
|
||||
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.
|
||||
diff --git a/tests/test_transport.py b/tests/test_transport.py
|
||||
index 6bc0be8..c8cd498 100644
|
||||
--- 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.TestCase):
|
||||
) 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()
|
||||
--
|
||||
2.33.0
|
||||
|
||||
115
backport-0003-CVE-2023-48795.patch
Normal file
115
backport-0003-CVE-2023-48795.patch
Normal file
@ -0,0 +1,115 @@
|
||||
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
|
||||
|
||||
Reference:https://github.com/paramiko/paramiko/commit/f4dedacb9040d27d9844f51c81c28e0247d3e4a3
|
||||
Conflict:The changlog file is adapted for different versions. The context of the test case import module is adapted.
|
||||
|
||||
---
|
||||
paramiko/__init__.py | 1 +
|
||||
paramiko/ssh_exception.py | 9 +++++++++
|
||||
paramiko/transport.py | 6 +++++-
|
||||
tests/test_transport.py | 22 +++++++++++++++++++---
|
||||
4 files changed, 34 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
|
||||
index cbc240a..1bc91d0 100644
|
||||
--- a/paramiko/__init__.py
|
||||
+++ b/paramiko/__init__.py
|
||||
@@ -43,6 +43,7 @@ from paramiko.ssh_exception import (
|
||||
ConfigParseError,
|
||||
CouldNotCanonicalize,
|
||||
IncompatiblePeer,
|
||||
+ MessageOrderError,
|
||||
PasswordRequiredException,
|
||||
ProxyCommandFailure,
|
||||
SSHException,
|
||||
diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py
|
||||
index 620ab25..8a1413b 100644
|
||||
--- a/paramiko/ssh_exception.py
|
||||
+++ b/paramiko/ssh_exception.py
|
||||
@@ -235,3 +235,12 @@ class ConfigParseError(SSHException):
|
||||
"""
|
||||
|
||||
pass
|
||||
+
|
||||
+
|
||||
+class MessageOrderError(SSHException):
|
||||
+ """
|
||||
+ Out-of-order protocol messages were received, violating "strict kex" mode.
|
||||
+ .. versionadded:: 3.4
|
||||
+ """
|
||||
+
|
||||
+ pass
|
||||
diff --git a/paramiko/transport.py b/paramiko/transport.py
|
||||
index 2d6d581..eb1bcd6 100644
|
||||
--- 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, ClosingContextManager):
|
||||
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
|
||||
)
|
||||
diff --git a/tests/test_transport.py b/tests/test_transport.py
|
||||
index c8cd498..19023eb 100644
|
||||
--- 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!
|
||||
--
|
||||
2.33.0
|
||||
|
||||
190
backport-0004-CVE-2023-48795.patch
Normal file
190
backport-0004-CVE-2023-48795.patch
Normal file
@ -0,0 +1,190 @@
|
||||
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
|
||||
|
||||
Reference:https://github.com/paramiko/paramiko/commit/75e311d3c0845a316b6e7b3fae2488d86ad5a270
|
||||
Conflict:Context adaptation exists in the changelog.rst file due to version inconsistency.
|
||||
|
||||
---
|
||||
paramiko/transport.py | 18 ++++++++++--
|
||||
sites/www/changelog.rst | 3 ++
|
||||
tests/test_transport.py | 61 +++++++++++++++++++++++++++++++++++++----
|
||||
3 files changed, 74 insertions(+), 8 deletions(-)
|
||||
|
||||
diff --git a/paramiko/transport.py b/paramiko/transport.py
|
||||
index eb1bcd6..9f976a2 100644
|
||||
--- a/paramiko/transport.py
|
||||
+++ b/paramiko/transport.py
|
||||
@@ -331,6 +331,7 @@ class Transport(threading.Thread, ClosingContextManager):
|
||||
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, ClosingContextManager):
|
||||
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, ClosingContextManager):
|
||||
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, ClosingContextManager):
|
||||
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, ClosingContextManager):
|
||||
|
||||
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, ClosingContextManager):
|
||||
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
|
||||
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
|
||||
index f180e77..aeddee0 100644
|
||||
--- a/sites/www/changelog.rst
|
||||
+++ b/sites/www/changelog.rst
|
||||
@@ -2,6 +2,9 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
+- :feature:`-` `Transport` grew a new ``packetizer_class`` kwarg for overriding
|
||||
+ the packet-handler class used internally. Mostly for testing, but advanced
|
||||
+ users may find this useful when doing deep hacks.
|
||||
- :bug:`-` Tweak ``ext-info-(c|s)`` detection during KEXINIT protocol phase;
|
||||
the original implementation made assumptions based on an OpenSSH
|
||||
implementation detail.
|
||||
diff --git a/tests/test_transport.py b/tests/test_transport.py
|
||||
index 19023eb..de26231 100644
|
||||
--- 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):
|
||||
@@ -1467,6 +1477,19 @@ class TestSHA2SignaturePubkeys(unittest.TestCase):
|
||||
assert tc.is_authenticated()
|
||||
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):
|
||||
@@ -1502,9 +1525,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 +1533,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()
|
||||
--
|
||||
2.33.0
|
||||
114
backport-0005-CVE-2023-48795.patch
Normal file
114
backport-0005-CVE-2023-48795.patch
Normal file
@ -0,0 +1,114 @@
|
||||
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
|
||||
|
||||
Reference:https://github.com/paramiko/paramiko/commit/fa46de7feeeb8a01dc471581a0258252ce4f2db6
|
||||
Conflict:NA
|
||||
|
||||
---
|
||||
paramiko/packet.py | 6 ++++++
|
||||
paramiko/transport.py | 22 ++++++++++++++++++++--
|
||||
tests/test_transport.py | 25 +++++++++++++++++++++++--
|
||||
3 files changed, 49 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/paramiko/packet.py b/paramiko/packet.py
|
||||
index 1266316..1fc06d9 100644
|
||||
--- 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.
|
||||
diff --git a/paramiko/transport.py b/paramiko/transport.py
|
||||
index 83b1c81..0c68668 100644
|
||||
--- a/paramiko/transport.py
|
||||
+++ b/paramiko/transport.py
|
||||
@@ -2469,9 +2469,13 @@ class Transport(threading.Thread, ClosingContextManager):
|
||||
|
||||
# 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
|
||||
@@ -2670,6 +2674,13 @@ class Transport(threading.Thread, ClosingContextManager):
|
||||
):
|
||||
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
|
||||
@@ -2677,6 +2688,13 @@ class Transport(threading.Thread, ClosingContextManager):
|
||||
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)
|
||||
diff --git a/tests/test_transport.py b/tests/test_transport.py
|
||||
index 7440e88..9c3e8f5 100644
|
||||
--- a/tests/test_transport.py
|
||||
+++ b/tests/test_transport.py
|
||||
@@ -1548,5 +1548,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
|
||||
--
|
||||
2.33.0
|
||||
161
backport-0006-CVE-2023-48795.patch
Normal file
161
backport-0006-CVE-2023-48795.patch
Normal file
@ -0,0 +1,161 @@
|
||||
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
|
||||
|
||||
Reference:https://github.com/paramiko/paramiko/commit/96db1e2be856eac66631761bae41167a1ebd2b4e
|
||||
Conflict:Context adaptation exists in the changelog.rst file due to version inconsistency.
|
||||
|
||||
---
|
||||
paramiko/packet.py | 17 +++++++++++++----
|
||||
paramiko/transport.py | 4 +++-
|
||||
sites/www/changelog.rst | 28 ++++++++++++++++++++++++++++
|
||||
tests/test_transport.py | 32 ++++++++++++++++++++++++++++++++
|
||||
4 files changed, 76 insertions(+), 5 deletions(-)
|
||||
|
||||
diff --git a/paramiko/packet.py b/paramiko/packet.py
|
||||
index 1fc06d9..8b9e6d6 100644
|
||||
--- 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
|
||||
diff --git a/paramiko/transport.py b/paramiko/transport.py
|
||||
index 0c68668..750f9b4 100644
|
||||
--- a/paramiko/transport.py
|
||||
+++ b/paramiko/transport.py
|
||||
@@ -2785,7 +2785,9 @@ class Transport(threading.Thread, ClosingContextManager):
|
||||
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()
|
||||
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
|
||||
index 0675d04..6a2e4c0 100644
|
||||
--- a/sites/www/changelog.rst
|
||||
+++ b/sites/www/changelog.rst
|
||||
@@ -5,6 +5,34 @@ Changelog
|
||||
- :feature:`-` `Transport` grew a new ``packetizer_class`` kwarg for overriding
|
||||
the packet-handler class used internally. Mostly for testing, but advanced
|
||||
users may find this useful when doing deep hacks.
|
||||
+- :bug:`-` Address `CVE 2023-48795<https://terrapin-attack.com/>`_ (aka the
|
||||
+ "Terrapin Attack", a vulnerability found in the SSH protocol re: treatment of
|
||||
+ packet sequence numbers) as follows:
|
||||
+ - The vulnerability only impacts encrypt-then-MAC digest algorithms in
|
||||
+ tandem with CBC ciphers, and ChaCha20-poly1305; of these, Paramiko
|
||||
+ currently only implements ``hmac-sha2-(256|512)-etm`` in tandem with
|
||||
+ ``AES-CBC``. If you are unable to upgrade to Paramiko versions containing
|
||||
+ the below fixes right away, you may instead use the
|
||||
+ ``disabled_algorithms`` connection option to disable the ETM MACs and/or
|
||||
+ the CBC ciphers (this option is present in Paramiko >=2.6).
|
||||
+ - As the fix for the vulnerability requires both ends of the connection to
|
||||
+ cooperate, the below changes will only take effect when the remote end is
|
||||
+ OpenSSH >= TK (or equivalent, such as Paramiko in server mode, as of this
|
||||
+ patch version) and configured to use the new "strict kex" mode. Paramiko
|
||||
+ will always attempt to use "strict kex" mode if offered by the server,
|
||||
+ unless you override this by specifying ``strict_kex=False`` in
|
||||
+ `Transport.__init__`.
|
||||
+ - Paramiko will now raise an `SSHException` subclass (`MessageOrderError`)
|
||||
+ when protocol messages are received in unexpected order. (This is not
|
||||
+ *really* a change in behavior, as most such cases already raised vanilla
|
||||
+ `SSHException` anyways.)
|
||||
+ - Key (re)negotiation -- i.e. ``MSG_NEWKEYS``, whenever it is encountered
|
||||
+ -- now resets packet sequence numbers. (This should be invisible to users
|
||||
+ during normal operation, only causing exceptions if the exploit is
|
||||
+ encountered, which will usually result in, again, `MessageOrderError`.)
|
||||
+ - Sequence number rollover will now raise `SSHException` if it occurs
|
||||
+ during initial key exchange (regardless of strict mode status).
|
||||
+
|
||||
- :bug:`-` Tweak ``ext-info-(c|s)`` detection during KEXINIT protocol phase;
|
||||
the original implementation made assumptions based on an OpenSSH
|
||||
implementation detail.
|
||||
diff --git a/tests/test_transport.py b/tests/test_transport.py
|
||||
index 9c3e8f5..4ed712e 100644
|
||||
--- 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
|
||||
|
||||
@@ -1571,3 +1572,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...
|
||||
--
|
||||
2.33.0
|
||||
285
backport-0007-CVE-2023-48795.patch
Normal file
285
backport-0007-CVE-2023-48795.patch
Normal file
@ -0,0 +1,285 @@
|
||||
From e22c5ea330814801d8487dc3da347f987bafe5ec Mon Sep 17 00:00:00 2001
|
||||
From: Jeff Forcier <jeff@bitprophet.org>
|
||||
Date: Thu, 4 May 2023 13:52:40 -0400
|
||||
Subject: [PATCH] Start consolidating test server nonsense
|
||||
|
||||
Reference:https://github.com/paramiko/paramiko/commit/e22c5ea330814801d8487dc3da347f987bafe5ec
|
||||
Conflict:Currently, _util.py does not exist due to different versions. Therefore, the reconstruction code of test_transport.py is still stored in this file
|
||||
The key name must be the same as the current one.
|
||||
|
||||
---
|
||||
tests/test_transport.py | 198 ++++++++++++++++++++++++++++++++++++----
|
||||
1 file changed, 181 insertions(+), 17 deletions(-)
|
||||
|
||||
diff --git a/tests/test_transport.py b/tests/test_transport.py
|
||||
index 4ed712e..6cdbfd6 100644
|
||||
--- a/tests/test_transport.py
|
||||
+++ b/tests/test_transport.py
|
||||
@@ -33,6 +33,7 @@ import random
|
||||
import sys
|
||||
import unittest
|
||||
from mock import Mock
|
||||
+from time import sleep
|
||||
|
||||
from paramiko import (
|
||||
AuthHandler,
|
||||
@@ -1196,6 +1197,146 @@ class AlgorithmDisablingTests(unittest.TestCase):
|
||||
assert "diffie-hellman-group14-sha256" not in kexen
|
||||
assert "zlib" not in compressions
|
||||
|
||||
+_disable_sha2 = dict(
|
||||
+ disabled_algorithms=dict(keys=["rsa-sha2-256", "rsa-sha2-512"])
|
||||
+)
|
||||
+_disable_sha1 = dict(disabled_algorithms=dict(keys=["ssh-rsa"]))
|
||||
+_disable_sha2_pubkey = dict(
|
||||
+ disabled_algorithms=dict(pubkeys=["rsa-sha2-256", "rsa-sha2-512"])
|
||||
+)
|
||||
+_disable_sha1_pubkey = dict(disabled_algorithms=dict(pubkeys=["ssh-rsa"]))
|
||||
+
|
||||
+
|
||||
+unicodey = "\u2022"
|
||||
+
|
||||
+
|
||||
+class TestServer(ServerInterface):
|
||||
+ paranoid_did_password = False
|
||||
+ paranoid_did_public_key = False
|
||||
+ # TODO: make this ed25519 or something else modern? (_is_ this used??)
|
||||
+ 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 check_channel_request(self, kind, chanid):
|
||||
+ if kind == "bogus":
|
||||
+ return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
|
||||
+ return OPEN_SUCCEEDED
|
||||
+
|
||||
+ def check_channel_exec_request(self, channel, command):
|
||||
+ if command != b"yes":
|
||||
+ return False
|
||||
+ return True
|
||||
+
|
||||
+ def check_channel_shell_request(self, channel):
|
||||
+ return True
|
||||
+
|
||||
+ def check_global_request(self, kind, msg):
|
||||
+ self._global_request = kind
|
||||
+ # NOTE: for w/e reason, older impl of this returned False always, even
|
||||
+ # tho that's only supposed to occur if the request cannot be served.
|
||||
+ # For now, leaving that the default unless test supplies specific
|
||||
+ # 'acceptable' request kind
|
||||
+ return kind == "acceptable"
|
||||
+
|
||||
+ def check_channel_x11_request(
|
||||
+ self,
|
||||
+ channel,
|
||||
+ single_connection,
|
||||
+ auth_protocol,
|
||||
+ auth_cookie,
|
||||
+ screen_number,
|
||||
+ ):
|
||||
+ self._x11_single_connection = single_connection
|
||||
+ self._x11_auth_protocol = auth_protocol
|
||||
+ self._x11_auth_cookie = auth_cookie
|
||||
+ self._x11_screen_number = screen_number
|
||||
+ return True
|
||||
+
|
||||
+ def check_port_forward_request(self, addr, port):
|
||||
+ self._listen = socket.socket()
|
||||
+ self._listen.bind(("127.0.0.1", 0))
|
||||
+ self._listen.listen(1)
|
||||
+ return self._listen.getsockname()[1]
|
||||
+
|
||||
+ def cancel_port_forward_request(self, addr, port):
|
||||
+ self._listen.close()
|
||||
+ self._listen = None
|
||||
+
|
||||
+ def check_channel_direct_tcpip_request(self, chanid, origin, destination):
|
||||
+ self._tcpip_dest = destination
|
||||
+ return OPEN_SUCCEEDED
|
||||
+
|
||||
+ def get_allowed_auths(self, username):
|
||||
+ if username == "slowdive":
|
||||
+ return "publickey,password"
|
||||
+ if username == "paranoid":
|
||||
+ if (
|
||||
+ not self.paranoid_did_password
|
||||
+ and not self.paranoid_did_public_key
|
||||
+ ):
|
||||
+ return "publickey,password"
|
||||
+ elif self.paranoid_did_password:
|
||||
+ return "publickey"
|
||||
+ else:
|
||||
+ return "password"
|
||||
+ if username == "commie":
|
||||
+ return "keyboard-interactive"
|
||||
+ if username == "utf8":
|
||||
+ return "password"
|
||||
+ if username == "non-utf8":
|
||||
+ return "password"
|
||||
+ return "publickey"
|
||||
+
|
||||
+ def check_auth_password(self, username, password):
|
||||
+ if (username == "slowdive") and (password == "pygmalion"):
|
||||
+ return AUTH_SUCCESSFUL
|
||||
+ if (username == "paranoid") and (password == "paranoid"):
|
||||
+ # 2-part auth (even openssh doesn't support this)
|
||||
+ self.paranoid_did_password = True
|
||||
+ if self.paranoid_did_public_key:
|
||||
+ return AUTH_SUCCESSFUL
|
||||
+ return AUTH_PARTIALLY_SUCCESSFUL
|
||||
+ if (username == "utf8") and (password == unicodey):
|
||||
+ return AUTH_SUCCESSFUL
|
||||
+ if (username == "non-utf8") and (password == "\xff"):
|
||||
+ return AUTH_SUCCESSFUL
|
||||
+ if username == "bad-server":
|
||||
+ raise Exception("Ack!")
|
||||
+ if username == "unresponsive-server":
|
||||
+ time.sleep(5)
|
||||
+ return AUTH_SUCCESSFUL
|
||||
+ return AUTH_FAILED
|
||||
+
|
||||
+ def check_auth_publickey(self, username, key):
|
||||
+ if (username == "paranoid") and (key == self.paranoid_key):
|
||||
+ # 2-part auth
|
||||
+ self.paranoid_did_public_key = True
|
||||
+ if self.paranoid_did_password:
|
||||
+ return AUTH_SUCCESSFUL
|
||||
+ return AUTH_PARTIALLY_SUCCESSFUL
|
||||
+ # TODO: make sure all tests incidentally using this to pass, _without
|
||||
+ # sending a username oops_, get updated somehow - probably via server()
|
||||
+ # default always injecting a username
|
||||
+ elif key in self.allowed_keys:
|
||||
+ return AUTH_SUCCESSFUL
|
||||
+ return AUTH_FAILED
|
||||
+
|
||||
+ def check_auth_interactive(self, username, submethods):
|
||||
+ if username == "commie":
|
||||
+ self.username = username
|
||||
+ return InteractiveQuery(
|
||||
+ "password", "Please enter a password.", ("Password", False)
|
||||
+ )
|
||||
+ return AUTH_FAILED
|
||||
+
|
||||
+ def check_auth_interactive_response(self, responses):
|
||||
+ if self.username == "commie":
|
||||
+ if (len(responses) == 1) and (responses[0] == "cat"):
|
||||
+ return AUTH_SUCCESSFUL
|
||||
+ return AUTH_FAILED
|
||||
+
|
||||
|
||||
@contextmanager
|
||||
def server(
|
||||
@@ -1206,13 +1347,20 @@ def server(
|
||||
connect=None,
|
||||
pubkeys=None,
|
||||
catch_error=False,
|
||||
+ transport_factory=None,
|
||||
+ server_transport_factory=None,
|
||||
+ defer=False,
|
||||
+ skip_verify=False,
|
||||
):
|
||||
"""
|
||||
SSH server contextmanager for testing.
|
||||
|
||||
+ Yields a tuple of ``(tc, ts)`` (client- and server-side `Transport`
|
||||
+ objects), or ``(tc, ts, err)`` when ``catch_error==True``.
|
||||
+
|
||||
:param hostkey:
|
||||
Host key to use for the server; if None, loads
|
||||
- ``test_rsa.key``.
|
||||
+ ``rsa.key``.
|
||||
:param init:
|
||||
Default `Transport` constructor kwargs to use for both sides.
|
||||
:param server_init:
|
||||
@@ -1226,6 +1374,17 @@ def server(
|
||||
:param catch_error:
|
||||
Whether to capture connection errors & yield from contextmanager.
|
||||
Necessary for connection_time exception testing.
|
||||
+ :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.
|
||||
+ :param bool defer:
|
||||
+ Whether to defer authentication during connecting.
|
||||
+
|
||||
+ This is really just shorthand for ``connect={}`` which would do roughly
|
||||
+ the same thing. Also: this implies skip_verify=True automatically!
|
||||
+ :param bool skip_verify:
|
||||
+ Whether NOT to do the default "make sure auth passed" check.
|
||||
"""
|
||||
if init is None:
|
||||
init = {}
|
||||
@@ -1234,18 +1393,27 @@ 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)
|
||||
- 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"))
|
||||
ts.add_server_key(hostkey)
|
||||
event = threading.Event()
|
||||
- server = NullServer(allowed_keys=pubkeys)
|
||||
+ server = TestServer(allowed_keys=pubkeys)
|
||||
assert not event.is_set()
|
||||
assert not ts.is_active()
|
||||
assert tc.get_username() is None
|
||||
@@ -1273,22 +1441,15 @@ def server(
|
||||
|
||||
yield (tc, ts, err) if catch_error else (tc, ts)
|
||||
|
||||
+ if not (catch_error or skip_verify or defer):
|
||||
+ assert ts.is_authenticated()
|
||||
+ assert tc.is_authenticated()
|
||||
+
|
||||
tc.close()
|
||||
ts.close()
|
||||
socks.close()
|
||||
sockc.close()
|
||||
|
||||
-
|
||||
-_disable_sha2 = dict(
|
||||
- disabled_algorithms=dict(keys=["rsa-sha2-256", "rsa-sha2-512"])
|
||||
-)
|
||||
-_disable_sha1 = dict(disabled_algorithms=dict(keys=["ssh-rsa"]))
|
||||
-_disable_sha2_pubkey = dict(
|
||||
- disabled_algorithms=dict(pubkeys=["rsa-sha2-256", "rsa-sha2-512"])
|
||||
-)
|
||||
-_disable_sha1_pubkey = dict(disabled_algorithms=dict(pubkeys=["ssh-rsa"]))
|
||||
-
|
||||
-
|
||||
class TestSHA2SignatureKeyExchange(unittest.TestCase):
|
||||
# NOTE: these all rely on the default server() hostkey being RSA
|
||||
# NOTE: these rely on both sides being properly implemented re: agreed-upon
|
||||
@@ -1352,7 +1513,10 @@ class TestSHA2SignatureKeyExchange(unittest.TestCase):
|
||||
# the entire preferred-hostkeys structure when given an explicit key as
|
||||
# a client.)
|
||||
hostkey = RSAKey.from_private_key_file(_support("test_rsa.key"))
|
||||
- with server(hostkey=hostkey, connect=dict(hostkey=hostkey)) as (tc, _):
|
||||
+ connect = dict(
|
||||
+ hostkey=hostkey, username="slowdive", password="pygmalion"
|
||||
+ )
|
||||
+ with server(hostkey=hostkey, connect=connect) as (tc, _):
|
||||
assert tc.host_key_type == "rsa-sha2-512"
|
||||
|
||||
|
||||
--
|
||||
2.33.0
|
||||
@ -1,6 +1,6 @@
|
||||
Name: python-paramiko
|
||||
Version: 2.11.0
|
||||
Release: 2
|
||||
Release: 3
|
||||
Summary: Python SSH module
|
||||
License: LGPLv2+
|
||||
URL: https://github.com/paramiko/paramiko
|
||||
@ -11,6 +11,13 @@ Source0: https://github.com/paramiko/paramiko/archive/%{version}/paramiko-
|
||||
Patch6000: backport-Skip-tests-requiring-invoke.patch
|
||||
Patch6001: 0003-remove-pytest-relaxed-dep.patch
|
||||
Patch6002: backport-fix-error-in-sftp-testcase.patch
|
||||
Patch6003: backport-0001-CVE-2023-48795.patch
|
||||
Patch6004: backport-0002-CVE-2023-48795.patch
|
||||
Patch6005: backport-0003-CVE-2023-48795.patch
|
||||
Patch6006: backport-0004-CVE-2023-48795.patch
|
||||
Patch6007: backport-0005-CVE-2023-48795.patch
|
||||
Patch6008: backport-0006-CVE-2023-48795.patch
|
||||
Patch6009: backport-0007-CVE-2023-48795.patch
|
||||
|
||||
BuildArch: noarch
|
||||
|
||||
@ -69,6 +76,9 @@ PYTHONPATH=%{buildroot}%{python3_sitelib} pytest-%{python3_version}
|
||||
%doc html/ demos/ NEWS README.rst
|
||||
|
||||
%changelog
|
||||
* Thu Jan 11 2024 zhangpan <zhangpan103@h-partners.com> - 2.11.0-3
|
||||
- fix CVE-2023-48795
|
||||
|
||||
* Mon Oct 31 2022 chengyechun <chengyechun1@huawei.com> - 2.11.0-2
|
||||
- Type:bugfix
|
||||
- CVE:NA
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user