From ea373f9f4c0b4e13936e16f8ae642b05a4ce21c8 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 9 Dec 2021 17:27:08 -0500 Subject: Weird typos introduced 2 years ago, bah Test currently passes on my workstation tho --- tests/test_transport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_transport.py b/tests/test_transport.py index e2174896..e1e37e47 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -245,7 +245,7 @@ class TransportTest(unittest.TestCase): self.assertEqual(True, self.tc.is_authenticated()) self.assertEqual(True, self.ts.is_authenticated()) - def testa_long_banner(self): + def test_long_banner(self): """ verify that a long banner doesn't mess up the handshake. """ @@ -339,7 +339,7 @@ class TransportTest(unittest.TestCase): self.assertEqual("This is on stderr.\n", f.readline()) self.assertEqual("", f.readline()) - def testa_channel_can_be_used_as_context_manager(self): + def test_channel_can_be_used_as_context_manager(self): """ verify that exec_command() does something reasonable. """ -- cgit v1.2.3 From 5bf2d8ae5de981883dcce49f2275d03f5a7decd6 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 8 Dec 2021 21:19:13 -0500 Subject: Longterm TODOs --- paramiko/auth_handler.py | 3 +++ paramiko/transport.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 5c7d6be6..011e57f3 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -272,6 +272,9 @@ class AuthHandler(object): def _parse_service_accept(self, m): service = m.get_text() if service == "ssh-userauth": + # TODO 3.0: this message sucks ass. change it to something more + # obvious. it always appears to mean "we already authed" but no! it + # just means "we are allowed to TRY authing!" self._log(DEBUG, "userauth is OK") m = Message() m.add_byte(cMSG_USERAUTH_REQUEST) diff --git a/paramiko/transport.py b/paramiko/transport.py index 8919043f..a09ed101 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -2272,6 +2272,14 @@ class Transport(threading.Thread, ClosingContextManager): available_server_keys = list( filter( list(self.server_key_dict.keys()).__contains__, + # TODO: ensure tests will catch if somebody streamlines + # this by mistake - case is the admittedly silly one where + # the only calls to add_server_key() contain keys which + # were filtered out of the below via disabled_algorithms. + # If this is streamlined, we would then be allowing the + # disabled algorithm(s) for hostkey use + # TODO: honestly this prob just wants to get thrown out + # when we make kex configuration more straightforward self.preferred_keys, ) ) @@ -2291,6 +2299,9 @@ class Transport(threading.Thread, ClosingContextManager): m.add_list(self.preferred_compression) m.add_string(bytes()) m.add_string(bytes()) + # TODO: guess Robey never implemented the "guessing" part of the + # protocol. (Transport also never stores or acts on this flag's value + # in _parse_kex_init(), besides logging it to DEBUG.) m.add_boolean(False) m.add_int(0) # save a copy for later (needed to compute a hash) @@ -2351,6 +2362,9 @@ class Transport(threading.Thread, ClosingContextManager): filter(kex_algo_list.__contains__, self.preferred_kex) ) if len(agreed_kex) == 0: + # TODO: do an auth-overhaul style aggregate exception here? + # TODO: would let us streamline log output & show all failures up + # front raise SSHException( "Incompatible ssh peer (no acceptable kex algorithm)" ) # noqa @@ -2877,6 +2891,9 @@ class Transport(threading.Thread, ClosingContextManager): } +# TODO 3.0: drop this, we barely use it ourselves, it badly replicates the +# Transport-internal algorithm management, AND does so in a way which doesn't +# honor newer things like disabled_algorithms! class SecurityOptions(object): """ Simple object containing the security preferences of an ssh transport. -- cgit v1.2.3 From dfffaeaa0170c784307d1c89dad60528a59b6ff2 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 8 Dec 2021 21:20:04 -0500 Subject: Enhance kex DEBUG logging to be more readable The one-liner from 2005 is not cutting it, sorry --- paramiko/transport.py | 51 ++++++++++++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index a09ed101..d4f0b149 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -2176,7 +2176,7 @@ class Transport(threading.Thread, ClosingContextManager): # Log useful, non-duplicative line re: an agreed-upon algorithm. # Old code implied algorithms could be asymmetrical (different for # inbound vs outbound) so we preserve that possibility. - msg = "{} agreed: ".format(which) + msg = "{}: ".format(which) if local == remote: msg += local else: @@ -2323,31 +2323,27 @@ class Transport(threading.Thread, ClosingContextManager): kex_follows = m.get_boolean() m.get_int() # unused - self._log( - DEBUG, - "kex algos:" - + str(kex_algo_list) - + " server key:" - + str(server_key_algo_list) - + " client encrypt:" - + str(client_encrypt_algo_list) - + " server encrypt:" - + str(server_encrypt_algo_list) - + " client mac:" - + str(client_mac_algo_list) - + " server mac:" - + str(server_mac_algo_list) - + " client compress:" - + str(client_compress_algo_list) - + " server compress:" - + str(server_compress_algo_list) - + " client lang:" - + str(client_lang_list) - + " server lang:" - + str(server_lang_list) - + " kex follows?" - + str(kex_follows), - ) + self._log(DEBUG, "=== Key exchange possibilities ===") + for prefix, value in ( + ("kex algos", kex_algo_list), + ("server key", server_key_algo_list), + # TODO: shouldn't these two lines say "cipher" to match usual + # terminology (including elsewhere in paramiko!)? + ("client encrypt", client_encrypt_algo_list), + ("server encrypt", server_encrypt_algo_list), + ("client mac", client_mac_algo_list), + ("server mac", server_mac_algo_list), + ("client compress", client_compress_algo_list), + ("server compress", server_compress_algo_list), + ("client lang", client_lang_list), + ("server lang", server_lang_list), + ): + if value == [""]: + value = [""] + value = ", ".join(value) + self._log(DEBUG, "{}: {}".format(prefix, value)) + self._log(DEBUG, "kex follows: {}".format(kex_follows)) + self._log(DEBUG, "=== Key exchange agreements ===") # as a server, we pick the first item in the client's list that we # support. @@ -2369,7 +2365,7 @@ class Transport(threading.Thread, ClosingContextManager): "Incompatible ssh peer (no acceptable kex algorithm)" ) # noqa self.kex_engine = self._kex_info[agreed_kex[0]](self) - self._log(DEBUG, "Kex agreed: {}".format(agreed_kex[0])) + self._log(DEBUG, "Kex: {}".format(agreed_kex[0])) if self.server_mode: available_server_keys = list( @@ -2502,6 +2498,7 @@ class Transport(threading.Thread, ClosingContextManager): local=self.local_compression, remote=self.remote_compression, ) + self._log(DEBUG, "=== End of kex handshake ===") # save for computing hash later... # now wait! openssh has a bug (and others might too) where there are -- cgit v1.2.3 From 363a28d94cada17f012c1604a3c99c71a2bda003 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 13 Dec 2021 15:55:36 -0500 Subject: Add support for RSA SHA2 host and public keys Includes a handful of refactors and new semiprivate attributes on Transport and AuthHandler for better test visibility. --- paramiko/__init__.py | 1 + paramiko/agent.py | 2 +- paramiko/auth_handler.py | 81 +++++++++++--- paramiko/common.py | 5 +- paramiko/dsskey.py | 2 +- paramiko/ecdsakey.py | 2 +- paramiko/ed25519key.py | 2 +- paramiko/kex_curve25519.py | 4 +- paramiko/kex_ecdh_nist.py | 4 +- paramiko/kex_gex.py | 4 +- paramiko/kex_group1.py | 4 +- paramiko/pkey.py | 11 +- paramiko/rsakey.py | 28 ++++- paramiko/ssh_exception.py | 15 +++ paramiko/transport.py | 179 ++++++++++++++++++++++++------ sites/www/changelog.rst | 64 +++++++++++ tests/test_kex.py | 3 +- tests/test_pkey.py | 26 +++-- tests/test_transport.py | 269 +++++++++++++++++++++++++++++++++++++++++++++ 19 files changed, 631 insertions(+), 75 deletions(-) diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 8642f84a..5318cc9c 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -42,6 +42,7 @@ from paramiko.ssh_exception import ( ChannelException, ConfigParseError, CouldNotCanonicalize, + IncompatiblePeer, PasswordRequiredException, ProxyCommandFailure, SSHException, diff --git a/paramiko/agent.py b/paramiko/agent.py index c7c8b7cb..3a02c06c 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -411,7 +411,7 @@ class AgentKey(PKey): def _fields(self): raise NotImplementedError - def sign_ssh_data(self, data): + def sign_ssh_data(self, data, algorithm=None): msg = Message() msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST) msg.add_string(self.blob) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 011e57f3..845b9143 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -61,7 +61,7 @@ from paramiko.common import ( cMSG_USERAUTH_BANNER, ) from paramiko.message import Message -from paramiko.py3compat import b +from paramiko.py3compat import b, u from paramiko.ssh_exception import ( SSHException, AuthenticationException, @@ -206,6 +206,23 @@ class AuthHandler(object): self.transport._send_message(m) self.transport.close() + def _get_algorithm_and_bits(self, key): + """ + Given any key, return appropriate signing algorithm & bits-to-sign. + + Intended for input to or verification of, key signatures. + """ + key_type, bits = None, None + # Use certificate contents, if available, plain pubkey otherwise + if key.public_blob: + key_type = key.public_blob.key_type + bits = key.public_blob.key_blob + else: + key_type = key.get_name() + bits = key + algorithm = self._finalize_pubkey_algorithm(key_type) + return algorithm, bits + def _get_session_blob(self, key, service, username): m = Message() m.add_string(self.transport.session_id) @@ -214,13 +231,9 @@ class AuthHandler(object): m.add_string(service) m.add_string("publickey") m.add_boolean(True) - # Use certificate contents, if available, plain pubkey otherwise - if key.public_blob: - m.add_string(key.public_blob.key_type) - m.add_string(key.public_blob.key_blob) - else: - m.add_string(key.get_name()) - m.add_string(key) + algorithm, bits = self._get_algorithm_and_bits(key) + m.add_string(algorithm) + m.add_string(bits) return m.asbytes() def wait_for_response(self, event): @@ -269,6 +282,39 @@ class AuthHandler(object): # dunno this one self._disconnect_service_not_available() + def _finalize_pubkey_algorithm(self, key_type): + # Short-circuit for non-RSA keys + if "rsa" not in key_type: + return key_type + self._log( + DEBUG, + "Finalizing pubkey algorithm for key of type {!r}".format( + key_type + ), + ) + # Only consider RSA algos from our list, lest we agree on another! + my_algos = [x for x in self.transport.preferred_pubkeys if "rsa" in x] + self._log(DEBUG, "Our pubkey algorithm list: {}".format(my_algos)) + # Short-circuit negatively if user disabled all RSA algos (heh) + if not my_algos: + raise SSHException( + "An RSA key was specified, but no RSA pubkey algorithms are configured!" # noqa + ) + # Check for server-sig-algs if supported & sent + server_algos = u( + self.transport.server_extensions.get("server-sig-algs", b("")) + ).split(",") + self._log(DEBUG, "Server-side algorithm list: {}".format(server_algos)) + # Only use algos from our list that the server likes, in our own + # preference order. (NOTE: purposefully using same style as in + # Transport...expect to refactor later) + agreement = list(filter(server_algos.__contains__, my_algos)) + # Fallback: first one in our (possibly tweaked by caller) list + final = agreement[0] if agreement else my_algos[0] + self.transport._agreed_pubkey_algorithm = final + self._log(DEBUG, "Agreed upon {!r} pubkey algorithm".format(final)) + return final + def _parse_service_accept(self, m): service = m.get_text() if service == "ssh-userauth": @@ -287,18 +333,15 @@ class AuthHandler(object): m.add_string(password) elif self.auth_method == "publickey": m.add_boolean(True) - # Use certificate contents, if available, plain pubkey - # otherwise - if self.private_key.public_blob: - m.add_string(self.private_key.public_blob.key_type) - m.add_string(self.private_key.public_blob.key_blob) - else: - m.add_string(self.private_key.get_name()) - m.add_string(self.private_key) + algorithm, bits = self._get_algorithm_and_bits( + self.private_key + ) + m.add_string(algorithm) + m.add_string(bits) blob = self._get_session_blob( self.private_key, "ssh-connection", self.username ) - sig = self.private_key.sign_ssh_data(blob) + sig = self.private_key.sign_ssh_data(blob, algorithm) m.add_string(sig) elif self.auth_method == "keyboard-interactive": m.add_string("") @@ -529,13 +572,15 @@ Error Message: {} username, key ) if result != AUTH_FAILED: + sig_algo = self._finalize_pubkey_algorithm(keytype) # key is okay, verify it if not sig_attached: # client wants to know if this key is acceptable, before it # signs anything... send special "ok" message m = Message() m.add_byte(cMSG_USERAUTH_PK_OK) - m.add_string(keytype) + # TODO: suspect we're not testing this + m.add_string(sig_algo) m.add_string(keyblob) self.transport._send_message(m) return diff --git a/paramiko/common.py b/paramiko/common.py index 7bd0cb10..55dd4bdf 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -29,7 +29,8 @@ from paramiko.py3compat import byte_chr, PY2, long, b MSG_DEBUG, MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT, -) = range(1, 7) + MSG_EXT_INFO, +) = range(1, 8) (MSG_KEXINIT, MSG_NEWKEYS) = range(20, 22) ( MSG_USERAUTH_REQUEST, @@ -68,6 +69,7 @@ cMSG_UNIMPLEMENTED = byte_chr(MSG_UNIMPLEMENTED) cMSG_DEBUG = byte_chr(MSG_DEBUG) cMSG_SERVICE_REQUEST = byte_chr(MSG_SERVICE_REQUEST) cMSG_SERVICE_ACCEPT = byte_chr(MSG_SERVICE_ACCEPT) +cMSG_EXT_INFO = byte_chr(MSG_EXT_INFO) cMSG_KEXINIT = byte_chr(MSG_KEXINIT) cMSG_NEWKEYS = byte_chr(MSG_NEWKEYS) cMSG_USERAUTH_REQUEST = byte_chr(MSG_USERAUTH_REQUEST) @@ -109,6 +111,7 @@ MSG_NAMES = { MSG_SERVICE_REQUEST: "service-request", MSG_SERVICE_ACCEPT: "service-accept", MSG_KEXINIT: "kexinit", + MSG_EXT_INFO: "ext-info", MSG_NEWKEYS: "newkeys", 30: "kex30", 31: "kex31", diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index 09d6f648..1a0c4797 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -105,7 +105,7 @@ class DSSKey(PKey): def can_sign(self): return self.x is not None - def sign_ssh_data(self, data): + def sign_ssh_data(self, data, algorithm=None): key = dsa.DSAPrivateNumbers( x=self.x, public_numbers=dsa.DSAPublicNumbers( diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index b609d130..c4e2b1af 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -211,7 +211,7 @@ class ECDSAKey(PKey): def can_sign(self): return self.signing_key is not None - def sign_ssh_data(self, data): + def sign_ssh_data(self, data, algorithm=None): ecdsa = ec.ECDSA(self.ecdsa_curve.hash_object()) sig = self.signing_key.sign(data, ecdsa) r, s = decode_dss_signature(sig) diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py index 7b19e352..d322a0c1 100644 --- a/paramiko/ed25519key.py +++ b/paramiko/ed25519key.py @@ -191,7 +191,7 @@ class Ed25519Key(PKey): def can_sign(self): return self._signing_key is not None - def sign_ssh_data(self, data): + def sign_ssh_data(self, data, algorithm=None): m = Message() m.add_string("ssh-ed25519") m.add_string(self._signing_key.sign(data).signature) diff --git a/paramiko/kex_curve25519.py b/paramiko/kex_curve25519.py index 59710c1a..3420fb4f 100644 --- a/paramiko/kex_curve25519.py +++ b/paramiko/kex_curve25519.py @@ -89,7 +89,9 @@ class KexCurve25519(object): hm.add_mpint(K) H = self.hash_algo(hm.asbytes()).digest() self.transport._set_K_H(K, H) - sig = self.transport.get_server_key().sign_ssh_data(H) + sig = self.transport.get_server_key().sign_ssh_data( + H, self.transport.host_key_type + ) # construct reply m = Message() m.add_byte(c_MSG_KEXECDH_REPLY) diff --git a/paramiko/kex_ecdh_nist.py b/paramiko/kex_ecdh_nist.py index ad5c9c79..19de2431 100644 --- a/paramiko/kex_ecdh_nist.py +++ b/paramiko/kex_ecdh_nist.py @@ -90,7 +90,9 @@ class KexNistp256: hm.add_mpint(long(K)) H = self.hash_algo(hm.asbytes()).digest() self.transport._set_K_H(K, H) - sig = self.transport.get_server_key().sign_ssh_data(H) + sig = self.transport.get_server_key().sign_ssh_data( + H, self.transport.host_key_type + ) # construct reply m = Message() m.add_byte(c_MSG_KEXECDH_REPLY) diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index fb8f01fd..ab462e6d 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -240,7 +240,9 @@ class KexGex(object): H = self.hash_algo(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it - sig = self.transport.get_server_key().sign_ssh_data(H) + sig = self.transport.get_server_key().sign_ssh_data( + H, self.transport.host_key_type + ) # send reply m = Message() m.add_byte(c_MSG_KEXDH_GEX_REPLY) diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index dce3fd91..6d548b01 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -143,7 +143,9 @@ class KexGroup1(object): H = self.hash_algo(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it - sig = self.transport.get_server_key().sign_ssh_data(H) + sig = self.transport.get_server_key().sign_ssh_data( + H, self.transport.host_key_type + ) # send reply m = Message() m.add_byte(c_MSG_KEXDH_REPLY) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 5bdfb1d4..7865a6ea 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -196,13 +196,20 @@ class PKey(object): """ return u(encodebytes(self.asbytes())).replace("\n", "") - def sign_ssh_data(self, data): + def sign_ssh_data(self, data, algorithm=None): """ Sign a blob of data with this private key, and return a `.Message` representing an SSH signature message. - :param str data: the data to sign. + :param str data: + the data to sign. + :param str algorithm: + the signature algorithm to use, if different from the key's + internal name. Default: ``None``. :return: an SSH signature `message <.Message>`. + + .. versionchanged:: 2.9 + Added the ``algorithm`` kwarg. """ return bytes() diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index 292d0ccc..26c5313c 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -37,6 +37,15 @@ class RSAKey(PKey): data. """ + HASHES = { + "ssh-rsa": hashes.SHA1, + "ssh-rsa-cert-v01@openssh.com": hashes.SHA1, + "rsa-sha2-256": hashes.SHA256, + "rsa-sha2-256-cert-v01@openssh.com": hashes.SHA256, + "rsa-sha2-512": hashes.SHA512, + "rsa-sha2-512-cert-v01@openssh.com": hashes.SHA512, + } + def __init__( self, msg=None, @@ -61,6 +70,8 @@ class RSAKey(PKey): else: self._check_type_and_load_cert( msg=msg, + # NOTE: this does NOT change when using rsa2 signatures; it's + # purely about key loading, not exchange or verification key_type="ssh-rsa", cert_type="ssh-rsa-cert-v01@openssh.com", ) @@ -111,18 +122,20 @@ class RSAKey(PKey): def can_sign(self): return isinstance(self.key, rsa.RSAPrivateKey) - def sign_ssh_data(self, data): + def sign_ssh_data(self, data, algorithm="ssh-rsa"): sig = self.key.sign( - data, padding=padding.PKCS1v15(), algorithm=hashes.SHA1() + data, + padding=padding.PKCS1v15(), + algorithm=self.HASHES[algorithm](), ) - m = Message() - m.add_string("ssh-rsa") + m.add_string(algorithm) m.add_string(sig) return m def verify_ssh_sig(self, data, msg): - if msg.get_text() != "ssh-rsa": + sig_algorithm = msg.get_text() + if sig_algorithm not in self.HASHES: return False key = self.key if isinstance(key, rsa.RSAPrivateKey): @@ -130,7 +143,10 @@ class RSAKey(PKey): try: key.verify( - msg.get_binary(), data, padding.PKCS1v15(), hashes.SHA1() + msg.get_binary(), + data, + padding.PKCS1v15(), + self.HASHES[sig_algorithm](), ) except InvalidSignature: return False diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 2789be99..39fcb10d 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -135,6 +135,21 @@ class BadHostKeyException(SSHException): ) +class IncompatiblePeer(SSHException): + """ + A disagreement arose regarding an algorithm required for key exchange. + + .. versionadded:: 2.9 + """ + + # TODO 3.0: consider making this annotate w/ 1..N 'missing' algorithms, + # either just the first one that would halt kex, or even updating the + # Transport logic so we record /all/ that /could/ halt kex. + # TODO: update docstrings where this may end up raised so they are more + # specific. + pass + + class ProxyCommandFailure(SSHException): """ The "ProxyCommand" found in the .ssh/config file returned an error. diff --git a/paramiko/transport.py b/paramiko/transport.py index d4f0b149..b99b3278 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -84,6 +84,8 @@ from paramiko.common import ( HIGHEST_USERAUTH_MESSAGE_ID, MSG_UNIMPLEMENTED, MSG_NAMES, + MSG_EXT_INFO, + cMSG_EXT_INFO, ) from paramiko.compress import ZlibCompressor, ZlibDecompressor from paramiko.dsskey import DSSKey @@ -107,6 +109,7 @@ from paramiko.ssh_exception import ( SSHException, BadAuthenticationType, ChannelException, + IncompatiblePeer, ProxyCommandFailure, ) from paramiko.util import retry_on_signal, ClosingContextManager, clamp_value @@ -168,11 +171,25 @@ class Transport(threading.Thread, ClosingContextManager): "hmac-sha1-96", "hmac-md5-96", ) + # ~= HostKeyAlgorithms in OpenSSH land _preferred_keys = ( "ssh-ed25519", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", + "rsa-sha2-512", + "rsa-sha2-256", + "ssh-rsa", + "ssh-dss", + ) + # ~= PubKeyAcceptedAlgorithms + _preferred_pubkeys = ( + "ssh-ed25519", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521", + "rsa-sha2-512", + "rsa-sha2-256", "ssh-rsa", "ssh-dss", ) @@ -259,8 +276,16 @@ class Transport(threading.Thread, ClosingContextManager): } _key_info = { + # TODO: at some point we will want to drop this as it's no longer + # considered secure due to using SHA-1 for signatures. OpenSSH 8.8 no + # longer supports it. Question becomes at what point do we want to + # prevent users with older setups from using this? "ssh-rsa": RSAKey, "ssh-rsa-cert-v01@openssh.com": RSAKey, + "rsa-sha2-256": RSAKey, + "rsa-sha2-256-cert-v01@openssh.com": RSAKey, + "rsa-sha2-512": RSAKey, + "rsa-sha2-512-cert-v01@openssh.com": RSAKey, "ssh-dss": DSSKey, "ssh-dss-cert-v01@openssh.com": DSSKey, "ecdsa-sha2-nistp256": ECDSAKey, @@ -310,6 +335,7 @@ class Transport(threading.Thread, ClosingContextManager): gss_kex=False, gss_deleg_creds=True, disabled_algorithms=None, + server_sig_algs=True, ): """ Create a new SSH session over an existing socket, or socket-like @@ -372,6 +398,10 @@ class Transport(threading.Thread, ClosingContextManager): your code talks to a server which implements it differently from Paramiko), specify ``disabled_algorithms={"kex": ["diffie-hellman-group16-sha512"]}``. + :param bool server_sig_algs: + Whether to send an extra message to compatible clients, in server + mode, with a list of supported pubkey algorithms. Default: + ``True``. .. versionchanged:: 1.15 Added the ``default_window_size`` and ``default_max_packet_size`` @@ -380,9 +410,12 @@ class Transport(threading.Thread, ClosingContextManager): Added the ``gss_kex`` and ``gss_deleg_creds`` kwargs. .. versionchanged:: 2.6 Added the ``disabled_algorithms`` kwarg. + .. versionchanged:: 2.9 + Added the ``server_sig_algs`` kwarg. """ self.active = False self.hostname = None + self.server_extensions = {} if isinstance(sock, string_types): # convert "host:port" into (host, port) @@ -488,6 +521,7 @@ class Transport(threading.Thread, ClosingContextManager): # how long (seconds) to wait for the auth response. self.auth_timeout = 30 self.disabled_algorithms = disabled_algorithms or {} + self.server_sig_algs = server_sig_algs # server mode: self.server_mode = False @@ -517,6 +551,10 @@ class Transport(threading.Thread, ClosingContextManager): def preferred_keys(self): return self._filter_algorithm("keys") + @property + def preferred_pubkeys(self): + return self._filter_algorithm("pubkeys") + @property def preferred_kex(self): return self._filter_algorithm("kex") @@ -743,6 +781,12 @@ class Transport(threading.Thread, ClosingContextManager): the host key to add, usually an `.RSAKey` or `.DSSKey`. """ self.server_key_dict[key.get_name()] = key + # Handle SHA-2 extensions for RSA by ensuring that lookups into + # self.server_key_dict will yield this key for any of the algorithm + # names. + if isinstance(key, RSAKey): + self.server_key_dict["rsa-sha2-256"] = key + self.server_key_dict["rsa-sha2-512"] = key def get_server_key(self): """ @@ -1280,7 +1324,17 @@ class Transport(threading.Thread, ClosingContextManager): Added the ``gss_trust_dns`` argument. """ if hostkey is not None: - self._preferred_keys = [hostkey.get_name()] + # TODO: a more robust implementation would be to ask each key class + # for its nameS plural, and just use that. + # TODO: that could be used in a bunch of other spots too + if isinstance(hostkey, RSAKey): + self._preferred_keys = [ + "rsa-sha2-512", + "rsa-sha2-256", + "ssh-rsa", + ] + else: + self._preferred_keys = [hostkey.get_name()] self.set_gss_host( gss_host=gss_host, @@ -2126,7 +2180,12 @@ class Transport(threading.Thread, ClosingContextManager): self._send_message(msg) self.packetizer.complete_handshake() except SSHException as e: - self._log(ERROR, "Exception: " + str(e)) + self._log( + ERROR, + "Exception ({}): {}".format( + "server" if self.server_mode else "client", e + ), + ) self._log(ERROR, util.tb_strings()) self.saved_exception = e except EOFError as e: @@ -2237,7 +2296,7 @@ class Transport(threading.Thread, ClosingContextManager): client = segs[2] if version != "1.99" and version != "2.0": msg = "Incompatible version ({} instead of 2.0)" - raise SSHException(msg.format(version)) + raise IncompatiblePeer(msg.format(version)) msg = "Connected (version {}, client {})".format(version, client) self._log(INFO, msg) @@ -2253,13 +2312,10 @@ class Transport(threading.Thread, ClosingContextManager): 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 = [ - k - for k in self.preferred_kex - if k.startswith(mp_required_prefix) - ] + kex_mp = [k for k in kex_algos if k.startswith(mp_required_prefix)] if (self._modulus_pack is None) and (len(kex_mp) > 0): # can't do group-exchange if we don't have a pack of potential # primes @@ -2285,11 +2341,16 @@ class Transport(threading.Thread, ClosingContextManager): ) else: available_server_keys = self.preferred_keys + # Signal support for MSG_EXT_INFO. + # 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") 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) @@ -2299,29 +2360,49 @@ class Transport(threading.Thread, ClosingContextManager): m.add_list(self.preferred_compression) m.add_string(bytes()) m.add_string(bytes()) - # TODO: guess Robey never implemented the "guessing" part of the - # protocol. (Transport also never stores or acts on this flag's value - # in _parse_kex_init(), besides logging it to DEBUG.) 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, "=== Key exchange possibilities ===") for prefix, value in ( @@ -2345,6 +2426,11 @@ 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" + self._remote_ext_info = None + if kex_algo_list[-1].startswith("ext-info-"): + self._remote_ext_info = kex_algo_list.pop() + # 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 @@ -2361,7 +2447,7 @@ class Transport(threading.Thread, ClosingContextManager): # TODO: do an auth-overhaul style aggregate exception here? # TODO: would let us streamline log output & show all failures up # front - raise SSHException( + raise IncompatiblePeer( "Incompatible ssh peer (no acceptable kex algorithm)" ) # noqa self.kex_engine = self._kex_info[agreed_kex[0]](self) @@ -2384,12 +2470,12 @@ class Transport(threading.Thread, ClosingContextManager): filter(server_key_algo_list.__contains__, self.preferred_keys) ) if len(agreed_keys) == 0: - raise SSHException( + raise IncompatiblePeer( "Incompatible ssh peer (no acceptable host key)" ) # noqa self.host_key_type = agreed_keys[0] if self.server_mode and (self.get_server_key() is None): - raise SSHException( + raise IncompatiblePeer( "Incompatible ssh peer (can't match requested host key type)" ) # noqa self._log_agreement("HostKey", agreed_keys[0], agreed_keys[0]) @@ -2421,7 +2507,7 @@ class Transport(threading.Thread, ClosingContextManager): ) ) if len(agreed_local_ciphers) == 0 or len(agreed_remote_ciphers) == 0: - raise SSHException( + raise IncompatiblePeer( "Incompatible ssh server (no acceptable ciphers)" ) # noqa self.local_cipher = agreed_local_ciphers[0] @@ -2445,7 +2531,9 @@ class Transport(threading.Thread, ClosingContextManager): filter(server_mac_algo_list.__contains__, self.preferred_macs) ) if (len(agreed_local_macs) == 0) or (len(agreed_remote_macs) == 0): - raise SSHException("Incompatible ssh server (no acceptable macs)") + raise IncompatiblePeer( + "Incompatible ssh server (no acceptable macs)" + ) self.local_mac = agreed_local_macs[0] self.remote_mac = agreed_remote_macs[0] self._log_agreement( @@ -2484,7 +2572,7 @@ class Transport(threading.Thread, ClosingContextManager): ): msg = "Incompatible ssh server (no acceptable compression)" msg += " {!r} {!r} {!r}" - raise SSHException( + raise IncompatiblePeer( msg.format( agreed_local_compression, agreed_remote_compression, @@ -2584,6 +2672,20 @@ class Transport(threading.Thread, ClosingContextManager): self.packetizer.set_outbound_compressor(compress_out()) if not self.packetizer.need_rekey(): self.in_kex = False + # If client indicated extension support, send that packet immediately + if ( + self.server_mode + and self.server_sig_algs + and self._remote_ext_info == "ext-info-c" + ): + extensions = {"server-sig-algs": ",".join(self.preferred_pubkeys)} + m = Message() + m.add_byte(cMSG_EXT_INFO) + m.add_int(len(extensions)) + for name, value in sorted(extensions.items()): + m.add_string(name) + m.add_string(value) + self._send_message(m) # we always expect to receive NEWKEYS now self._expect_packet(MSG_NEWKEYS) @@ -2599,6 +2701,20 @@ class Transport(threading.Thread, ClosingContextManager): self._log(DEBUG, "Switching on inbound compression ...") self.packetizer.set_inbound_compressor(compress_in()) + def _parse_ext_info(self, msg): + # Packet is a count followed by that many key-string to possibly-bytes + # pairs. + extensions = {} + for _ in range(msg.get_int()): + name = msg.get_text() + value = msg.get_string() + extensions[name] = value + self._log(DEBUG, "Got EXT_INFO: {}".format(extensions)) + # NOTE: this should work ok in cases where a server sends /two/ such + # messages; the RFC explicitly states a 2nd one should overwrite the + # 1st. + self.server_extensions = extensions + def _parse_newkeys(self, m): self._log(DEBUG, "Switch to new keys ...") self._activate_inbound() @@ -2866,6 +2982,7 @@ class Transport(threading.Thread, ClosingContextManager): 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, diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index c423f5a5..016a5ac9 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,70 @@ Changelog ========= +- :feature:`1643` Add support for SHA-2 variants of RSA key verification + algorithms (as described in :rfc:`8332`) as well as limited SSH extension + negotiation (:rfc:`8308`). How SSH servers/clients decide when and how to use + this functionality can be complicated; Paramiko's support is as follows: + + - Client verification of server host key during key exchange will now prefer + ``rsa-sha2-512``, ``rsa-sha2-256``, and legacy ``ssh-rsa`` algorithms, in + that order, instead of just ``ssh-rsa``. + + - Note that the preference order of other algorithm families such as + ``ed25519`` and ``ecdsa`` has not changed; for example, those two + groups are still preferred over RSA. + + - Server mode will now offer all 3 RSA algorithms for host key verification + during key exchange, similar to client mode, if it has been configured with + an RSA host key. + - Client mode key exchange now sends the ``ext-info-c`` flag signaling + support for ``MSG_EXT_INFO``, and support for parsing the latter + (specifically, its ``server-sig-algs`` flag) has been added. + - Client mode, when performing public key authentication with an RSA key or + cert, will act as follows: + + - In all cases, the list of algorithms to consider is based on the new + ``preferred_pubkeys`` list (see below) and ``disabled_algorithms``; this + list, like with host keys, prefers SHA2-512, SHA2-256 and SHA1, in that + order. + - When the server does not send ``server-sig-algs``, Paramiko will attempt + the first algorithm in the above list. Clients connecting to legacy + servers should thus use ``disabled_algorithms`` to turn off SHA2. + - When the server does send ``server-sig-algs``, the first algorithm + supported by both ends is used, or if there is none, it falls back to the + previous behavior. + + - Server mode is now capable of pubkey auth involving SHA-2 signatures from + clients, provided one's server implementation actually provides for doing + so. + + - This includes basic support for sending ``MSG_EXT_INFO`` (containing + ``server-sig-algs`` only) to clients advertising ``ext-info-c`` in their + key exchange list. + + In order to implement the above, the following API additions were made: + + - `PKey.sign_ssh_data `: Grew an extra, optional + ``algorithm`` keyword argument (defaulting to ``None`` for most subclasses, + and to ``"ssh-rsa"`` for `~paramiko.rsakey.RSAKey`). + - A new `~paramiko.ssh_exception.SSHException` subclass was added, + `~paramiko.ssh_exception.IncompatiblePeer`, and is raised in all spots + where key exchange aborts due to algorithmic incompatibility. + + - Like all other exceptions in that module, it inherits from + ``SSHException``, and as we did not change anything else about the raising + (i.e. the attributes and message text are the same) this change is + backwards compatible. + + - `~paramiko.transport.Transport` grew a ``_preferred_pubkeys`` attribute and + matching ``preferred_pubkeys`` property to match the other, kex-focused, + such members. This allows client pubkey authentication to honor the + ``disabled_algorithms`` feature. + + Thanks to Krisztián Kovács for the report and an early stab at a patch, as + well as the numerous users who submitted feedback on the issue, including but + not limited to: Christopher Rabotin, Sam Bull, and Manfred Kaiser. + - :release:`2.8.1 <2021-11-28>` - :bug:`985` (via :issue:`992`) Fix listdir failure when server uses a locale. Now on Python 2.7 `SFTPAttributes ` will diff --git a/tests/test_kex.py b/tests/test_kex.py index c251611a..b73989c2 100644 --- a/tests/test_kex.py +++ b/tests/test_kex.py @@ -76,7 +76,7 @@ class FakeKey(object): def asbytes(self): return b"fake-key" - def sign_ssh_data(self, H): + def sign_ssh_data(self, H, algorithm): return b"fake-sig" @@ -93,6 +93,7 @@ class FakeTransport(object): remote_version = "SSH-2.0-lame" local_kex_init = "local-kex-init" remote_kex_init = "remote-kex-init" + host_key_type = "fake-key" def _send_message(self, m): self._message = m diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 94b2492b..0cc20133 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -63,6 +63,8 @@ FINGER_ECDSA_256 = "256 25:19:eb:55:e6:a1:47:ff:4f:38:d2:75:6f:a5:d5:60" FINGER_ECDSA_384 = "384 c1:8d:a0:59:09:47:41:8e:a8:a6:07:01:29:23:b4:65" FINGER_ECDSA_521 = "521 44:58:22:52:12:33:16:0e:ce:0e:be:2c:7c:7e:cc:1e" SIGNED_RSA = "20:d7:8a:31:21:cb:f7:92:12:f2:a4:89:37:f5:78:af:e6:16:b6:25:b9:97:3d:a2:cd:5f:ca:20:21:73:4c:ad:34:73:8f:20:77:28:e2:94:15:08:d8:91:40:7a:85:83:bf:18:37:95:dc:54:1a:9b:88:29:6c:73:ca:38:b4:04:f1:56:b9:f2:42:9d:52:1b:29:29:b4:4f:fd:c9:2d:af:47:d2:40:76:30:f3:63:45:0c:d9:1d:43:86:0f:1c:70:e2:93:12:34:f3:ac:c5:0a:2f:14:50:66:59:f1:88:ee:c1:4a:e9:d1:9c:4e:46:f0:0e:47:6f:38:74:f1:44:a8" # noqa +SIGNED_RSA_256 = "cc:6:60:e0:0:2c:ac:9e:26:bc:d5:68:64:3f:9f:a7:e5:aa:41:eb:88:4a:25:5:9c:93:84:66:ef:ef:60:f4:34:fb:f4:c8:3d:55:33:6a:77:bd:b2:ee:83:f:71:27:41:7e:f5:7:5:0:a9:4c:7:80:6f:be:76:67:cb:58:35:b9:2b:f3:c2:d3:3c:ee:e1:3f:59:e0:fa:e4:5c:92:ed:ae:74:de:d:d6:27:16:8f:84:a3:86:68:c:94:90:7d:6e:cc:81:12:d8:b6:ad:aa:31:a8:13:3d:63:81:3e:bb:5:b6:38:4d:2:d:1b:5b:70:de:83:cc:3a:cb:31" # noqa +SIGNED_RSA_512 = "87:46:8b:75:92:33:78:a0:22:35:32:39:23:c6:ab:e1:6:92:ad:bc:7f:6e:ab:19:32:e4:78:b2:2c:8f:1d:c:65:da:fc:a5:7:ca:b6:55:55:31:83:b1:a0:af:d1:95:c5:2e:af:56:ba:f5:41:64:f:39:9d:af:82:43:22:8f:90:52:9d:89:e7:45:97:df:f3:f2:bc:7b:3a:db:89:e:34:fd:18:62:25:1b:ef:77:aa:c6:6c:99:36:3a:84:d6:9c:2a:34:8c:7f:f4:bb:c9:a5:9a:6c:11:f2:cf:da:51:5e:1e:7f:90:27:34:de:b2:f3:15:4f:db:47:32:6b:a7" # noqa FINGER_RSA_2K_OPENSSH = "2048 68:d1:72:01:bf:c0:0c:66:97:78:df:ce:75:74:46:d6" FINGER_DSS_1K_OPENSSH = "1024 cf:1d:eb:d7:61:d3:12:94:c6:c0:c6:54:35:35:b0:82" FINGER_EC_384_OPENSSH = "384 72:14:df:c1:9a:c3:e6:0e:11:29:d6:32:18:7b:ea:9b" @@ -238,21 +240,29 @@ class KeyTest(unittest.TestCase): self.assertTrue(not pub.can_sign()) self.assertEqual(key, pub) - def test_sign_rsa(self): - # verify that the rsa private key can sign and verify + def _sign_and_verify_rsa(self, algorithm, saved_sig): key = RSAKey.from_private_key_file(_support("test_rsa.key")) - msg = key.sign_ssh_data(b"ice weasels") - self.assertTrue(type(msg) is Message) + msg = key.sign_ssh_data(b"ice weasels", algorithm) + assert isinstance(msg, Message) msg.rewind() - self.assertEqual("ssh-rsa", msg.get_text()) - sig = bytes().join( - [byte_chr(int(x, 16)) for x in SIGNED_RSA.split(":")] + assert msg.get_text() == algorithm + expected = bytes().join( + [byte_chr(int(x, 16)) for x in saved_sig.split(":")] ) - self.assertEqual(sig, msg.get_binary()) + assert msg.get_binary() == expected msg.rewind() pub = RSAKey(data=key.asbytes()) self.assertTrue(pub.verify_ssh_sig(b"ice weasels", msg)) + def test_sign_and_verify_ssh_rsa(self): + self._sign_and_verify_rsa("ssh-rsa", SIGNED_RSA) + + def test_sign_and_verify_rsa_sha2_512(self): + self._sign_and_verify_rsa("rsa-sha2-512", SIGNED_RSA_512) + + def test_sign_and_verify_rsa_sha2_256(self): + self._sign_and_verify_rsa("rsa-sha2-256", SIGNED_RSA_256) + def test_sign_dss(self): # verify that the dss private key can sign and verify key = DSSKey.from_private_key_file(_support("test_dss.key")) diff --git a/tests/test_transport.py b/tests/test_transport.py index e1e37e47..6145e5cb 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -23,6 +23,7 @@ Some unit tests for the ssh2 protocol in Transport. from __future__ import with_statement from binascii import hexlify +from contextlib import contextmanager import select import socket import time @@ -38,6 +39,8 @@ from paramiko import ( Packetizer, RSAKey, SSHException, + AuthenticationException, + IncompatiblePeer, SecurityOptions, ServerInterface, Transport, @@ -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 @@ -154,6 +165,7 @@ class TransportTest(unittest.TestCase): self.socks.close() self.sockc.close() + # TODO: unify with newer contextmanager def setup_test_server( self, client_options=None, server_options=None, connect_kwargs=None ): @@ -1169,3 +1181,260 @@ class AlgorithmDisablingTests(unittest.TestCase): 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, +): + """ + 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. + """ + if init is None: + init = {} + if server_init is None: + server_init = {} + if client_init is None: + client_init = {} + if connect is None: + 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 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() + + +_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 + # hostkey during kex being what's actually used. Truly proving that eg + # SHA512 was used, is quite difficult w/o super gross hacks. However, there + # are new tests in test_pkey.py which use known signature blobs to prove + # the SHA2 family was in fact used! + + def test_base_case_ssh_rsa_still_used_as_fallback(self): + # Prove that ssh-rsa is used if either, or both, participants have SHA2 + # algorithms disabled + for which in ("init", "client_init", "server_init"): + with server(**{which: _disable_sha2}) as (tc, _): + assert tc.host_key_type == "ssh-rsa" + + def test_kex_with_sha2_512(self): + # It's the default! + with server() as (tc, _): + assert tc.host_key_type == "rsa-sha2-512" + + def test_kex_with_sha2_256(self): + # No 512 -> you get 256 + with server( + init=dict(disabled_algorithms=dict(keys=["rsa-sha2-512"])) + ) as (tc, _): + assert tc.host_key_type == "rsa-sha2-256" + + def _incompatible_peers(self, client_init, server_init): + with server( + client_init=client_init, server_init=server_init, catch_error=True + ) as (tc, ts, err): + # If neither side blew up then that's bad! + assert err is not None + # If client side blew up first, it'll be straightforward + if isinstance(err, IncompatiblePeer): + pass + # If server side blew up first, client sees EOF & we need to check + # the server transport for its saved error (otherwise it can only + # appear in log output) + elif isinstance(err, EOFError): + assert ts.saved_exception is not None + assert isinstance(ts.saved_exception, IncompatiblePeer) + # If it was something else, welp + else: + raise err + + def test_client_sha2_disabled_server_sha1_disabled_no_match(self): + self._incompatible_peers( + client_init=_disable_sha2, server_init=_disable_sha1 + ) + + def test_client_sha1_disabled_server_sha2_disabled_no_match(self): + self._incompatible_peers( + client_init=_disable_sha1, server_init=_disable_sha2 + ) + + def test_explicit_client_hostkey_not_limited(self): + # Be very explicit about the hostkey on BOTH ends, + # and ensure it still ends up choosing sha2-512. + # (This is a regression test vs previous implementation which overwrote + # 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, _): + assert tc.host_key_type == "rsa-sha2-512" + + +class TestExtInfo(unittest.TestCase): + def test_ext_info_handshake(self): + with server() as (tc, _): + kex = tc._get_latest_kex_init() + assert kex["kex_algo_list"][-1] == "ext-info-c" + 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 + } + + def test_client_uses_server_sig_algs_for_pubkey_auth(self): + privkey = RSAKey.from_private_key_file(_support("test_rsa.key")) + with server( + pubkeys=[privkey], + connect=dict(pkey=privkey), + server_init=dict( + disabled_algorithms=dict(pubkeys=["rsa-sha2-512"]) + ), + ) as (tc, _): + assert tc.is_authenticated() + # Client settled on 256 despite itself not having 512 disabled + assert tc._agreed_pubkey_algorithm == "rsa-sha2-256" + + +# TODO: these could move into test_auth.py but that badly needs refactoring +# with this module anyways... +class TestSHA2SignaturePubkeys(unittest.TestCase): + def test_pubkey_auth_honors_disabled_algorithms(self): + privkey = RSAKey.from_private_key_file(_support("test_rsa.key")) + with server( + pubkeys=[privkey], + connect=dict(pkey=privkey), + init=dict( + disabled_algorithms=dict( + pubkeys=["ssh-rsa", "rsa-sha2-256", "rsa-sha2-512"] + ) + ), + catch_error=True, + ) as (_, _, err): + assert isinstance(err, SSHException) + assert "no RSA pubkey algorithms" in str(err) + + def test_client_sha2_disabled_server_sha1_disabled_no_match(self): + privkey = RSAKey.from_private_key_file(_support("test_rsa.key")) + with server( + pubkeys=[privkey], + connect=dict(pkey=privkey), + client_init=_disable_sha2_pubkey, + server_init=_disable_sha1_pubkey, + catch_error=True, + ) as (tc, ts, err): + assert isinstance(err, AuthenticationException) + + def test_client_sha1_disabled_server_sha2_disabled_no_match(self): + privkey = RSAKey.from_private_key_file(_support("test_rsa.key")) + with server( + pubkeys=[privkey], + connect=dict(pkey=privkey), + client_init=_disable_sha1_pubkey, + server_init=_disable_sha2_pubkey, + catch_error=True, + ) as (tc, ts, err): + assert isinstance(err, AuthenticationException) + + def test_ssh_rsa_still_used_when_sha2_disabled(self): + privkey = RSAKey.from_private_key_file(_support("test_rsa.key")) + # NOTE: this works because key obj comparison uses public bytes + # TODO: would be nice for PKey to grow a legit "give me another obj of + # same class but just the public bits" using asbytes() + with server( + pubkeys=[privkey], connect=dict(pkey=privkey), init=_disable_sha2 + ) as (tc, _): + assert tc.is_authenticated() + + def test_sha2_512(self): + privkey = RSAKey.from_private_key_file(_support("test_rsa.key")) + with server( + pubkeys=[privkey], + connect=dict(pkey=privkey), + init=dict( + disabled_algorithms=dict(pubkeys=["ssh-rsa", "rsa-sha2-256"]) + ), + ) as (tc, ts): + assert tc.is_authenticated() + assert tc._agreed_pubkey_algorithm == "rsa-sha2-512" + + def test_sha2_256(self): + privkey = RSAKey.from_private_key_file(_support("test_rsa.key")) + with server( + pubkeys=[privkey], + connect=dict(pkey=privkey), + init=dict( + disabled_algorithms=dict(pubkeys=["ssh-rsa", "rsa-sha2-512"]) + ), + ) as (tc, ts): + assert tc.is_authenticated() + assert tc._agreed_pubkey_algorithm == "rsa-sha2-256" -- cgit v1.2.3 From 2b66625659e66858cb5f557325c5fdd9c35fd073 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 23 Dec 2021 15:13:54 -0500 Subject: Add agent RSA-SHA2 support, also tweak changelog w/ more tickets --- paramiko/agent.py | 14 +++++++++++++- sites/www/changelog.rst | 12 ++++++++---- tests/test_agent.py | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 tests/test_agent.py diff --git a/paramiko/agent.py b/paramiko/agent.py index 3a02c06c..f28bf128 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -42,6 +42,18 @@ SSH2_AGENT_IDENTITIES_ANSWER = 12 cSSH2_AGENTC_SIGN_REQUEST = byte_chr(13) SSH2_AGENT_SIGN_RESPONSE = 14 +SSH_AGENT_RSA_SHA2_256 = 2 +SSH_AGENT_RSA_SHA2_512 = 4 +# NOTE: RFC mildly confusing; while these flags are OR'd together, OpenSSH at +# least really treats them like "AND"s, in the sense that if it finds the +# SHA256 flag set it won't continue looking at the SHA512 one; it +# short-circuits right away. +# Thus, we never want to eg submit 6 to say "either's good". +ALGORITHM_FLAG_MAP = { + "rsa-sha2-256": SSH_AGENT_RSA_SHA2_256, + "rsa-sha2-512": SSH_AGENT_RSA_SHA2_512, +} + class AgentSSH(object): def __init__(self): @@ -416,7 +428,7 @@ class AgentKey(PKey): msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST) msg.add_string(self.blob) msg.add_string(data) - msg.add_int(0) + msg.add_int(ALGORITHM_FLAG_MAP.get(algorithm, 0)) ptype, result = self.agent._send_message(msg) if ptype != SSH2_AGENT_SIGN_RESPONSE: raise SSHException("key cannot be used for signing") diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 016a5ac9..a519d333 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,10 +2,11 @@ Changelog ========= -- :feature:`1643` Add support for SHA-2 variants of RSA key verification - algorithms (as described in :rfc:`8332`) as well as limited SSH extension - negotiation (:rfc:`8308`). How SSH servers/clients decide when and how to use - this functionality can be complicated; Paramiko's support is as follows: +- :feature:`1643` (also :issue:`1925`, :issue:`1644`, :issue:`1326`) Add + support for SHA-2 variants of RSA key verification algorithms (as described + in :rfc:`8332`) as well as limited SSH extension negotiation (:rfc:`8308`). + How SSH servers/clients decide when and how to use this functionality can be + complicated; Paramiko's support is as follows: - Client verification of server host key during key exchange will now prefer ``rsa-sha2-512``, ``rsa-sha2-256``, and legacy ``ssh-rsa`` algorithms, in @@ -35,6 +36,9 @@ Changelog supported by both ends is used, or if there is none, it falls back to the previous behavior. + - SSH agent support grew the ability to specify algorithm flags when + requesting private key signatures; this is now used to forward SHA2 + algorithms when appropriate. - Server mode is now capable of pubkey auth involving SHA-2 signatures from clients, provided one's server implementation actually provides for doing so. diff --git a/tests/test_agent.py b/tests/test_agent.py new file mode 100644 index 00000000..c3973bbb --- /dev/null +++ b/tests/test_agent.py @@ -0,0 +1,50 @@ +import unittest + +from paramiko.message import Message +from paramiko.agent import ( + SSH2_AGENT_SIGN_RESPONSE, + cSSH2_AGENTC_SIGN_REQUEST, + SSH_AGENT_RSA_SHA2_256, + SSH_AGENT_RSA_SHA2_512, + AgentKey, +) +from paramiko.py3compat import b + + +class ChaosAgent: + def _send_message(self, msg): + self._sent_message = msg + sig = Message() + sig.add_string(b("lol")) + sig.rewind() + return SSH2_AGENT_SIGN_RESPONSE, sig + + +class AgentTests(unittest.TestCase): + def _sign_with_agent(self, kwargs, expectation): + agent = ChaosAgent() + key = AgentKey(agent, b("secret!!!")) + result = key.sign_ssh_data(b("token"), **kwargs) + assert result == b("lol") + msg = agent._sent_message + msg.rewind() + assert msg.get_byte() == cSSH2_AGENTC_SIGN_REQUEST + assert msg.get_string() == b("secret!!!") + assert msg.get_string() == b("token") + assert msg.get_int() == expectation + + def test_agent_signing_defaults_to_0_for_flags_field(self): + # No algorithm kwarg at all + self._sign_with_agent(kwargs=dict(), expectation=0) + + def test_agent_signing_is_2_for_SHA256(self): + self._sign_with_agent( + kwargs=dict(algorithm="rsa-sha2-256"), + expectation=SSH_AGENT_RSA_SHA2_256, + ) + + def test_agent_signing_is_2_for_SHA512(self): + self._sign_with_agent( + kwargs=dict(algorithm="rsa-sha2-512"), + expectation=SSH_AGENT_RSA_SHA2_512, + ) -- cgit v1.2.3 From a88ea9b5e58ee0301be9cc48d3d7239a3e281c64 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 23 Dec 2021 16:24:40 -0500 Subject: Changelog format tweak --- sites/www/changelog.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index a519d333..c10a35d8 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -57,9 +57,9 @@ Changelog where key exchange aborts due to algorithmic incompatibility. - Like all other exceptions in that module, it inherits from - ``SSHException``, and as we did not change anything else about the raising - (i.e. the attributes and message text are the same) this change is - backwards compatible. + ``SSHException``, and as we did not change anything else about the + raising (i.e. the attributes and message text are the same) this change + is backwards compatible. - `~paramiko.transport.Transport` grew a ``_preferred_pubkeys`` attribute and matching ``preferred_pubkeys`` property to match the other, kex-focused, -- cgit v1.2.3 From c42311a4b1c905c7a3ee129258490448e6e22203 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 23 Dec 2021 16:26:28 -0500 Subject: Cut 2.9.0 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index 0f0c6561..4e24c2f7 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (2, 8, 1) +__version_info__ = (2, 9, 0) __version__ = ".".join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index c10a35d8..eeae86ef 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +- :release:`2.9.0 <2021-12-23>` - :feature:`1643` (also :issue:`1925`, :issue:`1644`, :issue:`1326`) Add support for SHA-2 variants of RSA key verification algorithms (as described in :rfc:`8332`) as well as limited SSH extension negotiation (:rfc:`8308`). -- cgit v1.2.3 From 69fb31fcc14fef16b612d18b78016e74732b2de3 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 24 Dec 2021 12:58:25 -0500 Subject: Changelog and test re #1955 --- sites/www/changelog.rst | 4 ++++ tests/test_transport.py | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index eeae86ef..091fe118 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +- :bug:`1955` Server-side support for ``rsa-sha2-256`` and ``ssh-rsa`` wasn't + fully operable after 2.9.0's release (signatures for RSA pubkeys were always + run through ``rsa-sha2-512`` instead). Report and early stab at a fix + courtesy of Jun Omae. - :release:`2.9.0 <2021-12-23>` - :feature:`1643` (also :issue:`1925`, :issue:`1644`, :issue:`1326`) Add support for SHA-2 variants of RSA key verification algorithms (as described diff --git a/tests/test_transport.py b/tests/test_transport.py index 6145e5cb..77ffd6c1 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -1438,3 +1438,16 @@ class TestSHA2SignaturePubkeys(unittest.TestCase): ) as (tc, ts): assert tc.is_authenticated() assert tc._agreed_pubkey_algorithm == "rsa-sha2-256" + + def test_sha2_256_when_client_only_enables_256(self): + privkey = RSAKey.from_private_key_file(_support("test_rsa.key")) + with server( + pubkeys=[privkey], + connect=dict(pkey=privkey), + # Client-side only; server still accepts all 3. + client_init=dict( + disabled_algorithms=dict(pubkeys=["ssh-rsa", "rsa-sha2-512"]) + ), + ) as (tc, ts): + assert tc.is_authenticated() + assert tc._agreed_pubkey_algorithm == "rsa-sha2-256" -- cgit v1.2.3 From d2865ed7c3364406b2f1f4f984dcdd315196a353 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 24 Dec 2021 13:30:06 -0500 Subject: Fix #1955 --- paramiko/auth_handler.py | 53 +++++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 845b9143..da109d7c 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -206,24 +206,19 @@ class AuthHandler(object): self.transport._send_message(m) self.transport.close() - def _get_algorithm_and_bits(self, key): + def _get_key_type_and_bits(self, key): """ - Given any key, return appropriate signing algorithm & bits-to-sign. + Given any key, return its type/algorithm & bits-to-sign. Intended for input to or verification of, key signatures. """ - key_type, bits = None, None # Use certificate contents, if available, plain pubkey otherwise if key.public_blob: - key_type = key.public_blob.key_type - bits = key.public_blob.key_blob + return key.public_blob.key_type, key.public_blob.key_blob else: - key_type = key.get_name() - bits = key - algorithm = self._finalize_pubkey_algorithm(key_type) - return algorithm, bits + return key.get_name(), key - def _get_session_blob(self, key, service, username): + def _get_session_blob(self, key, service, username, algorithm): m = Message() m.add_string(self.transport.session_id) m.add_byte(cMSG_USERAUTH_REQUEST) @@ -231,7 +226,7 @@ class AuthHandler(object): m.add_string(service) m.add_string("publickey") m.add_boolean(True) - algorithm, bits = self._get_algorithm_and_bits(key) + _, bits = self._get_key_type_and_bits(key) m.add_string(algorithm) m.add_string(bits) return m.asbytes() @@ -282,6 +277,17 @@ class AuthHandler(object): # dunno this one self._disconnect_service_not_available() + def _generate_key_from_request(self, algorithm, keyblob): + # For use in server mode. + options = self.transport.preferred_pubkeys + if algorithm.replace("-cert-v01@openssh.com", "") not in options: + err = ( + "Auth rejected: pubkey algorithm '{}' unsupported or disabled" + ) + self._log(INFO, err.format(algorithm)) + return None + return self.transport._key_info[algorithm](Message(keyblob)) + def _finalize_pubkey_algorithm(self, key_type): # Short-circuit for non-RSA keys if "rsa" not in key_type: @@ -333,13 +339,15 @@ class AuthHandler(object): m.add_string(password) elif self.auth_method == "publickey": m.add_boolean(True) - algorithm, bits = self._get_algorithm_and_bits( - self.private_key - ) + key_type, bits = self._get_key_type_and_bits(self.private_key) + algorithm = self._finalize_pubkey_algorithm(key_type) m.add_string(algorithm) m.add_string(bits) blob = self._get_session_blob( - self.private_key, "ssh-connection", self.username + self.private_key, + "ssh-connection", + self.username, + algorithm, ) sig = self.private_key.sign_ssh_data(blob, algorithm) m.add_string(sig) @@ -551,10 +559,13 @@ Error Message: {} ) elif method == "publickey": sig_attached = m.get_boolean() - keytype = m.get_text() + # NOTE: server never wants to guess a client's algo, they're + # telling us directly. No need for _finalize_pubkey_algorithm + # anywhere in this flow. + algorithm = m.get_text() keyblob = m.get_binary() try: - key = self.transport._key_info[keytype](Message(keyblob)) + key = self._generate_key_from_request(algorithm, keyblob) except SSHException as e: self._log(INFO, "Auth rejected: public key: {}".format(str(e))) key = None @@ -572,20 +583,20 @@ Error Message: {} username, key ) if result != AUTH_FAILED: - sig_algo = self._finalize_pubkey_algorithm(keytype) # key is okay, verify it if not sig_attached: # client wants to know if this key is acceptable, before it # signs anything... send special "ok" message m = Message() m.add_byte(cMSG_USERAUTH_PK_OK) - # TODO: suspect we're not testing this - m.add_string(sig_algo) + m.add_string(algorithm) m.add_string(keyblob) self.transport._send_message(m) return sig = Message(m.get_binary()) - blob = self._get_session_blob(key, service, username) + blob = self._get_session_blob( + key, service, username, algorithm + ) if not key.verify_ssh_sig(blob, sig): self._log(INFO, "Auth rejected: invalid signature") result = AUTH_FAILED -- cgit v1.2.3 From bbefff00961125a35a5fb6a769679aa297224b45 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 24 Dec 2021 14:53:27 -0500 Subject: Cut 2.9.1 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index 4e24c2f7..a8a1c31e 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (2, 9, 0) +__version_info__ = (2, 9, 1) __version__ = ".".join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 091fe118..5027ed42 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +- :release:`2.9.1 <2021-12-24>` - :bug:`1955` Server-side support for ``rsa-sha2-256`` and ``ssh-rsa`` wasn't fully operable after 2.9.0's release (signatures for RSA pubkeys were always run through ``rsa-sha2-512`` instead). Report and early stab at a fix -- cgit v1.2.3 From 5f222495b5a62f3a1c465292bcace15888f40515 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sat, 8 Jan 2022 12:16:48 -0500 Subject: Add more visible backwards compat warning re 2.9 RSA2 changes --- sites/www/changelog.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 5027ed42..ef7ed367 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -11,6 +11,17 @@ Changelog - :feature:`1643` (also :issue:`1925`, :issue:`1644`, :issue:`1326`) Add support for SHA-2 variants of RSA key verification algorithms (as described in :rfc:`8332`) as well as limited SSH extension negotiation (:rfc:`8308`). + + .. warning:: + This change is slightly backwards incompatible, insofar as action is + required if your target systems do not support either RSA2 or the + ``server-sig-algs`` protocol extension. + + Specifically, you need to specify ``disabled_algorithms={'keys': + ['rsa-sha2-256', 'rsa-sha2-512']}`` in either `SSHClient + ` or `Transport + `. See below for details on why. + How SSH servers/clients decide when and how to use this functionality can be complicated; Paramiko's support is as follows: -- cgit v1.2.3 From 6699d35ad2d13fb74280a193e2e284a4a45f6f68 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sat, 8 Jan 2022 13:43:50 -0500 Subject: Fix up logging and exception handling re: pubkey auth and presence/lack of server-sig-algs Re #1961 --- paramiko/auth_handler.py | 47 +++++++++++++++++++++++++++++++++++------------ sites/www/changelog.rst | 7 +++++++ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index da109d7c..41ec4487 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -307,19 +307,42 @@ class AuthHandler(object): "An RSA key was specified, but no RSA pubkey algorithms are configured!" # noqa ) # Check for server-sig-algs if supported & sent - server_algos = u( + server_algo_str = u( self.transport.server_extensions.get("server-sig-algs", b("")) - ).split(",") - self._log(DEBUG, "Server-side algorithm list: {}".format(server_algos)) - # Only use algos from our list that the server likes, in our own - # preference order. (NOTE: purposefully using same style as in - # Transport...expect to refactor later) - agreement = list(filter(server_algos.__contains__, my_algos)) - # Fallback: first one in our (possibly tweaked by caller) list - final = agreement[0] if agreement else my_algos[0] - self.transport._agreed_pubkey_algorithm = final - self._log(DEBUG, "Agreed upon {!r} pubkey algorithm".format(final)) - return final + ) + pubkey_algo = None + if server_algo_str: + server_algos = server_algo_str.split(",") + self._log( + DEBUG, "Server-side algorithm list: {}".format(server_algos) + ) + # Only use algos from our list that the server likes, in our own + # preference order. (NOTE: purposefully using same style as in + # Transport...expect to refactor later) + agreement = list(filter(server_algos.__contains__, my_algos)) + if agreement: + pubkey_algo = agreement[0] + self._log( + DEBUG, + "Agreed upon {!r} pubkey algorithm".format(pubkey_algo), + ) + else: + self._log(DEBUG, "No common pubkey algorithms exist! Dying.") + # TODO: MAY want to use IncompatiblePeer again here but that's + # technically for initial key exchange, not pubkey auth. + err = "Unable to agree on a pubkey algorithm for signing a {!r} key!" # noqa + raise AuthenticationException(err.format(key_type)) + else: + # Fallback: first one in our (possibly tweaked by caller) list + pubkey_algo = my_algos[0] + msg = "Server did not send a server-sig-algs list; defaulting to our first preferred algo ({!r})" # noqa + self._log(DEBUG, msg.format(pubkey_algo)) + self._log( + DEBUG, + "NOTE: you may use the 'disabled_algorithms' SSHClient/Transport init kwarg to disable that or other algorithms if your server does not support them!", # noqa + ) + self.transport._agreed_pubkey_algorithm = pubkey_algo + return pubkey_algo def _parse_service_accept(self, m): service = m.get_text() diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index ef7ed367..61a92acb 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,13 @@ Changelog ========= +- :bug:`-` Connecting to servers which support ``server-sig-algs`` but which + have no overlap between that list and what a Paramiko client supports, now + raise an exception instead of defaulting to ``rsa-sha2-512`` (since the use + of ``server-sig-algs`` allows us to know what the server supports). +- :bug:`-` Enhanced log output when connecting to servers that do not support + ``server-sig-algs`` extensions, making the new-as-of-2.9 defaulting to SHA2 + pubkey algorithms more obvious when it kicks in. - :release:`2.9.1 <2021-12-24>` - :bug:`1955` Server-side support for ``rsa-sha2-256`` and ``ssh-rsa`` wasn't fully operable after 2.9.0's release (signatures for RSA pubkeys were always -- cgit v1.2.3 From 88f35a537428e430f7f26eee8026715e357b55d6 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sat, 8 Jan 2022 14:29:57 -0500 Subject: Cut 2.9.2 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index a8a1c31e..3ec897c5 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (2, 9, 1) +__version_info__ = (2, 9, 2) __version__ = ".".join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 61a92acb..460089fe 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +- :release:`2.9.2 <2022-01-08>` - :bug:`-` Connecting to servers which support ``server-sig-algs`` but which have no overlap between that list and what a Paramiko client supports, now raise an exception instead of defaulting to ``rsa-sha2-512`` (since the use -- cgit v1.2.3 From c9a3409b6d4078b77167467feafe5f102ec53671 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 11 Feb 2022 13:04:33 -0500 Subject: Clarify disabled algorithms keys vs pubkeys in changelog --- sites/www/changelog.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 460089fe..0b1bd9fe 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -51,9 +51,9 @@ Changelog cert, will act as follows: - In all cases, the list of algorithms to consider is based on the new - ``preferred_pubkeys`` list (see below) and ``disabled_algorithms``; this - list, like with host keys, prefers SHA2-512, SHA2-256 and SHA1, in that - order. + ``preferred_pubkeys`` list (see below) and ``disabled_algorithms`` + (specifically, its ``pubkeys`` key); this list, like with host keys, + prefers SHA2-512, SHA2-256 and SHA1, in that order. - When the server does not send ``server-sig-algs``, Paramiko will attempt the first algorithm in the above list. Clients connecting to legacy servers should thus use ``disabled_algorithms`` to turn off SHA2. -- cgit v1.2.3 From 11232cca7be7bc5fc51eef1e8869db4450bc79e3 Mon Sep 17 00:00:00 2001 From: Sondre Lillebø Gundersen Date: Thu, 10 Feb 2022 10:58:41 +0100 Subject: Add six to `install_requires` --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 56997532..c62a1bb4 100644 --- a/setup.py +++ b/setup.py @@ -90,6 +90,6 @@ setup( # TODO 3.0: alternately, given how prevalent ed25519 is now, and how we use # invoke for (increasing) subproc stuff, consider making the default flavor # "full" and adding a "minimal" or similar that is just-crypto? - install_requires=["bcrypt>=3.1.3", "cryptography>=2.5", "pynacl>=1.0.1"], + install_requires=["bcrypt>=3.1.3", "cryptography>=2.5", "pynacl>=1.0.1", "six"], extras_require=extras_require, ) -- cgit v1.2.3 From e0d5b1c63e25509d8014a5abfa7edcb83da696ec Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 25 Feb 2022 14:27:16 -0500 Subject: Comment and changelog re #1985 --- setup.py | 1 + sites/www/changelog.rst | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/setup.py b/setup.py index c62a1bb4..5a0acbac 100644 --- a/setup.py +++ b/setup.py @@ -90,6 +90,7 @@ setup( # TODO 3.0: alternately, given how prevalent ed25519 is now, and how we use # invoke for (increasing) subproc stuff, consider making the default flavor # "full" and adding a "minimal" or similar that is just-crypto? + # TODO 3.0: remove six, obviously install_requires=["bcrypt>=3.1.3", "cryptography>=2.5", "pynacl>=1.0.1", "six"], extras_require=extras_require, ) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index c423f5a5..af1cef05 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,12 @@ Changelog ========= +- :support:`1985` Add ``six`` explicitly to install-requires; it snuck into + active use at some point but has only been indicated by transitive dependency + on ``bcrypt`` until they somewhat-recently dropped it. This will be + short-lived until we `drop Python 2 + support `_. Thanks to Sondre + Lillebø Gundersen for catch & patch. - :release:`2.8.1 <2021-11-28>` - :bug:`985` (via :issue:`992`) Fix listdir failure when server uses a locale. Now on Python 2.7 `SFTPAttributes ` will -- cgit v1.2.3 From a318255e18accb7448ef76338b36483b06f40b4e Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 25 Feb 2022 14:28:31 -0500 Subject: blacken --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5a0acbac..c9de0154 100644 --- a/setup.py +++ b/setup.py @@ -91,6 +91,11 @@ setup( # invoke for (increasing) subproc stuff, consider making the default flavor # "full" and adding a "minimal" or similar that is just-crypto? # TODO 3.0: remove six, obviously - install_requires=["bcrypt>=3.1.3", "cryptography>=2.5", "pynacl>=1.0.1", "six"], + install_requires=[ + "bcrypt>=3.1.3", + "cryptography>=2.5", + "pynacl>=1.0.1", + "six", + ], extras_require=extras_require, ) -- cgit v1.2.3 From b4bd81dce17d23b3e402b7fd492bb2ebd30b284c Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 7 Mar 2022 16:21:28 -0500 Subject: Massively speed up low-level SFTP read/write This doesn't impact most users who perform reads/writes using SFTPClient.get(fo)/put(fo) as those naturally perform chunking. However, users accessing the raw SFTPFile objects via SFTPClient.open() and then reading/writing large (more than a few MB) files, may experience severe slowdown due to inefficient slicing of the file being read/written. This change replaces the naive "slice a list of bytes" code with bytearray and memoryview, which are significantly more performant in these use cases, while remaining backwards compatible. Patch courtesy of Sevastian Tchernov. --- paramiko/file.py | 9 +++++---- sites/www/changelog.rst | 10 ++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/paramiko/file.py b/paramiko/file.py index 9e9f6eb8..9dd9e9e7 100644 --- a/paramiko/file.py +++ b/paramiko/file.py @@ -192,7 +192,7 @@ class BufferedFile(ClosingContextManager): raise IOError("File is not open for reading") if (size is None) or (size < 0): # go for broke - result = self._rbuffer + result = bytearray(self._rbuffer) self._rbuffer = bytes() self._pos += len(result) while True: @@ -202,10 +202,10 @@ class BufferedFile(ClosingContextManager): new_data = None if (new_data is None) or (len(new_data) == 0): break - result += new_data + result.extend(new_data) self._realpos += len(new_data) self._pos += len(new_data) - return result + return bytes(result) if size <= len(self._rbuffer): result = self._rbuffer[:size] self._rbuffer = self._rbuffer[size:] @@ -515,9 +515,10 @@ class BufferedFile(ClosingContextManager): # self.newlines = None - def _write_all(self, data): + def _write_all(self, raw_data): # the underlying stream may be something that does partial writes (like # a socket). + data = memoryview(raw_data) while len(data) > 0: count = self._write(data) data = data[count:] diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 48119f00..9387aabf 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,16 @@ Changelog ========= +- :bug:`892 major` Significantly speed up low-level read/write actions on + `~paramiko.sftp_file.SFTPFile` objects by using `bytearray`/`memoryview`. + This is unlikely to change anything for users of the higher level methods + like ``SFTPClient.get`` or ``SFTPClient.getfo``, but users of + `SFTPClient.open ` will likely see + orders of magnitude improvements for files larger than a few megabytes in + size. + + Thanks to ``@jkji`` for the original report and to Sevastian Tchernov for the + patch. - :support:`1985` Add ``six`` explicitly to install-requires; it snuck into active use at some point but has only been indicated by transitive dependency on ``bcrypt`` until they somewhat-recently dropped it. This will be -- cgit v1.2.3 From 58b56875e77be6c2b6b65bbec7300a16c6fd5573 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 11 Mar 2022 11:36:52 -0500 Subject: Clarify SFTPClient.open() docs re: actual behavior of BufferedFile Closes #2000 --- paramiko/sftp_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 6294fb48..d6c3a70d 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -344,13 +344,13 @@ class SFTPClient(BaseSFTP, ClosingContextManager): ``O_EXCL`` flag in posix. The file will be buffered in standard Python style by default, but - can be altered with the ``bufsize`` parameter. ``0`` turns off + can be altered with the ``bufsize`` parameter. ``<=0`` turns off buffering, ``1`` uses line buffering, and any number greater than 1 (``>1``) uses that specific buffer size. :param str filename: name of the file to open :param str mode: mode (Python-style) to open in - :param int bufsize: desired buffering (-1 = default buffer size) + :param int bufsize: desired buffering (default: ``-1``) :return: an `.SFTPFile` object representing the open file :raises: ``IOError`` -- if the file could not be opened. -- cgit v1.2.3 From 3bb7877c13c22ec184ec77e4c4396539ae9f90b4 Mon Sep 17 00:00:00 2001 From: Lew Gordon Date: Mon, 29 Mar 2021 13:20:32 -0400 Subject: support Windows OpenSSH agent besides Putty's pageant (addresses #1509) Since quite a while there exists a native openssh port for windows. If the Putty pageant is not present, try to use the native port's agent instead. --- paramiko/agent.py | 18 ++++++++++++++++-- paramiko/win_openssh.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 paramiko/win_openssh.py diff --git a/paramiko/agent.py b/paramiko/agent.py index f28bf128..fc3b66fb 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -246,7 +246,12 @@ class AgentClientProxy(object): if win_pageant.can_talk_to_agent(): conn = win_pageant.PageantConnection() else: - return + import paramiko.win_openssh as win_openssh + + if win_openssh.can_talk_to_agent(): + conn = win_openssh.OpenSSHAgentConnection() + else: + return else: # no agent support return @@ -366,6 +371,10 @@ class Agent(AgentSSH): :raises: `.SSHException` -- if an SSH agent is found, but speaks an incompatible protocol + + .. versionchanged:: 2.10 + Added support for native openssh agent on windows (extending previous + putty pageant support) """ def __init__(self): @@ -384,7 +393,12 @@ class Agent(AgentSSH): if win_pageant.can_talk_to_agent(): conn = win_pageant.PageantConnection() else: - return + import paramiko.win_openssh as win_openssh + + if win_openssh.can_talk_to_agent(): + conn = win_openssh.OpenSSHAgentConnection() + else: + return else: # no agent support return diff --git a/paramiko/win_openssh.py b/paramiko/win_openssh.py new file mode 100644 index 00000000..593cdbe4 --- /dev/null +++ b/paramiko/win_openssh.py @@ -0,0 +1,39 @@ +# Copyright (C) 2021 Lew Gordon +# Copyright (C) 2022 Patrick Spendrin +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +import os.path + +PIPE_NAME = r"\\.\pipe\openssh-ssh-agent" + + +def can_talk_to_agent(): + return os.path.exists(PIPE_NAME) + + +class OpenSSHAgentConnection: + def __init__(self): + self._pipe = open(PIPE_NAME, "rb+", buffering=0) + + def send(self, data): + return self._pipe.write(data) + + def recv(self, n): + return self._pipe.read(n) + + def close(self): + return self._pipe.close() -- cgit v1.2.3 From a8e9a0fd717d1fe77b0b69419d49c9147711d4a5 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 10 Mar 2022 16:50:19 -0500 Subject: Changelog re #1509, re #1837, closes #1868 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 9387aabf..a1289968 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +- :feature:`1509` (via :issue:`1868`, :issue:`1837`) Add support for OpenSSH's + Windows agent as a fallback when Putty/WinPageant isn't available or + functional. Reported by ``@benj56``, early patchset by ``@lewgordon``, and + later PR courtesy of Patrick Spendrin. - :bug:`892 major` Significantly speed up low-level read/write actions on `~paramiko.sftp_file.SFTPFile` objects by using `bytearray`/`memoryview`. This is unlikely to change anything for users of the higher level methods -- cgit v1.2.3 From 845b4fa9c14f7836e144a8838c10a5cc64a6c204 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 10 Mar 2022 16:58:36 -0500 Subject: Refactor SSH agent socket connection stuff Feels like this entire module wants more rigorous rewriting, but at least for now any future tweaks to agent bits won't hit this awful copypasta. Minor functionality update: now both methods of dealing with unix sockets will use retry_on_signal to try and skip over spurious EINTRs --- paramiko/agent.py | 80 ++++++++++++++++++++++--------------------------- paramiko/win_openssh.py | 1 + 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/paramiko/agent.py b/paramiko/agent.py index fc3b66fb..13dc7975 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -205,6 +205,36 @@ class AgentRemoteProxy(AgentProxyThread): return self.__chan, None +def get_agent_connection(): + """ + Returns some SSH agent object, or None if none were found/supported. + + .. versionadded:: 2.10 + """ + if ("SSH_AUTH_SOCK" in os.environ) and (sys.platform != "win32"): + conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + retry_on_signal( + lambda: conn.connect(os.environ["SSH_AUTH_SOCK"]) + ) + return conn + except: + # probably a dangling env var: the ssh agent is gone + return + elif sys.platform == "win32": + from . import win_pageant, win_openssh + + conn = None + if win_pageant.can_talk_to_agent(): + conn = win_pageant.PageantConnection() + elif win_openssh.can_talk_to_agent(): + conn = win_openssh.OpenSSHAgentConnection() + return conn + else: + # no agent support + return + + class AgentClientProxy(object): """ Class proxying request as a client: @@ -231,29 +261,8 @@ class AgentClientProxy(object): """ Method automatically called by ``AgentProxyThread.run``. """ - if ("SSH_AUTH_SOCK" in os.environ) and (sys.platform != "win32"): - conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - try: - retry_on_signal( - lambda: conn.connect(os.environ["SSH_AUTH_SOCK"]) - ) - except: - # probably a dangling env var: the ssh agent is gone - return - elif sys.platform == "win32": - import paramiko.win_pageant as win_pageant - - if win_pageant.can_talk_to_agent(): - conn = win_pageant.PageantConnection() - else: - import paramiko.win_openssh as win_openssh - - if win_openssh.can_talk_to_agent(): - conn = win_openssh.OpenSSHAgentConnection() - else: - return - else: - # no agent support + conn = get_agent_connection() + if not conn: return self._conn = conn @@ -380,29 +389,10 @@ class Agent(AgentSSH): def __init__(self): AgentSSH.__init__(self) - if ("SSH_AUTH_SOCK" in os.environ) and (sys.platform != "win32"): - conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - try: - conn.connect(os.environ["SSH_AUTH_SOCK"]) - except: - # probably a dangling env var: the ssh agent is gone - return - elif sys.platform == "win32": - from . import win_pageant - - if win_pageant.can_talk_to_agent(): - conn = win_pageant.PageantConnection() - else: - import paramiko.win_openssh as win_openssh - - if win_openssh.can_talk_to_agent(): - conn = win_openssh.OpenSSHAgentConnection() - else: - return - else: - # no agent support + conn = get_agent_connection() + if not conn: return - self._connect(conn) + self._connect() def close(self): """ diff --git a/paramiko/win_openssh.py b/paramiko/win_openssh.py index 593cdbe4..ece7c8fd 100644 --- a/paramiko/win_openssh.py +++ b/paramiko/win_openssh.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + import os.path PIPE_NAME = r"\\.\pipe\openssh-ssh-agent" -- cgit v1.2.3 From 4313c026a8cb926ca984cc2039be3b8b06c36859 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 11 Mar 2022 17:37:58 -0500 Subject: Tweak changelog language a bit --- sites/www/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index a1289968..6ffd9cf1 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -4,8 +4,8 @@ Changelog - :feature:`1509` (via :issue:`1868`, :issue:`1837`) Add support for OpenSSH's Windows agent as a fallback when Putty/WinPageant isn't available or - functional. Reported by ``@benj56``, early patchset by ``@lewgordon``, and - later PR courtesy of Patrick Spendrin. + functional. Reported by ``@benj56`` with patches/PRs from ``@lewgordon`` and + Patrick Spendrin. - :bug:`892 major` Significantly speed up low-level read/write actions on `~paramiko.sftp_file.SFTPFile` objects by using `bytearray`/`memoryview`. This is unlikely to change anything for users of the higher level methods -- cgit v1.2.3 From f96319f35b4e2965b14d91e57418325639380548 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 11 Mar 2022 18:33:42 -0500 Subject: blacken --- paramiko/agent.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/paramiko/agent.py b/paramiko/agent.py index 13dc7975..2ff7b449 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -214,9 +214,7 @@ def get_agent_connection(): if ("SSH_AUTH_SOCK" in os.environ) and (sys.platform != "win32"): conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: - retry_on_signal( - lambda: conn.connect(os.environ["SSH_AUTH_SOCK"]) - ) + retry_on_signal(lambda: conn.connect(os.environ["SSH_AUTH_SOCK"])) return conn except: # probably a dangling env var: the ssh agent is gone -- cgit v1.2.3 From b9fc4f7c2cbfd319512b89d6af6421bb7cc1d2f0 Mon Sep 17 00:00:00 2001 From: Jason Brand Date: Tue, 15 Feb 2022 23:15:23 +0000 Subject: %C support in config file --- paramiko/config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/paramiko/config.py b/paramiko/config.py index e6877d01..0e3a5eaa 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -27,6 +27,7 @@ import os import re import shlex import socket +import hashlib from functools import partial from .py3compat import StringIO @@ -435,7 +436,10 @@ class SSHConfig(object): # The actual tokens! replacements = { # TODO: %%??? - # TODO: %C? + "%C": hashlib.sha1((local_hostname + + target_hostname + + str(port) + + remoteuser).encode("utf-8")).hexdigest(), "%d": homedir, "%h": configured_hostname, # TODO: %i? -- cgit v1.2.3 From cd65798c2777e5ab5f15e96a672ccc238237ddcd Mon Sep 17 00:00:00 2001 From: Jason Brand Date: Tue, 15 Feb 2022 23:16:41 +0000 Subject: Add %C to doc --- sites/docs/api/config.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/sites/docs/api/config.rst b/sites/docs/api/config.rst index ea4939b2..d42de8ac 100644 --- a/sites/docs/api/config.rst +++ b/sites/docs/api/config.rst @@ -99,6 +99,7 @@ properties of the local system). Specifically, we are known to support the below, where applicable (e.g. as in OpenSSH, ``%L`` works in ``ControlPath`` but not elsewhere): +- ``%C`` - ``%d`` - ``%h`` - ``%l`` -- cgit v1.2.3 From 0f90e5bdd908bbace418045ddd35224283f16afe Mon Sep 17 00:00:00 2001 From: Jason Brand Date: Tue, 15 Feb 2022 23:24:59 +0000 Subject: Import only sha1 --- paramiko/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/paramiko/config.py b/paramiko/config.py index 0e3a5eaa..27fdca4f 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -27,7 +27,7 @@ import os import re import shlex import socket -import hashlib +from hashlib import sha1 from functools import partial from .py3compat import StringIO @@ -436,10 +436,10 @@ class SSHConfig(object): # The actual tokens! replacements = { # TODO: %%??? - "%C": hashlib.sha1((local_hostname + - target_hostname + - str(port) + - remoteuser).encode("utf-8")).hexdigest(), + "%C": sha1((local_hostname + + target_hostname + + str(port) + + remoteuser).encode("utf-8")).hexdigest(), "%d": homedir, "%h": configured_hostname, # TODO: %i? -- cgit v1.2.3 From 3f3451fd46353fa173f6c083b1c38438d04a68ea Mon Sep 17 00:00:00 2001 From: Jason Brand Date: Tue, 15 Feb 2022 23:34:22 +0000 Subject: Add to changelog --- sites/www/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 6ffd9cf1..5dc44418 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +- :feature:`1976` Support for ``%C`` token in configuration file. - :feature:`1509` (via :issue:`1868`, :issue:`1837`) Add support for OpenSSH's Windows agent as a fallback when Putty/WinPageant isn't available or functional. Reported by ``@benj56`` with patches/PRs from ``@lewgordon`` and -- cgit v1.2.3 From f6342fc5f00b48e679e7c2c3579b1b27d94ffc1f Mon Sep 17 00:00:00 2001 From: Jason Brand Date: Wed, 16 Feb 2022 00:22:37 +0000 Subject: Prettify, add %C as acceptable controlpath token, mock gethostname --- paramiko/config.py | 8 +++----- tests/test_config.py | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/paramiko/config.py b/paramiko/config.py index 27fdca4f..c2a58e4e 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -60,7 +60,7 @@ class SSHConfig(object): # TODO: do a full scan of ssh.c & friends to make sure we're fully # compatible across the board, e.g. OpenSSH 8.1 added %n to ProxyCommand. TOKENS_BY_CONFIG_KEY = { - "controlpath": ["%h", "%l", "%L", "%n", "%p", "%r", "%u"], + "controlpath": ["%C", "%h", "%l", "%L", "%n", "%p", "%r", "%u"], "hostname": ["%h"], "identityfile": ["~", "%d", "%h", "%l", "%u", "%r"], "proxycommand": ["~", "%h", "%p", "%r"], @@ -433,13 +433,11 @@ class SSHConfig(object): local_hostname = socket.gethostname().split(".")[0] local_fqdn = LazyFqdn(config, local_hostname) homedir = os.path.expanduser("~") + tohash = local_hostname + target_hostname + repr(port) + remoteuser # The actual tokens! replacements = { # TODO: %%??? - "%C": sha1((local_hostname + - target_hostname + - str(port) + - remoteuser).encode("utf-8")).hexdigest(), + "%C": sha1(tohash.encode()).hexdigest(), "%d": homedir, "%h": configured_hostname, # TODO: %i? diff --git a/tests/test_config.py b/tests/test_config.py index 5e9aa059..892b4c92 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -42,6 +42,7 @@ def socket(): # Patch out getfqdn to return some real string for when it gets called; # some code (eg tokenization) gets mad w/ MagicMocks mocket.getfqdn.return_value = "some.fake.fqdn" + mocket.gethostname.return_value = "local.fake.fqdn" yield mocket -- cgit v1.2.3 From 1bf3dce7255ff2055dcdbc4d29454fb0184dfaf7 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 11 Mar 2022 18:05:33 -0500 Subject: Changelog enhancement --- sites/www/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 5dc44418..389c70fb 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,7 +2,8 @@ Changelog ========= -- :feature:`1976` Support for ``%C`` token in configuration file. +- :feature:`1976` Add support for the ``%C`` token when parsing SSH config + files. Foundational PR submitted by ``@jbrand42``. - :feature:`1509` (via :issue:`1868`, :issue:`1837`) Add support for OpenSSH's Windows agent as a fallback when Putty/WinPageant isn't available or functional. Reported by ``@benj56`` with patches/PRs from ``@lewgordon`` and -- cgit v1.2.3 From 5fcb8da16d4b33fa52880c1c3e848654a698d34d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 11 Mar 2022 18:08:39 -0500 Subject: OpenSSH docs state %C should also work in IdentityFile and Match exec (at least, of what we presently ourselves support - it's also allowed in others) --- paramiko/config.py | 4 ++-- tests/configs/match-exec | 2 +- tests/test_config.py | 23 +++++++++++++++++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/paramiko/config.py b/paramiko/config.py index c2a58e4e..9c21e4e5 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -62,11 +62,11 @@ class SSHConfig(object): TOKENS_BY_CONFIG_KEY = { "controlpath": ["%C", "%h", "%l", "%L", "%n", "%p", "%r", "%u"], "hostname": ["%h"], - "identityfile": ["~", "%d", "%h", "%l", "%u", "%r"], + "identityfile": ["%C", "~", "%d", "%h", "%l", "%u", "%r"], "proxycommand": ["~", "%h", "%p", "%r"], # Doesn't seem worth making this 'special' for now, it will fit well # enough (no actual match-exec config key to be confused with). - "match-exec": ["%d", "%h", "%L", "%l", "%n", "%p", "%r", "%u"], + "match-exec": ["%C", "%d", "%h", "%L", "%l", "%n", "%p", "%r", "%u"], } def __init__(self): diff --git a/tests/configs/match-exec b/tests/configs/match-exec index 763346ea..62a147aa 100644 --- a/tests/configs/match-exec +++ b/tests/configs/match-exec @@ -12,5 +12,5 @@ Host target User intermediate HostName configured -Match exec "%d %h %L %l %n %p %r %u" +Match exec "%C %d %h %L %l %n %p %r %u" Port 1337 diff --git a/tests/test_config.py b/tests/test_config.py index 892b4c92..08096cff 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -218,6 +218,9 @@ Host explicit_user Host explicit_host HostName ohai ControlPath remoteuser %r host %h orighost %n + +Host hashbrowns + ControlPath %C """ ) result = config.lookup("explicit_user")["controlpath"] @@ -226,6 +229,9 @@ Host explicit_host result = config.lookup("explicit_host")["controlpath"] # Remote user falls back to local user; host and orighost may differ assert result == "remoteuser gandalf host ohai orighost explicit_host" + # Supports %C + result = config.lookup("hashbrowns")["controlpath"] + assert result == "fc995d9f41ca1bcec7bc1d7f1ca87b9ff568a6d4" def test_negation(self): config = SSHConfig.from_text( @@ -280,7 +286,6 @@ ProxyCommand foo=bar:%h-%p def test_identityfile(self): config = SSHConfig.from_text( """ - IdentityFile id_dsa0 Host * @@ -291,6 +296,9 @@ IdentityFile id_dsa2 Host dsa2* IdentityFile id_dsa22 + +Host hashbrowns +IdentityFile %C """ ) for host, values in { @@ -303,8 +311,15 @@ IdentityFile id_dsa22 "hostname": "dsa22", "identityfile": ["id_dsa0", "id_dsa1", "id_dsa22"], }, + "hashbrowns": { + "hostname": "hashbrowns", + "identityfile": [ + "id_dsa0", + "id_dsa1", + "d5c0115d09912e39ff27844ea9d6052fc6048f99", + ], + }, }.items(): - assert config.lookup(host) == values def test_config_addressfamily_and_lazy_fqdn(self): @@ -740,10 +755,10 @@ class TestMatchExec(object): @patch("paramiko.config.getpass") @patch("paramiko.config.invoke.run") def test_tokenizes_argument(self, run, getpass, socket): - socket.gethostname.return_value = "local.fqdn" getpass.getuser.return_value = "gandalf" - # Actual exec value is "%d %h %L %l %n %p %r %u" + # Actual exec value is "%C %d %h %L %l %n %p %r %u" parts = ( + "bf5ba06778434a9384ee4217e462f64888bd0cd2", expanduser("~"), "configured", "local", -- cgit v1.2.3 From 29d7bf43f4a8beabcfe14cdc969d6a370e57ecf8 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 11 Mar 2022 21:09:21 -0500 Subject: Clearly our agent stuff is not fully tested yet... --- paramiko/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/agent.py b/paramiko/agent.py index 2ff7b449..1dc99b18 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -390,7 +390,7 @@ class Agent(AgentSSH): conn = get_agent_connection() if not conn: return - self._connect() + self._connect(conn) def close(self): """ -- cgit v1.2.3 From 02ad67eaec68bacc18838158b902ccaade8f5dc8 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 11 Mar 2022 21:21:26 -0500 Subject: Helps to actually leverage your mocked system calls --- tests/test_config.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 08096cff..b46dc7b4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -207,7 +207,7 @@ Host test assert got == expected @patch("paramiko.config.getpass") - def test_controlpath_token_expansion(self, getpass): + def test_controlpath_token_expansion(self, getpass, socket): getpass.getuser.return_value = "gandalf" config = SSHConfig.from_text( """ @@ -231,7 +231,7 @@ Host hashbrowns assert result == "remoteuser gandalf host ohai orighost explicit_host" # Supports %C result = config.lookup("hashbrowns")["controlpath"] - assert result == "fc995d9f41ca1bcec7bc1d7f1ca87b9ff568a6d4" + assert result == "a438e7dbf5308b923aba9db8fe2ca63447ac8688" def test_negation(self): config = SSHConfig.from_text( @@ -283,7 +283,9 @@ ProxyCommand foo=bar:%h-%p assert config.lookup(host) == values - def test_identityfile(self): + @patch("paramiko.config.getpass") + def test_identityfile(self, getpass, socket): + getpass.getuser.return_value = "gandalf" config = SSHConfig.from_text( """ IdentityFile id_dsa0 @@ -316,7 +318,7 @@ IdentityFile %C "identityfile": [ "id_dsa0", "id_dsa1", - "d5c0115d09912e39ff27844ea9d6052fc6048f99", + "a438e7dbf5308b923aba9db8fe2ca63447ac8688", ], }, }.items(): -- cgit v1.2.3 From e50e19f7d26665fd60f143320cce2e9c03f27c80 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 11 Mar 2022 22:33:28 -0500 Subject: Fix up changelog entry with real links --- sites/www/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 389c70fb..40b039ce 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -11,7 +11,8 @@ Changelog - :bug:`892 major` Significantly speed up low-level read/write actions on `~paramiko.sftp_file.SFTPFile` objects by using `bytearray`/`memoryview`. This is unlikely to change anything for users of the higher level methods - like ``SFTPClient.get`` or ``SFTPClient.getfo``, but users of + like `SFTPClient.get ` or + `SFTPClient.getfo `, but users of `SFTPClient.open ` will likely see orders of magnitude improvements for files larger than a few megabytes in size. -- cgit v1.2.3 From aa3cc6fa3e9f1df72d4ffd2d5fc02ae734a6cba4 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 11 Mar 2022 22:34:03 -0500 Subject: Cut 2.10.0 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index 3ec897c5..f71dc7b2 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (2, 9, 2) +__version_info__ = (2, 10, 0) __version__ = ".".join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 40b039ce..af648ddc 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +- :release:`2.10.0 <2022-03-11>` - :feature:`1976` Add support for the ``%C`` token when parsing SSH config files. Foundational PR submitted by ``@jbrand42``. - :feature:`1509` (via :issue:`1868`, :issue:`1837`) Add support for OpenSSH's -- cgit v1.2.3 From 4c491e299c9b800358b16fa4886d8d94f45abe2e Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 25 Feb 2022 14:50:42 -0500 Subject: Fix CVE re: PKey.write_private_key chmod race CVE-2022-24302 (see changelog for link) --- paramiko/pkey.py | 12 +++++++++- sites/www/changelog.rst | 14 ++++++++++++ tests/test_pkey.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 7865a6ea..67945261 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -558,7 +558,17 @@ class PKey(object): :raises: ``IOError`` -- if there was an error writing the file. """ - with open(filename, "w") as f: + # Ensure that we create new key files directly with a user-only mode, + # instead of opening, writing, then chmodding, which leaves us open to + # CVE-2022-24302. + # NOTE: O_TRUNC is a noop on new files, and O_CREAT is a noop on + # existing files, so using all 3 in both cases is fine. Ditto the use + # of the 'mode' argument; it should be safe to give even for existing + # files (though it will not act like a chmod in that case). + kwargs = dict(flags=os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode=o600) + # NOTE: yea, you still gotta inform the FLO that it is in "write" mode + with os.fdopen(os.open(filename, **kwargs), mode="w") as f: + # TODO 3.0: remove the now redundant chmod os.chmod(filename, o600) self._write_private_key(f, key, format, password=password) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index af648ddc..37d149f2 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,20 @@ Changelog ========= +- :bug:`-` (`CVE-2022-24302 + `_) Creation + of new private key files using `~paramiko.pkey.PKey` subclasses was subject + to a race condition between file creation & mode modification, which could be + exploited by an attacker with knowledge of where the Paramiko-using code + would write out such files. + + This has been patched by using `os.open` and `os.fdopen` to ensure new files + are opened with the correct mode immediately. We've left the subsequent + explicit ``chmod`` in place to minimize any possible disruption, though it + may get removed in future backwards-incompatible updates. + + Thanks to Jan Schejbal for the report & feedback on the solution, and to + Jeremy Katz at Tidelift for coordinating the disclosure. - :release:`2.10.0 <2022-03-11>` - :feature:`1976` Add support for the ``%C`` token when parsing SSH config files. Foundational PR submitted by ``@jbrand42``. diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 0cc20133..d44a96ac 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -23,6 +23,7 @@ Some unit tests for public/private key objects. import unittest import os +import stat from binascii import hexlify from hashlib import md5 @@ -36,10 +37,11 @@ from paramiko import ( SSHException, ) from paramiko.py3compat import StringIO, byte_chr, b, bytes, PY2 +from paramiko.common import o600 from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateNumbers -from mock import patch +from mock import patch, Mock import pytest from .util import _support, is_low_entropy @@ -696,3 +698,57 @@ class KeyTest(unittest.TestCase): key1.load_certificate, _support("test_rsa.key-cert.pub"), ) + + @patch("paramiko.pkey.os") + def _test_keyfile_race(self, os_, exists): + # Re: CVE-2022-24302 + password = "television" + newpassword = "radio" + source = _support("test_ecdsa_384.key") + new = source + ".new" + # Mock setup + os_.path.exists.return_value = exists + # Attach os flag values to mock + for attr, value in vars(os).items(): + if attr.startswith("O_"): + setattr(os_, attr, value) + # Load fixture key + key = ECDSAKey(filename=source, password=password) + key._write_private_key = Mock() + # Write out in new location + key.write_private_key_file(new, password=newpassword) + # Expected open via os module + os_.open.assert_called_once_with(new, flags=os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode=o600) + os_.fdopen.assert_called_once_with(os_.open.return_value, mode="w") + # Old chmod still around for backwards compat + os_.chmod.assert_called_once_with(new, o600) + assert ( + key._write_private_key.call_args[0][0] + == os_.fdopen.return_value.__enter__.return_value + ) + + def test_new_keyfiles_avoid_file_descriptor_race_on_chmod(self): + self._test_keyfile_race(exists=False) + + def test_existing_keyfiles_still_work_ok(self): + self._test_keyfile_race(exists=True) + + def test_new_keyfiles_avoid_descriptor_race_integration(self): + # Integration-style version of above + password = "television" + newpassword = "radio" + source = _support("test_ecdsa_384.key") + new = source + ".new" + # Load fixture key + key = ECDSAKey(filename=source, password=password) + try: + # Write out in new location + key.write_private_key_file(new, password=newpassword) + # Test mode + assert stat.S_IMODE(os.stat(new).st_mode) == o600 + # Prove can open with new password + reloaded = ECDSAKey(filename=new, password=newpassword) + assert reloaded == key + finally: + if os.path.exists(new): + os.unlink(new) -- cgit v1.2.3 From 286bd9f0374922341d48923b0c3ef09aab57919f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 11 Mar 2022 23:19:39 -0500 Subject: Cut 2.10.1 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index f71dc7b2..e955fe73 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (2, 10, 0) +__version_info__ = (2, 10, 1) __version__ = ".".join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 37d149f2..45bacd22 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +- :release:`2.10.1 <2022-03-11>` - :bug:`-` (`CVE-2022-24302 `_) Creation of new private key files using `~paramiko.pkey.PKey` subclasses was subject -- cgit v1.2.3 From abbf52a8c390b38ab4b8d83fc23bbaab3a31abb4 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 11 Mar 2022 23:32:33 -0500 Subject: blacken --- tests/test_pkey.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_pkey.py b/tests/test_pkey.py index d44a96ac..97b406c2 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -718,7 +718,9 @@ class KeyTest(unittest.TestCase): # Write out in new location key.write_private_key_file(new, password=newpassword) # Expected open via os module - os_.open.assert_called_once_with(new, flags=os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode=o600) + os_.open.assert_called_once_with( + new, flags=os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode=o600 + ) os_.fdopen.assert_called_once_with(os_.open.return_value, mode="w") # Old chmod still around for backwards compat os_.chmod.assert_called_once_with(new, o600) -- cgit v1.2.3 From 76b781754bfefe21706762442c422bac523701e4 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 14 Mar 2022 19:21:01 -0400 Subject: Use args, not kwargs, to retain py2 compat for now --- paramiko/pkey.py | 5 +++-- sites/www/changelog.rst | 8 ++++++++ tests/test_pkey.py | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 67945261..797e7723 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -565,9 +565,10 @@ class PKey(object): # existing files, so using all 3 in both cases is fine. Ditto the use # of the 'mode' argument; it should be safe to give even for existing # files (though it will not act like a chmod in that case). - kwargs = dict(flags=os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode=o600) + # TODO 3.0: turn into kwargs again + args = [os.O_WRONLY | os.O_TRUNC | os.O_CREAT, o600] # NOTE: yea, you still gotta inform the FLO that it is in "write" mode - with os.fdopen(os.open(filename, **kwargs), mode="w") as f: + with os.fdopen(os.open(filename, *args), "w") as f: # TODO 3.0: remove the now redundant chmod os.chmod(filename, o600) self._write_private_key(f, key, format, password=password) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 45bacd22..d5215995 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,14 @@ Changelog ========= +- :bug:`2001` Fix Python 2 compatibility breakage introduced in 2.10.1. Spotted + by Christian Hammond. + + .. warning:: + This is almost certainly the last time we will fix Python 2 related + errors! Please see `the roadmap + `_. + - :release:`2.10.1 <2022-03-11>` - :bug:`-` (`CVE-2022-24302 `_) Creation diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 97b406c2..cff99aac 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -719,9 +719,9 @@ class KeyTest(unittest.TestCase): key.write_private_key_file(new, password=newpassword) # Expected open via os module os_.open.assert_called_once_with( - new, flags=os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode=o600 + new, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, o600 ) - os_.fdopen.assert_called_once_with(os_.open.return_value, mode="w") + os_.fdopen.assert_called_once_with(os_.open.return_value, "w") # Old chmod still around for backwards compat os_.chmod.assert_called_once_with(new, o600) assert ( -- cgit v1.2.3 From 57033fb57986a00263e57c615674bfec41efb59c Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 14 Mar 2022 19:24:10 -0400 Subject: Cut 2.10.2 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index e955fe73..b14b6865 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (2, 10, 1) +__version_info__ = (2, 10, 2) __version__ = ".".join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index d5215995..ff2be259 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +- :release:`2.10.2 <2022-03-14>` - :bug:`2001` Fix Python 2 compatibility breakage introduced in 2.10.1. Spotted by Christian Hammond. -- cgit v1.2.3 From 94f3894ab74744653604a52a31b28e1ba333e6d8 Mon Sep 17 00:00:00 2001 From: Paul Howarth Date: Tue, 15 Mar 2022 19:32:20 +0000 Subject: Fix Free Software Foundation address They moved from Temple Place to Franklin Street in 2005. --- demos/demo.py | 2 +- demos/demo_keygen.py | 2 +- demos/demo_server.py | 2 +- demos/demo_sftp.py | 2 +- demos/demo_simple.py | 2 +- demos/forward.py | 2 +- demos/interactive.py | 2 +- demos/rforward.py | 2 +- paramiko/__init__.py | 2 +- paramiko/agent.py | 2 +- paramiko/auth_handler.py | 2 +- paramiko/ber.py | 2 +- paramiko/buffered_pipe.py | 2 +- paramiko/channel.py | 2 +- paramiko/client.py | 2 +- paramiko/common.py | 2 +- paramiko/compress.py | 2 +- paramiko/config.py | 2 +- paramiko/dsskey.py | 2 +- paramiko/ecdsakey.py | 2 +- paramiko/ed25519key.py | 2 +- paramiko/file.py | 2 +- paramiko/hostkeys.py | 2 +- paramiko/kex_gex.py | 2 +- paramiko/kex_group1.py | 2 +- paramiko/kex_group14.py | 2 +- paramiko/kex_group16.py | 2 +- paramiko/kex_gss.py | 2 +- paramiko/message.py | 2 +- paramiko/packet.py | 2 +- paramiko/pipe.py | 2 +- paramiko/pkey.py | 2 +- paramiko/primes.py | 2 +- paramiko/proxy.py | 2 +- paramiko/rsakey.py | 2 +- paramiko/server.py | 2 +- paramiko/sftp.py | 2 +- paramiko/sftp_attr.py | 2 +- paramiko/sftp_client.py | 2 +- paramiko/sftp_file.py | 2 +- paramiko/sftp_handle.py | 2 +- paramiko/sftp_server.py | 2 +- paramiko/sftp_si.py | 2 +- paramiko/ssh_exception.py | 2 +- paramiko/ssh_gss.py | 2 +- paramiko/transport.py | 2 +- paramiko/util.py | 2 +- paramiko/win_openssh.py | 2 +- paramiko/win_pageant.py | 2 +- setup_helper.py | 2 +- tests/loop.py | 2 +- tests/stub_sftp.py | 2 +- tests/test_auth.py | 2 +- tests/test_buffered_pipe.py | 2 +- tests/test_client.py | 2 +- tests/test_file.py | 2 +- tests/test_gssapi.py | 2 +- tests/test_hostkeys.py | 2 +- tests/test_kex.py | 2 +- tests/test_kex_gss.py | 2 +- tests/test_message.py | 2 +- tests/test_packetizer.py | 2 +- tests/test_pkey.py | 2 +- tests/test_sftp.py | 2 +- tests/test_sftp_big.py | 2 +- tests/test_ssh_gss.py | 2 +- tests/test_transport.py | 2 +- tests/test_util.py | 2 +- 68 files changed, 68 insertions(+), 68 deletions(-) diff --git a/demos/demo.py b/demos/demo.py index c9b0a5f5..5252db7c 100755 --- a/demos/demo.py +++ b/demos/demo.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import base64 diff --git a/demos/demo_keygen.py b/demos/demo_keygen.py index 6a80272d..12637ed0 100755 --- a/demos/demo_keygen.py +++ b/demos/demo_keygen.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import sys diff --git a/demos/demo_server.py b/demos/demo_server.py index 313e5fb2..6cb2dc51 100644 --- a/demos/demo_server.py +++ b/demos/demo_server.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import base64 from binascii import hexlify diff --git a/demos/demo_sftp.py b/demos/demo_sftp.py index 7f6a002e..dbcb2cb7 100644 --- a/demos/demo_sftp.py +++ b/demos/demo_sftp.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # based on code provided by raymond mosteller (thanks!) diff --git a/demos/demo_simple.py b/demos/demo_simple.py index 5dd4f6c1..bd932c3e 100644 --- a/demos/demo_simple.py +++ b/demos/demo_simple.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import base64 diff --git a/demos/forward.py b/demos/forward.py index cd9dabf1..869e3906 100644 --- a/demos/forward.py +++ b/demos/forward.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Sample script showing how to do local port forwarding over paramiko. diff --git a/demos/interactive.py b/demos/interactive.py index 037787c4..16eae0e7 100644 --- a/demos/interactive.py +++ b/demos/interactive.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import socket diff --git a/demos/rforward.py b/demos/rforward.py index a2e8a776..200634ab 100755 --- a/demos/rforward.py +++ b/demos/rforward.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Sample script showing how to do remote port forwarding over paramiko. diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 5318cc9c..cbc240a6 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # flake8: noqa import sys diff --git a/paramiko/agent.py b/paramiko/agent.py index 1dc99b18..17eb4568 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ SSH Agent interface diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 41ec4487..4615918e 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ `.AuthHandler` diff --git a/paramiko/ber.py b/paramiko/ber.py index 92d7121e..a064e6b1 100644 --- a/paramiko/ber.py +++ b/paramiko/ber.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from paramiko.common import max_byte, zero_byte from paramiko.py3compat import b, byte_ord, byte_chr, long diff --git a/paramiko/buffered_pipe.py b/paramiko/buffered_pipe.py index 69445c97..c29ac91e 100644 --- a/paramiko/buffered_pipe.py +++ b/paramiko/buffered_pipe.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Attempt to generalize the "feeder" part of a `.Channel`: an object which can be diff --git a/paramiko/channel.py b/paramiko/channel.py index 72f65012..592ddcd2 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Abstraction for an SSH2 channel. diff --git a/paramiko/client.py b/paramiko/client.py index 80c956cd..581f9b6f 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ SSH client & key policies diff --git a/paramiko/common.py b/paramiko/common.py index 55dd4bdf..cf6972d5 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Common constants and global variables. diff --git a/paramiko/compress.py b/paramiko/compress.py index fa3b6aa3..7fe26db1 100644 --- a/paramiko/compress.py +++ b/paramiko/compress.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Compression implementations for a Transport. diff --git a/paramiko/config.py b/paramiko/config.py index 9c21e4e5..155e2c0d 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -15,7 +15,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Configuration file (aka ``ssh_config``) support. diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index 1a0c4797..5a0f85eb 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ DSS keys. diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index c4e2b1af..62bc8d9b 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ ECDSA keys diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py index d322a0c1..b29d82c5 100644 --- a/paramiko/ed25519key.py +++ b/paramiko/ed25519key.py @@ -12,7 +12,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import bcrypt diff --git a/paramiko/file.py b/paramiko/file.py index 9dd9e9e7..90f4a7b9 100644 --- a/paramiko/file.py +++ b/paramiko/file.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from paramiko.common import ( linefeed_byte_value, crlf, diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index 94474e40..f1b4a936 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import binascii diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index ab462e6d..e6ed2392 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Variant on `KexGroup1 ` where the prime "p" and diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 6d548b01..78894566 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of diff --git a/paramiko/kex_group14.py b/paramiko/kex_group14.py index a620c1a3..2d82d764 100644 --- a/paramiko/kex_group14.py +++ b/paramiko/kex_group14.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of diff --git a/paramiko/kex_group16.py b/paramiko/kex_group16.py index 15b0acfe..b53aad38 100644 --- a/paramiko/kex_group16.py +++ b/paramiko/kex_group16.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index f83a2dc4..08e5d787 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -17,7 +17,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ diff --git a/paramiko/message.py b/paramiko/message.py index 9771cfbc..6095d5de 100644 --- a/paramiko/message.py +++ b/paramiko/message.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Implementation of an SSH2 "message". diff --git a/paramiko/packet.py b/paramiko/packet.py index 12663168..af78e312 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Packet handling diff --git a/paramiko/pipe.py b/paramiko/pipe.py index dda885da..3905949d 100644 --- a/paramiko/pipe.py +++ b/paramiko/pipe.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Abstraction of a one-way pipe where the read end can be used in diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 797e7723..3c975630 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Common API for all public keys. diff --git a/paramiko/primes.py b/paramiko/primes.py index 8dff7683..564ab26f 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Utility functions for dealing with primes. diff --git a/paramiko/proxy.py b/paramiko/proxy.py index 077e8e35..3e3e61a6 100644 --- a/paramiko/proxy.py +++ b/paramiko/proxy.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index 26c5313c..fd74d9f3 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ RSA keys. diff --git a/paramiko/server.py b/paramiko/server.py index 2fe9cc19..80ebf06a 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ `.ServerInterface` is an interface to override for server support. diff --git a/paramiko/sftp.py b/paramiko/sftp.py index 25debc85..cfed9028 100644 --- a/paramiko/sftp.py +++ b/paramiko/sftp.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import select import socket diff --git a/paramiko/sftp_attr.py b/paramiko/sftp_attr.py index 8b1c17bd..28a196b1 100644 --- a/paramiko/sftp_attr.py +++ b/paramiko/sftp_attr.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import stat import time diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index d6c3a70d..ec5704de 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from binascii import hexlify diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py index 0104d857..50842b46 100644 --- a/paramiko/sftp_file.py +++ b/paramiko/sftp_file.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ SFTP file object diff --git a/paramiko/sftp_handle.py b/paramiko/sftp_handle.py index a7e22f01..1b4e1363 100644 --- a/paramiko/sftp_handle.py +++ b/paramiko/sftp_handle.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Abstraction of an SFTP file handle (for server mode). diff --git a/paramiko/sftp_server.py b/paramiko/sftp_server.py index 8265df96..f0db5765 100644 --- a/paramiko/sftp_server.py +++ b/paramiko/sftp_server.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Server-mode SFTP support. diff --git a/paramiko/sftp_si.py b/paramiko/sftp_si.py index 40dc561c..3199310a 100644 --- a/paramiko/sftp_si.py +++ b/paramiko/sftp_si.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ An interface to override for SFTP server support. diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 39fcb10d..620ab259 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import socket diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index 5d4cb416..4f1581c3 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ diff --git a/paramiko/transport.py b/paramiko/transport.py index b99b3278..36ff237d 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -15,7 +15,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Core protocol implementation diff --git a/paramiko/util.py b/paramiko/util.py index 93970289..a4e8f70b 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Useful functions used by the rest of paramiko. diff --git a/paramiko/win_openssh.py b/paramiko/win_openssh.py index ece7c8fd..5dd71cd4 100644 --- a/paramiko/win_openssh.py +++ b/paramiko/win_openssh.py @@ -15,7 +15,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os.path diff --git a/paramiko/win_pageant.py b/paramiko/win_pageant.py index a550b7f3..b733d813 100644 --- a/paramiko/win_pageant.py +++ b/paramiko/win_pageant.py @@ -15,7 +15,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Functions for communicating with Pageant, the basic windows ssh agent program. diff --git a/setup_helper.py b/setup_helper.py index d0a8700e..fc4e755f 100644 --- a/setup_helper.py +++ b/setup_helper.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # Note: Despite the copyright notice, this was submitted by John # Arbash Meinel. Thanks John! diff --git a/tests/loop.py b/tests/loop.py index dd1f5a0c..87fb089a 100644 --- a/tests/loop.py +++ b/tests/loop.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import socket import threading diff --git a/tests/stub_sftp.py b/tests/stub_sftp.py index 1528a0b8..0c0372e9 100644 --- a/tests/stub_sftp.py +++ b/tests/stub_sftp.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ A stub SFTP server for loopback SFTP testing. diff --git a/tests/test_auth.py b/tests/test_auth.py index 01fbac5b..0f0a6169 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Some unit tests for authenticating over a Transport. diff --git a/tests/test_buffered_pipe.py b/tests/test_buffered_pipe.py index 61c99cc0..35e2cded 100644 --- a/tests/test_buffered_pipe.py +++ b/tests/test_buffered_pipe.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Some unit tests for BufferedPipe. diff --git a/tests/test_client.py b/tests/test_client.py index f14aac23..10132aae 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Some unit tests for SSHClient. diff --git a/tests/test_file.py b/tests/test_file.py index 2a3da74b..d4062c02 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Some unit tests for the BufferedFile abstraction. diff --git a/tests/test_gssapi.py b/tests/test_gssapi.py index acdc7c82..23c3ef42 100644 --- a/tests/test_gssapi.py +++ b/tests/test_gssapi.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Test the used APIs for GSS-API / SSPI authentication diff --git a/tests/test_hostkeys.py b/tests/test_hostkeys.py index 41a9244f..723ea1a5 100644 --- a/tests/test_hostkeys.py +++ b/tests/test_hostkeys.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Some unit tests for HostKeys. diff --git a/tests/test_kex.py b/tests/test_kex.py index b73989c2..b6463558 100644 --- a/tests/test_kex.py +++ b/tests/test_kex.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Some unit tests for the key exchange protocols. diff --git a/tests/test_kex_gss.py b/tests/test_kex_gss.py index 6f5625dc..26659ae3 100644 --- a/tests/test_kex_gss.py +++ b/tests/test_kex_gss.py @@ -17,7 +17,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Unit Tests for the GSS-API / SSPI SSHv2 Diffie-Hellman Key Exchange and user diff --git a/tests/test_message.py b/tests/test_message.py index 57766d90..23b06858 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Some unit tests for ssh protocol message blocks. diff --git a/tests/test_packetizer.py b/tests/test_packetizer.py index de80770e..27dee358 100644 --- a/tests/test_packetizer.py +++ b/tests/test_packetizer.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Some unit tests for the ssh2 protocol in Transport. diff --git a/tests/test_pkey.py b/tests/test_pkey.py index cff99aac..8aff7eac 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -15,7 +15,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Some unit tests for public/private key objects. diff --git a/tests/test_sftp.py b/tests/test_sftp.py index 6134d070..0650e8db 100644 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ some unit tests to make sure sftp works. diff --git a/tests/test_sftp_big.py b/tests/test_sftp_big.py index fc556faf..4643bcaa 100644 --- a/tests/test_sftp_big.py +++ b/tests/test_sftp_big.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ some unit tests to make sure sftp works well with large files. diff --git a/tests/test_ssh_gss.py b/tests/test_ssh_gss.py index 92801c20..4d171854 100644 --- a/tests/test_ssh_gss.py +++ b/tests/test_ssh_gss.py @@ -17,7 +17,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Unit Tests for the GSS-API / SSPI SSHv2 Authentication (gssapi-with-mic) diff --git a/tests/test_transport.py b/tests/test_transport.py index 77ffd6c1..a9262f3d 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Some unit tests for the ssh2 protocol in Transport. diff --git a/tests/test_util.py b/tests/test_util.py index 8ce260d1..0e485759 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Some unit tests for utility functions. -- cgit v1.2.3 From 8506148cb01f6869e3184156c80bee26510961d5 Mon Sep 17 00:00:00 2001 From: Richard Kojedzinszky Date: Sun, 13 Mar 2022 08:47:30 +0100 Subject: util: store thread assigned id in thread-local storage Fixes #2002 --- paramiko/util.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/paramiko/util.py b/paramiko/util.py index 93970289..a344915c 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -225,24 +225,20 @@ def mod_inverse(x, m): return u2 -_g_thread_ids = {} +_g_thread_data = threading.local() _g_thread_counter = 0 _g_thread_lock = threading.Lock() def get_thread_id(): - global _g_thread_ids, _g_thread_counter, _g_thread_lock - tid = id(threading.currentThread()) + global _g_thread_data, _g_thread_counter, _g_thread_lock try: - return _g_thread_ids[tid] - except KeyError: - _g_thread_lock.acquire() - try: + return _g_thread_data.id + except AttributeError: + with _g_thread_lock: _g_thread_counter += 1 - ret = _g_thread_ids[tid] = _g_thread_counter - finally: - _g_thread_lock.release() - return ret + _g_thread_data.id = _g_thread_counter + return _g_thread_data.id def log_to_file(filename, level=DEBUG): -- cgit v1.2.3 From 9cdd965cd9edd645364e905be1644b492bf581a1 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 18 Mar 2022 16:40:38 -0400 Subject: Changelog re #2002, re #2003, closes #2002 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index ff2be259..20fa786f 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +- :bug:`2002` (via :issue:`2003`) Switch from module-global to thread-local + storage when recording thread IDs for a logging helper; this should avoid one + flavor of memory leak for long-running processes. Catch & patch via Richard + Kojedzinszky. - :release:`2.10.2 <2022-03-14>` - :bug:`2001` Fix Python 2 compatibility breakage introduced in 2.10.1. Spotted by Christian Hammond. -- cgit v1.2.3 From 3853951b304c34a97ddb39204c5b47d944f2498a Mon Sep 17 00:00:00 2001 From: Richard Kojedzinszky Date: Sun, 13 Mar 2022 08:47:30 +0100 Subject: util: store thread assigned id in thread-local storage Fixes #2002 --- paramiko/util.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/paramiko/util.py b/paramiko/util.py index a4e8f70b..4267caf1 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -225,24 +225,20 @@ def mod_inverse(x, m): return u2 -_g_thread_ids = {} +_g_thread_data = threading.local() _g_thread_counter = 0 _g_thread_lock = threading.Lock() def get_thread_id(): - global _g_thread_ids, _g_thread_counter, _g_thread_lock - tid = id(threading.currentThread()) + global _g_thread_data, _g_thread_counter, _g_thread_lock try: - return _g_thread_ids[tid] - except KeyError: - _g_thread_lock.acquire() - try: + return _g_thread_data.id + except AttributeError: + with _g_thread_lock: _g_thread_counter += 1 - ret = _g_thread_ids[tid] = _g_thread_counter - finally: - _g_thread_lock.release() - return ret + _g_thread_data.id = _g_thread_counter + return _g_thread_data.id def log_to_file(filename, level=DEBUG): -- cgit v1.2.3 From b924489765a4baccd977fe15096026175aca9894 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 18 Mar 2022 16:40:38 -0400 Subject: Changelog re #2002, re #2003, closes #2002 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index ff2be259..20fa786f 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +- :bug:`2002` (via :issue:`2003`) Switch from module-global to thread-local + storage when recording thread IDs for a logging helper; this should avoid one + flavor of memory leak for long-running processes. Catch & patch via Richard + Kojedzinszky. - :release:`2.10.2 <2022-03-14>` - :bug:`2001` Fix Python 2 compatibility breakage introduced in 2.10.1. Spotted by Christian Hammond. -- cgit v1.2.3 From 4ef50df54d3dad257afe2663f34dab3c06090b10 Mon Sep 17 00:00:00 2001 From: Jun Omae Date: Thu, 6 Jan 2022 16:49:44 +0900 Subject: Fix publickey authentication with signed RSA key --- paramiko/auth_handler.py | 2 ++ paramiko/rsakey.py | 2 +- tests/test_pkey.py | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 41ec4487..e9023673 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -341,6 +341,8 @@ class AuthHandler(object): DEBUG, "NOTE: you may use the 'disabled_algorithms' SSHClient/Transport init kwarg to disable that or other algorithms if your server does not support them!", # noqa ) + if key_type.endswith("-cert-v01@openssh.com"): + pubkey_algo += "-cert-v01@openssh.com" self.transport._agreed_pubkey_algorithm = pubkey_algo return pubkey_algo diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index 26c5313c..d2dc99e4 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -129,7 +129,7 @@ class RSAKey(PKey): algorithm=self.HASHES[algorithm](), ) m = Message() - m.add_string(algorithm) + m.add_string(algorithm.replace("-cert-v01@openssh.com", "")) m.add_string(sig) return m diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 0cc20133..e652740c 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -696,3 +696,22 @@ class KeyTest(unittest.TestCase): key1.load_certificate, _support("test_rsa.key-cert.pub"), ) + + def test_sign_rsa_with_certificate(self): + data = b"ice weasels" + key_path = _support(os.path.join("cert_support", "test_rsa.key")) + key = RSAKey.from_private_key_file(key_path) + msg = key.sign_ssh_data(data, "rsa-sha2-256") + msg.rewind() + assert "rsa-sha2-256" == msg.get_text() + sign = msg.get_binary() + cert_path = _support( + os.path.join("cert_support", "test_rsa.key-cert.pub") + ) + key.load_certificate(cert_path) + msg = key.sign_ssh_data(data, "rsa-sha2-256-cert-v01@openssh.com") + msg.rewind() + assert "rsa-sha2-256" == msg.get_text() + assert sign == msg.get_binary() + msg.rewind() + assert key.verify_ssh_sig(b"ice weasels", msg) -- cgit v1.2.3 From d25e5f31490da2aee8b75d8a3aca338abc490f73 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 18 Mar 2022 16:47:53 -0400 Subject: Changelog closes #1963, closes #1977 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 48119f00..0d1fd19e 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +- :bug:`1963` (via :issue:`1977`) Certificate-based pubkey auth was + inadvertently broken when adding SHA2 support; this has been fixed. Reported + by Erik Forsberg and fixed by Jun Omae. - :support:`1985` Add ``six`` explicitly to install-requires; it snuck into active use at some point but has only been indicated by transitive dependency on ``bcrypt`` until they somewhat-recently dropped it. This will be -- cgit v1.2.3 From a02c1657dc79e5e25ed0424faa5db66ecda8c4a2 Mon Sep 17 00:00:00 2001 From: Richard Kojedzinszky Date: Sun, 13 Mar 2022 08:47:30 +0100 Subject: util: store thread assigned id in thread-local storage Fixes #2002 --- paramiko/util.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/paramiko/util.py b/paramiko/util.py index 93970289..a344915c 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -225,24 +225,20 @@ def mod_inverse(x, m): return u2 -_g_thread_ids = {} +_g_thread_data = threading.local() _g_thread_counter = 0 _g_thread_lock = threading.Lock() def get_thread_id(): - global _g_thread_ids, _g_thread_counter, _g_thread_lock - tid = id(threading.currentThread()) + global _g_thread_data, _g_thread_counter, _g_thread_lock try: - return _g_thread_ids[tid] - except KeyError: - _g_thread_lock.acquire() - try: + return _g_thread_data.id + except AttributeError: + with _g_thread_lock: _g_thread_counter += 1 - ret = _g_thread_ids[tid] = _g_thread_counter - finally: - _g_thread_lock.release() - return ret + _g_thread_data.id = _g_thread_counter + return _g_thread_data.id def log_to_file(filename, level=DEBUG): -- cgit v1.2.3 From 6af11051451f7c470fbf5a30299c78ba160f13bc Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 18 Mar 2022 16:40:38 -0400 Subject: Changelog re #2002, re #2003, closes #2002 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 0d1fd19e..85fb1465 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -5,6 +5,10 @@ Changelog - :bug:`1963` (via :issue:`1977`) Certificate-based pubkey auth was inadvertently broken when adding SHA2 support; this has been fixed. Reported by Erik Forsberg and fixed by Jun Omae. +- :bug:`2002` (via :issue:`2003`) Switch from module-global to thread-local + storage when recording thread IDs for a logging helper; this should avoid one + flavor of memory leak for long-running processes. Catch & patch via Richard + Kojedzinszky. - :support:`1985` Add ``six`` explicitly to install-requires; it snuck into active use at some point but has only been indicated by transitive dependency on ``bcrypt`` until they somewhat-recently dropped it. This will be -- cgit v1.2.3 From 094a5e10982c7d7ef9f17fdf755d755fec9fe19b Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 18 Mar 2022 17:00:36 -0400 Subject: Cut 2.9.3 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index 3ec897c5..c5f8c829 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (2, 9, 2) +__version_info__ = (2, 9, 3) __version__ = ".".join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 85fb1465..358b8d8e 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +- :release:`2.9.3 <2022-03-18>` - :bug:`1963` (via :issue:`1977`) Certificate-based pubkey auth was inadvertently broken when adding SHA2 support; this has been fixed. Reported by Erik Forsberg and fixed by Jun Omae. -- cgit v1.2.3 From 239d2bd7a620be5cdaaa26f981ea72f5f55c9050 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 18 Mar 2022 17:02:18 -0400 Subject: Cut 2.10.3 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index b14b6865..82bc1161 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (2, 10, 2) +__version_info__ = (2, 10, 3) __version__ = ".".join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index d4f716c1..067a73ba 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +- :release:`2.10.3 <2022-03-18>` - :release:`2.9.3 <2022-03-18>` - :bug:`1963` (via :issue:`1977`) Certificate-based pubkey auth was inadvertently broken when adding SHA2 support; this has been fixed. Reported -- cgit v1.2.3 From d4ff3dd430f6358b98dd104a4eb51bbc2a7ce844 Mon Sep 17 00:00:00 2001 From: Paul Howarth Date: Sun, 20 Mar 2022 21:09:54 +0000 Subject: Skip tests requiring sha1 signing if the backend doesn't support that Red Hat Enterprise Linux 9 will have SHA-1 signatures disabled by default. It is likely that SHA-1 signatures will disappear elsewhere over time too. This change detects if sha1 signatures are not supported by the backend and skips tests that rely on that functionality. This is a workaround for #2004. It would be good to reduce the reliance of the test suite on sha1 signatures except in the cases where that is explicitly being tested, and the markers added here give a decent starting point for seeing where to change things. --- tests/test_client.py | 24 +++++++++++++++++++++++- tests/test_pkey.py | 9 +++++++-- tests/test_transport.py | 7 ++++++- tests/util.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 10132aae..a0a321ce 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -41,13 +41,17 @@ from paramiko import SSHClient from paramiko.pkey import PublicBlob from paramiko.ssh_exception import SSHException, AuthenticationException -from .util import _support, slow +from .util import _support, sha1_signing_unsupported, slow requires_gss_auth = unittest.skipUnless( paramiko.GSS_AUTH_AVAILABLE, "GSS auth not available" ) +requires_sha1_signing = unittest.skipIf( + sha1_signing_unsupported(), "SHA-1 signing not supported" +) + FINGERPRINTS = { "ssh-dss": b"\x44\x78\xf0\xb9\xa2\x3c\xc5\x18\x20\x09\xff\x75\x5b\xc1\xd2\x6c", # noqa "ssh-rsa": b"\x60\x73\x38\x44\xcb\x51\x86\x65\x7f\xde\xda\xa2\x2b\x5a\x57\xd5", # noqa @@ -235,33 +239,39 @@ class ClientTest(unittest.TestCase): class SSHClientTest(ClientTest): + @requires_sha1_signing def test_client(self): """ verify that the SSHClient stuff works too. """ self._test_connection(password="pygmalion") + @requires_sha1_signing def test_client_dsa(self): """ verify that SSHClient works with a DSA key. """ self._test_connection(key_filename=_support("test_dss.key")) + @requires_sha1_signing def test_client_rsa(self): """ verify that SSHClient works with an RSA key. """ self._test_connection(key_filename=_support("test_rsa.key")) + @requires_sha1_signing def test_client_ecdsa(self): """ verify that SSHClient works with an ECDSA key. """ self._test_connection(key_filename=_support("test_ecdsa_256.key")) + @requires_sha1_signing def test_client_ed25519(self): self._test_connection(key_filename=_support("test_ed25519.key")) + @requires_sha1_signing def test_multiple_key_files(self): """ verify that SSHClient accepts and tries multiple key files. @@ -293,6 +303,7 @@ class SSHClientTest(ClientTest): self.tearDown() self.setUp() + @requires_sha1_signing def test_multiple_key_files_failure(self): """ Expect failure when multiple keys in play and none are accepted @@ -306,6 +317,7 @@ class SSHClientTest(ClientTest): allowed_keys=["ecdsa-sha2-nistp256"], ) + @requires_sha1_signing def test_certs_allowed_as_key_filename_values(self): # NOTE: giving cert path here, not key path. (Key path test is below. # They're similar except for which path is given; the expected auth and @@ -319,6 +331,7 @@ class SSHClientTest(ClientTest): public_blob=PublicBlob.from_file(cert_path), ) + @requires_sha1_signing def test_certs_implicitly_loaded_alongside_key_filename_keys(self): # NOTE: a regular test_connection() w/ test_rsa.key would incidentally # test this (because test_xxx.key-cert.pub exists) but incidental tests @@ -469,6 +482,7 @@ class SSHClientTest(ClientTest): kwargs = dict(self.connect_kwargs, banner_timeout=0.5) self.assertRaises(paramiko.SSHException, self.tc.connect, **kwargs) + @requires_sha1_signing def test_auth_trickledown(self): """ Failed key auth doesn't prevent subsequent pw auth from succeeding @@ -489,6 +503,7 @@ class SSHClientTest(ClientTest): ) self._test_connection(**kwargs) + @requires_sha1_signing @slow def test_auth_timeout(self): """ @@ -591,6 +606,7 @@ class SSHClientTest(ClientTest): host_key = paramiko.ECDSAKey.generate() self._client_host_key_bad(host_key) + @requires_sha1_signing def test_host_key_negotiation_2(self): host_key = paramiko.RSAKey.generate(2048) self._client_host_key_bad(host_key) @@ -598,6 +614,7 @@ class SSHClientTest(ClientTest): def test_host_key_negotiation_3(self): self._client_host_key_good(paramiko.ECDSAKey, "test_ecdsa_256.key") + @requires_sha1_signing def test_host_key_negotiation_4(self): self._client_host_key_good(paramiko.RSAKey, "test_rsa.key") @@ -681,6 +698,7 @@ class PasswordPassphraseTests(ClientTest): # instead of suffering a real connection cycle. # TODO: in that case, move the below to be part of an integration suite? + @requires_sha1_signing def test_password_kwarg_works_for_password_auth(self): # Straightforward / duplicate of earlier basic password test. self._test_connection(password="pygmalion") @@ -688,10 +706,12 @@ class PasswordPassphraseTests(ClientTest): # 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") + @requires_sha1_signing def test_passphrase_kwarg_used_for_key_passphrase(self): # Straightforward again, with new passphrase kwarg. self._test_connection( @@ -699,6 +719,7 @@ class PasswordPassphraseTests(ClientTest): passphrase="television", ) + @requires_sha1_signing def test_password_kwarg_used_for_passphrase_when_no_passphrase_kwarg_given( self ): # noqa @@ -709,6 +730,7 @@ class PasswordPassphraseTests(ClientTest): ) @raises(AuthenticationException) # TODO: more granular + @requires_sha1_signing def test_password_kwarg_not_used_for_passphrase_when_passphrase_kwarg_given( # noqa self ): diff --git a/tests/test_pkey.py b/tests/test_pkey.py index ed62da21..6eac35a0 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -44,9 +44,13 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateNumbers from mock import patch, Mock import pytest -from .util import _support, is_low_entropy +from .util import _support, is_low_entropy, sha1_signing_unsupported +requires_sha1_signing = unittest.skipIf( + sha1_signing_unsupported(), "SHA-1 signing not supported" +) + # from openssh's ssh-keygen PUB_RSA = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4c=" # noqa PUB_DSS = "ssh-dss AAAAB3NzaC1kc3MAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgwj/oLjYbVtjAy6pl/h0EKCWx2rf1IetyNsTxWrniA9I6HeDj65X1FyDkg6g8tvCnaNB8Xp/UUhuzHuGsMIipRxBxw9LF608EqZcj1E3ytktoW5B5OcjrkEoz3xG7C+rpIjYvAAAAFQDwz4UnmsGiSNu5iqjn3uTzwUpshwAAAIEAkxfFeY8P2wZpDjX0MimZl5wkoFQDL25cPzGBuB4OnB8NoUk/yjAHIIpEShw8V+LzouMK5CTJQo5+Ngw3qIch/WgRmMHy4kBq1SsXMjQCte1So6HBMvBPIW5SiMTmjCfZZiw4AYHK+B/JaOwaG9yRg2Ejg4Ok10+XFDxlqZo8Y+wAAACARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lYukmnjO1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+NwacIBlXa8cMDL7Q/69o0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgE=" # noqa @@ -136,7 +140,6 @@ x1234 = b"\x01\x02\x03\x04" TEST_KEY_BYTESTR_2 = "\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01#\x00\x00\x00\x81\x00\xd3\x8fV\xea\x07\x85\xa6k%\x8d<\x1f\xbc\x8dT\x98\xa5\x96$\xf3E#\xbe>\xbc\xd2\x93\x93\x87f\xceD\x18\xdb \x0c\xb3\xa1a\x96\xf8e#\xcc\xacS\x8a#\xefVlE\x83\x1epv\xc1o\x17M\xef\xdf\x89DUXL\xa6\x8b\xaa<\x06\x10\xd7\x93w\xec\xaf\xe2\xaf\x95\xd8\xfb\xd9\xbfw\xcb\x9f0)#y{\x10\x90\xaa\x85l\tPru\x8c\t\x19\xce\xa0\xf1\xd2\xdc\x8e/\x8b\xa8f\x9c0\xdey\x84\xd2F\xf7\xcbmm\x1f\x87" # noqa TEST_KEY_BYTESTR_3 = "\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01#\x00\x00\x00\x00ӏV\x07k%<\x1fT$E#>ғfD\x18 \x0cae#̬S#VlE\x1epvo\x17M߉DUXL<\x06\x10דw\u2bd5ٿw˟0)#y{\x10l\tPru\t\x19Π\u070e/f0yFmm\x1f" # noqa - class KeyTest(unittest.TestCase): def setUp(self): pass @@ -256,6 +259,7 @@ class KeyTest(unittest.TestCase): pub = RSAKey(data=key.asbytes()) self.assertTrue(pub.verify_ssh_sig(b"ice weasels", msg)) + @requires_sha1_signing def test_sign_and_verify_ssh_rsa(self): self._sign_and_verify_rsa("ssh-rsa", SIGNED_RSA) @@ -280,6 +284,7 @@ class KeyTest(unittest.TestCase): pub = DSSKey(data=key.asbytes()) self.assertTrue(pub.verify_ssh_sig(b"ice weasels", msg)) + @requires_sha1_signing def test_generate_rsa(self): key = RSAKey.generate(1024) msg = key.sign_ssh_data(b"jerri blank") diff --git a/tests/test_transport.py b/tests/test_transport.py index a9262f3d..8124f129 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -61,7 +61,7 @@ from paramiko.common import ( from paramiko.py3compat import bytes, byte_chr from paramiko.message import Message -from .util import needs_builtin, _support, slow +from .util import needs_builtin, _support, sha1_signing_unsupported, slow from .loop import LoopSocket @@ -77,6 +77,9 @@ Note: An SSH banner may eventually appear. Maybe. """ +requires_sha1_signing = unittest.skipIf( + sha1_signing_unsupported(), "SHA-1 signing not supported" +) class NullServer(ServerInterface): paranoid_did_password = False @@ -1283,6 +1286,7 @@ class TestSHA2SignatureKeyExchange(unittest.TestCase): # are new tests in test_pkey.py which use known signature blobs to prove # the SHA2 family was in fact used! + @requires_sha1_signing def test_base_case_ssh_rsa_still_used_as_fallback(self): # Prove that ssh-rsa is used if either, or both, participants have SHA2 # algorithms disabled @@ -1405,6 +1409,7 @@ class TestSHA2SignaturePubkeys(unittest.TestCase): ) as (tc, ts, err): assert isinstance(err, AuthenticationException) + @requires_sha1_signing def test_ssh_rsa_still_used_when_sha2_disabled(self): privkey = RSAKey.from_private_key_file(_support("test_rsa.key")) # NOTE: this works because key obj comparison uses public bytes diff --git a/tests/util.py b/tests/util.py index 1355ce8a..69df87f5 100644 --- a/tests/util.py +++ b/tests/util.py @@ -9,6 +9,9 @@ import pytest from paramiko.py3compat import builtins, PY2 from paramiko.ssh_gss import GSS_AUTH_AVAILABLE +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding, rsa tests_dir = dirname(realpath(__file__)) @@ -144,3 +147,28 @@ def is_low_entropy(): # I don't see a way to tell internally if the hash seed was set this # way, but env should be plenty sufficient, this is only for testing. return is_32bit and os.environ.get("PYTHONHASHSEED", None) == "0" + + +def sha1_signing_unsupported(): + """ + This is used to skip tests in environments where SHA-1 signing is + not supported by the backend. + """ + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + message = b"Some dummy text" + try: + signature = private_key.sign( + message, + padding.PSS( + mgf=padding.MGF1(hashes.SHA1()), + salt_length=padding.PSS.MAX_LENGTH + ), + hashes.SHA1() + ) + return False + except UnsupportedAlgorithm as e: + return e._reason is _Reasons.UNSUPPORTED_HASH + -- cgit v1.2.3 From c5b1714f88cb03d6802ca3c98f3b7f65bc7e4fd5 Mon Sep 17 00:00:00 2001 From: Paul Howarth Date: Mon, 21 Mar 2022 09:28:57 +0000 Subject: blacken --- tests/test_pkey.py | 1 + tests/test_transport.py | 1 + tests/util.py | 8 +++----- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 6eac35a0..e0d1b0ac 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -140,6 +140,7 @@ x1234 = b"\x01\x02\x03\x04" TEST_KEY_BYTESTR_2 = "\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01#\x00\x00\x00\x81\x00\xd3\x8fV\xea\x07\x85\xa6k%\x8d<\x1f\xbc\x8dT\x98\xa5\x96$\xf3E#\xbe>\xbc\xd2\x93\x93\x87f\xceD\x18\xdb \x0c\xb3\xa1a\x96\xf8e#\xcc\xacS\x8a#\xefVlE\x83\x1epv\xc1o\x17M\xef\xdf\x89DUXL\xa6\x8b\xaa<\x06\x10\xd7\x93w\xec\xaf\xe2\xaf\x95\xd8\xfb\xd9\xbfw\xcb\x9f0)#y{\x10\x90\xaa\x85l\tPru\x8c\t\x19\xce\xa0\xf1\xd2\xdc\x8e/\x8b\xa8f\x9c0\xdey\x84\xd2F\xf7\xcbmm\x1f\x87" # noqa TEST_KEY_BYTESTR_3 = "\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01#\x00\x00\x00\x00ӏV\x07k%<\x1fT$E#>ғfD\x18 \x0cae#̬S#VlE\x1epvo\x17M߉DUXL<\x06\x10דw\u2bd5ٿw˟0)#y{\x10l\tPru\t\x19Π\u070e/f0yFmm\x1f" # noqa + class KeyTest(unittest.TestCase): def setUp(self): pass diff --git a/tests/test_transport.py b/tests/test_transport.py index 8124f129..b9daec60 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -81,6 +81,7 @@ requires_sha1_signing = unittest.skipIf( sha1_signing_unsupported(), "SHA-1 signing not supported" ) + class NullServer(ServerInterface): paranoid_did_password = False paranoid_did_public_key = False diff --git a/tests/util.py b/tests/util.py index 69df87f5..ce96dc88 100644 --- a/tests/util.py +++ b/tests/util.py @@ -155,8 +155,7 @@ def sha1_signing_unsupported(): not supported by the backend. """ private_key = rsa.generate_private_key( - public_exponent=65537, - key_size=2048, + public_exponent=65537, key_size=2048 ) message = b"Some dummy text" try: @@ -164,11 +163,10 @@ def sha1_signing_unsupported(): message, padding.PSS( mgf=padding.MGF1(hashes.SHA1()), - salt_length=padding.PSS.MAX_LENGTH + salt_length=padding.PSS.MAX_LENGTH, ), - hashes.SHA1() + hashes.SHA1(), ) return False except UnsupportedAlgorithm as e: return e._reason is _Reasons.UNSUPPORTED_HASH - -- cgit v1.2.3 From 5dcdb2c1f258f66331f648503b0d482c4e326a43 Mon Sep 17 00:00:00 2001 From: Paul Howarth Date: Mon, 21 Mar 2022 09:34:27 +0000 Subject: Remove unused variable, keep lint happy --- tests/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/util.py b/tests/util.py index ce96dc88..e6551ac7 100644 --- a/tests/util.py +++ b/tests/util.py @@ -159,7 +159,7 @@ def sha1_signing_unsupported(): ) message = b"Some dummy text" try: - signature = private_key.sign( + private_key.sign( message, padding.PSS( mgf=padding.MGF1(hashes.SHA1()), -- cgit v1.2.3 From 43189ef6f2b55b4a09c042ce3fd4d1703c2091e3 Mon Sep 17 00:00:00 2001 From: Paul Howarth Date: Mon, 21 Mar 2022 11:25:11 +0000 Subject: Fix for compatibility with old versions of cryptography --- tests/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/util.py b/tests/util.py index e6551ac7..4bf73949 100644 --- a/tests/util.py +++ b/tests/util.py @@ -10,6 +10,7 @@ from paramiko.py3compat import builtins, PY2 from paramiko.ssh_gss import GSS_AUTH_AVAILABLE from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding, rsa @@ -155,7 +156,7 @@ def sha1_signing_unsupported(): not supported by the backend. """ private_key = rsa.generate_private_key( - public_exponent=65537, key_size=2048 + public_exponent=65537, key_size=2048, backend=default_backend() ) message = b"Some dummy text" try: -- cgit v1.2.3 From 3b029927fa2f5f1c2706d6716755cc2588caee3f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 25 Mar 2022 18:20:37 -0400 Subject: Refactor sha1 test skipping a tad --- tests/test_client.py | 6 +----- tests/test_pkey.py | 6 +----- tests/test_transport.py | 6 +----- tests/util.py | 4 ++++ 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index a0a321ce..1c28d824 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -41,17 +41,13 @@ from paramiko import SSHClient from paramiko.pkey import PublicBlob from paramiko.ssh_exception import SSHException, AuthenticationException -from .util import _support, sha1_signing_unsupported, slow +from .util import _support, requires_sha1_signing, slow requires_gss_auth = unittest.skipUnless( paramiko.GSS_AUTH_AVAILABLE, "GSS auth not available" ) -requires_sha1_signing = unittest.skipIf( - sha1_signing_unsupported(), "SHA-1 signing not supported" -) - FINGERPRINTS = { "ssh-dss": b"\x44\x78\xf0\xb9\xa2\x3c\xc5\x18\x20\x09\xff\x75\x5b\xc1\xd2\x6c", # noqa "ssh-rsa": b"\x60\x73\x38\x44\xcb\x51\x86\x65\x7f\xde\xda\xa2\x2b\x5a\x57\xd5", # noqa diff --git a/tests/test_pkey.py b/tests/test_pkey.py index e0d1b0ac..44ab688f 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -44,13 +44,9 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateNumbers from mock import patch, Mock import pytest -from .util import _support, is_low_entropy, sha1_signing_unsupported +from .util import _support, is_low_entropy, requires_sha1_signing -requires_sha1_signing = unittest.skipIf( - sha1_signing_unsupported(), "SHA-1 signing not supported" -) - # from openssh's ssh-keygen PUB_RSA = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4c=" # noqa PUB_DSS = "ssh-dss AAAAB3NzaC1kc3MAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgwj/oLjYbVtjAy6pl/h0EKCWx2rf1IetyNsTxWrniA9I6HeDj65X1FyDkg6g8tvCnaNB8Xp/UUhuzHuGsMIipRxBxw9LF608EqZcj1E3ytktoW5B5OcjrkEoz3xG7C+rpIjYvAAAAFQDwz4UnmsGiSNu5iqjn3uTzwUpshwAAAIEAkxfFeY8P2wZpDjX0MimZl5wkoFQDL25cPzGBuB4OnB8NoUk/yjAHIIpEShw8V+LzouMK5CTJQo5+Ngw3qIch/WgRmMHy4kBq1SsXMjQCte1So6HBMvBPIW5SiMTmjCfZZiw4AYHK+B/JaOwaG9yRg2Ejg4Ok10+XFDxlqZo8Y+wAAACARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lYukmnjO1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+NwacIBlXa8cMDL7Q/69o0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgE=" # noqa diff --git a/tests/test_transport.py b/tests/test_transport.py index b9daec60..52a49b0e 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -61,7 +61,7 @@ from paramiko.common import ( from paramiko.py3compat import bytes, byte_chr from paramiko.message import Message -from .util import needs_builtin, _support, sha1_signing_unsupported, slow +from .util import needs_builtin, _support, requires_sha1_signing, slow from .loop import LoopSocket @@ -77,10 +77,6 @@ Note: An SSH banner may eventually appear. Maybe. """ -requires_sha1_signing = unittest.skipIf( - sha1_signing_unsupported(), "SHA-1 signing not supported" -) - class NullServer(ServerInterface): paranoid_did_password = False diff --git a/tests/util.py b/tests/util.py index 4bf73949..542a0671 100644 --- a/tests/util.py +++ b/tests/util.py @@ -171,3 +171,7 @@ def sha1_signing_unsupported(): return False except UnsupportedAlgorithm as e: return e._reason is _Reasons.UNSUPPORTED_HASH + +requires_sha1_signing = unittest.skipIf( + sha1_signing_unsupported(), "SHA-1 signing not supported" +) -- cgit v1.2.3 From 9151b5a5ef6634142cc810193a59630c863549c3 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 25 Mar 2022 18:22:19 -0400 Subject: Changelog re #2011, closes #2004 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 067a73ba..4c033ab3 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +- :support:`2004` (via :issue:`2011`) Apply unittest ``skipIf`` to tests + currently using SHA1 in their critical path, to avoid failures on systems + starting to disable SHA1 outright in their crypto backends (eg RHEL 9). + Report & patch via Paul Howarth. - :release:`2.10.3 <2022-03-18>` - :release:`2.9.3 <2022-03-18>` - :bug:`1963` (via :issue:`1977`) Certificate-based pubkey auth was -- cgit v1.2.3 From 7a2c84afaada7a513ee482ba36e8848528b6f5f3 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 22 Apr 2022 19:11:03 -0400 Subject: Add -cert-v01@openssh.com variants to accepted host key algorithms Solves #2035 --- paramiko/transport.py | 10 +++++++++- sites/www/changelog.rst | 4 ++++ tests/test_transport.py | 8 +++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index b99b3278..83cedbf6 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -549,7 +549,15 @@ class Transport(threading.Thread, ClosingContextManager): @property def preferred_keys(self): - return self._filter_algorithm("keys") + # Interleave cert variants here; resistant to various background + # overwriting of _preferred_keys, and necessary as hostkeys can't use + # the logic pubkey auth does re: injecting/checking for certs at + # runtime + filtered = self._filter_algorithm("keys") + return tuple( + filtered + + tuple("{}-cert-v01@openssh.com".format(x) for x in filtered) + ) @property def preferred_pubkeys(self): diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 067a73ba..eb1e0704 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +- :bug:`2035` Servers offering certificate variants of hostkey algorithms (eg + ``ssh-rsa-cert-v01@openssh.com``) could not have their host keys verified by + Paramiko clients, as it only ever considered non-cert key types for that part + of connection handshaking. This has been fixed. - :release:`2.10.3 <2022-03-18>` - :release:`2.9.3 <2022-03-18>` - :bug:`1963` (via :issue:`1977`) Certificate-based pubkey auth was diff --git a/tests/test_transport.py b/tests/test_transport.py index 77ffd6c1..2eb95b31 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -1121,7 +1121,12 @@ class AlgorithmDisablingTests(unittest.TestCase): t = Transport(sock=Mock()) assert t.preferred_ciphers == t._preferred_ciphers assert t.preferred_macs == t._preferred_macs - assert t.preferred_keys == t._preferred_keys + assert t.preferred_keys == tuple( + t._preferred_keys + + tuple( + "{}-cert-v01@openssh.com".format(x) for x in t._preferred_keys + ) + ) assert t.preferred_kex == t._preferred_kex def test_preferred_lists_filter_disabled_algorithms(self): @@ -1140,6 +1145,7 @@ class AlgorithmDisablingTests(unittest.TestCase): assert "hmac-md5" not in t.preferred_macs assert "ssh-dss" in t._preferred_keys assert "ssh-dss" not in t.preferred_keys + assert "ssh-dss-cert-v01@openssh.com" not in t.preferred_keys assert "diffie-hellman-group14-sha256" in t._preferred_kex assert "diffie-hellman-group14-sha256" not in t.preferred_kex -- cgit v1.2.3 From f8036d69ff425b81b7070ce91e31252c72bfe632 Mon Sep 17 00:00:00 2001 From: Christopher Papke Date: Tue, 5 Apr 2022 14:54:42 -0700 Subject: don't throw exception when comparing PKey to non-PKey --- paramiko/pkey.py | 2 +- tests/test_pkey.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 7865a6ea..f494c80e 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -140,7 +140,7 @@ class PKey(object): return cmp(self.asbytes(), other.asbytes()) # noqa def __eq__(self, other): - return self._fields == other._fields + return isinstance(other, PKey) and self._fields == other._fields def __hash__(self): return hash(self._fields) diff --git a/tests/test_pkey.py b/tests/test_pkey.py index e652740c..687e776b 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -622,6 +622,11 @@ class KeyTest(unittest.TestCase): for key1, key2 in self.keys(): assert key1 == key2 + def test_keys_are_not_equal_to_other(self): + for value in [None, True, ""]: + for key1, _ in self.keys(): + assert key1 != value + def test_keys_are_hashable(self): # NOTE: this isn't a great test due to hashseed randomization under # Python 3 preventing use of static values, but it does still prove -- cgit v1.2.3 From d7fe051087fc9bd31dc0c42da63b3ae4852f6d2d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Apr 2022 08:14:06 -0400 Subject: Changelog re #1964, #2024, #2023 --- sites/www/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 358b8d8e..a7c6b2e6 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,12 @@ Changelog ========= +- :bug:`1964` (via :issue:`2024` as also reported in :issue:`2023`) + `~paramiko.pkey.PKey` instances' ``__eq__`` did not have the usual safety + guard in place to ensure they were being compared to another ``PKey`` object, + causing occasional spurious ``BadHostKeyException`` (among other things). + This has been fixed. Thanks to Shengdun Hua for the original report/patch and + to Christopher Papke for the final version of the fix. - :release:`2.9.3 <2022-03-18>` - :bug:`1963` (via :issue:`1977`) Certificate-based pubkey auth was inadvertently broken when adding SHA2 support; this has been fixed. Reported -- cgit v1.2.3 From df1701c1834cae333d5e6d9f41b0a4bea3da72e4 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Apr 2022 08:16:32 -0400 Subject: blacken --- tests/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/util.py b/tests/util.py index 542a0671..3ec5d092 100644 --- a/tests/util.py +++ b/tests/util.py @@ -172,6 +172,7 @@ def sha1_signing_unsupported(): except UnsupportedAlgorithm as e: return e._reason is _Reasons.UNSUPPORTED_HASH + requires_sha1_signing = unittest.skipIf( sha1_signing_unsupported(), "SHA-1 signing not supported" ) -- cgit v1.2.3