diff options
author | Jeff Forcier <jeff@bitprophet.org> | 2022-04-25 10:32:31 -0400 |
---|---|---|
committer | Jeff Forcier <jeff@bitprophet.org> | 2022-04-25 10:32:31 -0400 |
commit | 9b22c28a02e5ae0291857a7fb9051cf942280731 (patch) | |
tree | 74a2aa4583f2917ee73cdbe4b4ca9b0cc280b96a /paramiko | |
parent | ab335cdab8d6dc218e5d8658c3b32f4e7d0d74e5 (diff) | |
parent | df1701c1834cae333d5e6d9f41b0a4bea3da72e4 (diff) | |
download | paramiko-9b22c28a02e5ae0291857a7fb9051cf942280731.tar.gz |
Merge branch 'main' into 1951-int
Diffstat (limited to 'paramiko')
44 files changed, 531 insertions, 194 deletions
diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 8642f84a..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 @@ -42,6 +42,7 @@ from paramiko.ssh_exception import ( ChannelException, ConfigParseError, CouldNotCanonicalize, + IncompatiblePeer, PasswordRequiredException, ProxyCommandFailure, SSHException, diff --git a/paramiko/_version.py b/paramiko/_version.py index 0f0c6561..82bc1161 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (2, 8, 1) +__version_info__ = (2, 10, 3) __version__ = ".".join(map(str, __version_info__)) diff --git a/paramiko/agent.py b/paramiko/agent.py index c7c8b7cb..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 @@ -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): @@ -193,6 +205,34 @@ 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: @@ -219,24 +259,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: - return - else: - # no agent support + conn = get_agent_connection() + if not conn: return self._conn = conn @@ -354,27 +378,17 @@ 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): 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: - return - else: - # no agent support + conn = get_agent_connection() + if not conn: return self._connect(conn) @@ -411,12 +425,12 @@ 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) 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/paramiko/auth_handler.py b/paramiko/auth_handler.py index 5c7d6be6..c188242e 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` @@ -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,7 +206,19 @@ class AuthHandler(object): self.transport._send_message(m) self.transport.close() - def _get_session_blob(self, key, service, username): + def _get_key_type_and_bits(self, key): + """ + Given any key, return its type/algorithm & bits-to-sign. + + Intended for input to or verification of, key signatures. + """ + # Use certificate contents, if available, plain pubkey otherwise + if key.public_blob: + return key.public_blob.key_type, key.public_blob.key_blob + else: + return key.get_name(), key + + def _get_session_blob(self, key, service, username, algorithm): m = Message() m.add_string(self.transport.session_id) m.add_byte(cMSG_USERAUTH_REQUEST) @@ -214,13 +226,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) + _, bits = self._get_key_type_and_bits(key) + m.add_string(algorithm) + m.add_string(bits) return m.asbytes() def wait_for_response(self, event): @@ -269,9 +277,81 @@ 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: + 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_algo_str = u( + self.transport.server_extensions.get("server-sig-algs", b("")) + ) + 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 + ) + if key_type.endswith("-cert-v01@openssh.com"): + pubkey_algo += "-cert-v01@openssh.com" + self.transport._agreed_pubkey_algorithm = pubkey_algo + return pubkey_algo + 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) @@ -284,18 +364,17 @@ 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) + 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) + sig = self.private_key.sign_ssh_data(blob, algorithm) m.add_string(sig) elif self.auth_method == "keyboard-interactive": m.add_string("") @@ -505,10 +584,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 @@ -532,12 +614,14 @@ Error Message: {} # signs anything... send special "ok" message m = Message() m.add_byte(cMSG_USERAUTH_PK_OK) - m.add_string(keytype) + 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 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 7bd0cb10..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. @@ -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/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 ba1f38c3..f6570271 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. @@ -27,6 +27,7 @@ import os import re import shlex import socket +from hashlib import sha1 from functools import partial from .py3compat import StringIO @@ -59,14 +60,14 @@ 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"], + "identityfile": ["%C", "~", "%d", "%h", "%l", "%u", "%r"], "proxycommand": ["~", "%h", "%p", "%r"], "proxyjump": ["~", "%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): @@ -433,10 +434,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: %%??? - # TODO: %C? + "%C": sha1(tohash.encode()).hexdigest(), "%d": homedir, "%h": configured_hostname, # TODO: %i? diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index 09d6f648..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. @@ -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..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 @@ -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..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 @@ -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/file.py b/paramiko/file.py index 9e9f6eb8..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, @@ -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): # <http://www.python.org/doc/current/lib/built-in-funcs.html> 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/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_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..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 <paramiko.kex_group1.KexGroup1>` where the prime "p" and @@ -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..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 @@ -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/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 5bdfb1d4..585cb74a 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. @@ -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) @@ -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() @@ -551,7 +558,18 @@ 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). + # 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, *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/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 292d0ccc..f7971dca 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. @@ -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.replace("-cert-v01@openssh.com", "")) 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/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 6294fb48..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 @@ -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. 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 2789be99..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 @@ -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/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 8919043f..2168032f 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 @@ -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 @@ -515,7 +549,19 @@ 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): + return self._filter_algorithm("pubkeys") @property def preferred_kex(self): @@ -743,6 +789,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 +1332,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 +2188,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: @@ -2176,7 +2243,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: @@ -2237,7 +2304,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 +2320,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 @@ -2272,16 +2336,29 @@ 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, ) ) 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) @@ -2294,50 +2371,74 @@ class Transport(threading.Thread, ClosingContextManager): 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 - 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), + 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 ( + ("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 = ["<none>"] + value = ", ".join(value) + self._log(DEBUG, "{}: {}".format(prefix, value)) + 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 @@ -2351,11 +2452,14 @@ class Transport(threading.Thread, ClosingContextManager): filter(kex_algo_list.__contains__, self.preferred_kex) ) if len(agreed_kex) == 0: - raise SSHException( + # TODO: do an auth-overhaul style aggregate exception here? + # TODO: would let us streamline log output & show all failures up + # front + raise IncompatiblePeer( "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( @@ -2374,12 +2478,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]) @@ -2411,7 +2515,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] @@ -2435,7 +2539,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( @@ -2474,7 +2580,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, @@ -2488,6 +2594,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 @@ -2573,6 +2680,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) @@ -2588,6 +2709,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() @@ -2855,6 +2990,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, @@ -2877,6 +3013,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. diff --git a/paramiko/util.py b/paramiko/util.py index 93970289..4267caf1 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. @@ -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): diff --git a/paramiko/win_openssh.py b/paramiko/win_openssh.py new file mode 100644 index 00000000..5dd71cd4 --- /dev/null +++ b/paramiko/win_openssh.py @@ -0,0 +1,40 @@ +# Copyright (C) 2021 Lew Gordon <lew.gordon@genesys.com> +# Copyright (C) 2022 Patrick Spendrin <ps_ml@gmx.de> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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() 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. |