summaryrefslogtreecommitdiff
path: root/paramiko
diff options
context:
space:
mode:
authorJeff Forcier <jeff@bitprophet.org>2022-04-25 10:32:31 -0400
committerJeff Forcier <jeff@bitprophet.org>2022-04-25 10:32:31 -0400
commit9b22c28a02e5ae0291857a7fb9051cf942280731 (patch)
tree74a2aa4583f2917ee73cdbe4b4ca9b0cc280b96a /paramiko
parentab335cdab8d6dc218e5d8658c3b32f4e7d0d74e5 (diff)
parentdf1701c1834cae333d5e6d9f41b0a4bea3da72e4 (diff)
downloadparamiko-9b22c28a02e5ae0291857a7fb9051cf942280731.tar.gz
Merge branch 'main' into 1951-int
Diffstat (limited to 'paramiko')
-rw-r--r--paramiko/__init__.py3
-rw-r--r--paramiko/_version.py2
-rw-r--r--paramiko/agent.py88
-rw-r--r--paramiko/auth_handler.py132
-rw-r--r--paramiko/ber.py2
-rw-r--r--paramiko/buffered_pipe.py2
-rw-r--r--paramiko/channel.py2
-rw-r--r--paramiko/client.py2
-rw-r--r--paramiko/common.py7
-rw-r--r--paramiko/compress.py2
-rw-r--r--paramiko/config.py12
-rw-r--r--paramiko/dsskey.py4
-rw-r--r--paramiko/ecdsakey.py4
-rw-r--r--paramiko/ed25519key.py4
-rw-r--r--paramiko/file.py11
-rw-r--r--paramiko/hostkeys.py2
-rw-r--r--paramiko/kex_curve25519.py4
-rw-r--r--paramiko/kex_ecdh_nist.py4
-rw-r--r--paramiko/kex_gex.py6
-rw-r--r--paramiko/kex_group1.py6
-rw-r--r--paramiko/kex_group14.py2
-rw-r--r--paramiko/kex_group16.py2
-rw-r--r--paramiko/kex_gss.py2
-rw-r--r--paramiko/message.py2
-rw-r--r--paramiko/packet.py2
-rw-r--r--paramiko/pipe.py2
-rw-r--r--paramiko/pkey.py28
-rw-r--r--paramiko/primes.py2
-rw-r--r--paramiko/proxy.py2
-rw-r--r--paramiko/rsakey.py30
-rw-r--r--paramiko/server.py2
-rw-r--r--paramiko/sftp.py2
-rw-r--r--paramiko/sftp_attr.py2
-rw-r--r--paramiko/sftp_client.py6
-rw-r--r--paramiko/sftp_file.py2
-rw-r--r--paramiko/sftp_handle.py2
-rw-r--r--paramiko/sftp_server.py2
-rw-r--r--paramiko/sftp_si.py2
-rw-r--r--paramiko/ssh_exception.py17
-rw-r--r--paramiko/ssh_gss.py2
-rw-r--r--paramiko/transport.py251
-rw-r--r--paramiko/util.py20
-rw-r--r--paramiko/win_openssh.py40
-rw-r--r--paramiko/win_pageant.py2
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.