summaryrefslogtreecommitdiff
path: root/paramiko
diff options
context:
space:
mode:
authorAdam Meily <meily.adam@gmail.com>2015-12-17 20:18:00 -0500
committerAdam Meily <meily.adam@gmail.com>2015-12-17 20:18:00 -0500
commit9a87a8eb599d2b3914a3f3fcedbea8b25ab33d5a (patch)
tree9ca66cf1c5f262e55f5c9cf420d316a6f25d0a00 /paramiko
parent787839d3bc41cdd86905df6883b7de2b1bfb165a (diff)
parent844dda88ca1827ede2c3f73792facab7d28eef74 (diff)
downloadparamiko-9a87a8eb599d2b3914a3f3fcedbea8b25ab33d5a.tar.gz
Merge branch 'master' of github.com:paramiko/paramiko
Conflicts: paramiko/sftp_file.py sites/www/changelog.rst
Diffstat (limited to 'paramiko')
-rw-r--r--paramiko/_winapi.py143
-rw-r--r--paramiko/agent.py20
-rw-r--r--paramiko/auth_handler.py24
-rw-r--r--paramiko/buffered_pipe.py4
-rw-r--r--paramiko/channel.py18
-rw-r--r--paramiko/client.py32
-rw-r--r--paramiko/config.py4
-rw-r--r--paramiko/ecdsakey.py8
-rw-r--r--paramiko/hostkeys.py8
-rw-r--r--paramiko/kex_gex.py13
-rw-r--r--paramiko/kex_group1.py1
-rw-r--r--paramiko/kex_group14.py2
-rw-r--r--paramiko/kex_gss.py5
-rw-r--r--paramiko/message.py45
-rw-r--r--paramiko/packet.py49
-rw-r--r--paramiko/pkey.py24
-rw-r--r--paramiko/py3compat.py5
-rw-r--r--paramiko/server.py2
-rw-r--r--paramiko/sftp_attr.py5
-rw-r--r--paramiko/sftp_client.py8
-rw-r--r--paramiko/sftp_file.py7
-rw-r--r--paramiko/ssh_exception.py8
-rw-r--r--paramiko/ssh_gss.py31
-rw-r--r--paramiko/transport.py241
-rw-r--r--paramiko/util.py2
25 files changed, 512 insertions, 197 deletions
diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py
index d6aabf76..9a8bdedd 100644
--- a/paramiko/_winapi.py
+++ b/paramiko/_winapi.py
@@ -1,23 +1,16 @@
"""
Windows API functions implemented as ctypes functions and classes as found
-in jaraco.windows (2.10).
+in jaraco.windows (3.3).
If you encounter issues with this module, please consider reporting the issues
in jaraco.windows and asking the author to port the fixes back here.
"""
-import ctypes
+import sys
import ctypes.wintypes
-from paramiko.py3compat import u
-try:
- import builtins
-except ImportError:
- import __builtin__ as builtins
-try:
- USHORT = ctypes.wintypes.USHORT
-except AttributeError:
- USHORT = ctypes.c_ushort
+from paramiko.py3compat import u, builtins
+
######################
# jaraco.windows.error
@@ -29,11 +22,7 @@ def format_system_message(errno):
"""
# first some flags used by FormatMessageW
ALLOCATE_BUFFER = 0x100
- ARGUMENT_ARRAY = 0x2000
- FROM_HMODULE = 0x800
- FROM_STRING = 0x400
FROM_SYSTEM = 0x1000
- IGNORE_INSERTS = 0x200
# Let FormatMessageW allocate the buffer (we'll free it below)
# Also, let it know we want a system error message.
@@ -44,7 +33,7 @@ def format_system_message(errno):
result_buffer = ctypes.wintypes.LPWSTR()
buffer_size = 0
arguments = None
- format_bytes = ctypes.windll.kernel32.FormatMessageW(
+ bytes = ctypes.windll.kernel32.FormatMessageW(
flags,
source,
message_id,
@@ -52,11 +41,11 @@ def format_system_message(errno):
ctypes.byref(result_buffer),
buffer_size,
arguments,
- )
+ )
# note the following will cause an infinite loop if GetLastError
# repeatedly returns an error that cannot be formatted, although
# this should not happen.
- handle_nonzero_success(format_bytes)
+ handle_nonzero_success(bytes)
message = result_buffer.value
ctypes.windll.kernel32.LocalFree(result_buffer)
return message
@@ -69,7 +58,11 @@ class WindowsError(builtins.WindowsError):
if value is None:
value = ctypes.windll.kernel32.GetLastError()
strerror = format_system_message(value)
- super(WindowsError, self).__init__(value, strerror)
+ if sys.version_info > (3,3):
+ args = 0, strerror, None, value
+ else:
+ args = value, strerror
+ super(WindowsError, self).__init__(*args)
@property
def message(self):
@@ -90,6 +83,27 @@ def handle_nonzero_success(result):
raise WindowsError()
+###########################
+# jaraco.windows.api.memory
+
+GMEM_MOVEABLE = 0x2
+
+GlobalAlloc = ctypes.windll.kernel32.GlobalAlloc
+GlobalAlloc.argtypes = ctypes.wintypes.UINT, ctypes.c_ssize_t
+GlobalAlloc.restype = ctypes.wintypes.HANDLE
+
+GlobalLock = ctypes.windll.kernel32.GlobalLock
+GlobalLock.argtypes = ctypes.wintypes.HGLOBAL,
+GlobalLock.restype = ctypes.wintypes.LPVOID
+
+GlobalUnlock = ctypes.windll.kernel32.GlobalUnlock
+GlobalUnlock.argtypes = ctypes.wintypes.HGLOBAL,
+GlobalUnlock.restype = ctypes.wintypes.BOOL
+
+GlobalSize = ctypes.windll.kernel32.GlobalSize
+GlobalSize.argtypes = ctypes.wintypes.HGLOBAL,
+GlobalSize.restype = ctypes.c_size_t
+
CreateFileMapping = ctypes.windll.kernel32.CreateFileMappingW
CreateFileMapping.argtypes = [
ctypes.wintypes.HANDLE,
@@ -104,6 +118,9 @@ CreateFileMapping.restype = ctypes.wintypes.HANDLE
MapViewOfFile = ctypes.windll.kernel32.MapViewOfFile
MapViewOfFile.restype = ctypes.wintypes.HANDLE
+#####################
+# jaraco.windows.mmap
+
class MemoryMap(object):
"""
A memory map object which can have security attributes overridden.
@@ -136,10 +153,13 @@ class MemoryMap(object):
self.pos = pos
def write(self, msg):
+ assert isinstance(msg, bytes)
n = len(msg)
if self.pos + n >= self.length: # A little safety.
raise ValueError("Refusing to write %d bytes" % n)
- ctypes.windll.kernel32.RtlMoveMemory(self.view + self.pos, msg, n)
+ dest = self.view + self.pos
+ length = ctypes.wintypes.SIZE(n)
+ ctypes.windll.kernel32.RtlMoveMemory(dest, msg, length)
self.pos += n
def read(self, n):
@@ -147,7 +167,9 @@ class MemoryMap(object):
Read n bytes from mapped view.
"""
out = ctypes.create_string_buffer(n)
- ctypes.windll.kernel32.RtlMoveMemory(out, self.view + self.pos, n)
+ source = self.view + self.pos
+ length = ctypes.wintypes.SIZE(n)
+ ctypes.windll.kernel32.RtlMoveMemory(out, source, length)
self.pos += n
return out.raw
@@ -155,8 +177,71 @@ class MemoryMap(object):
ctypes.windll.kernel32.UnmapViewOfFile(self.view)
ctypes.windll.kernel32.CloseHandle(self.filemap)
-#########################
-# jaraco.windows.security
+#############################
+# jaraco.windows.api.security
+
+# from WinNT.h
+READ_CONTROL = 0x00020000
+STANDARD_RIGHTS_REQUIRED = 0x000F0000
+STANDARD_RIGHTS_READ = READ_CONTROL
+STANDARD_RIGHTS_WRITE = READ_CONTROL
+STANDARD_RIGHTS_EXECUTE = READ_CONTROL
+STANDARD_RIGHTS_ALL = 0x001F0000
+
+# from NTSecAPI.h
+POLICY_VIEW_LOCAL_INFORMATION = 0x00000001
+POLICY_VIEW_AUDIT_INFORMATION = 0x00000002
+POLICY_GET_PRIVATE_INFORMATION = 0x00000004
+POLICY_TRUST_ADMIN = 0x00000008
+POLICY_CREATE_ACCOUNT = 0x00000010
+POLICY_CREATE_SECRET = 0x00000020
+POLICY_CREATE_PRIVILEGE = 0x00000040
+POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080
+POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100
+POLICY_AUDIT_LOG_ADMIN = 0x00000200
+POLICY_SERVER_ADMIN = 0x00000400
+POLICY_LOOKUP_NAMES = 0x00000800
+POLICY_NOTIFICATION = 0x00001000
+
+POLICY_ALL_ACCESS = (
+ STANDARD_RIGHTS_REQUIRED |
+ POLICY_VIEW_LOCAL_INFORMATION |
+ POLICY_VIEW_AUDIT_INFORMATION |
+ POLICY_GET_PRIVATE_INFORMATION |
+ POLICY_TRUST_ADMIN |
+ POLICY_CREATE_ACCOUNT |
+ POLICY_CREATE_SECRET |
+ POLICY_CREATE_PRIVILEGE |
+ POLICY_SET_DEFAULT_QUOTA_LIMITS |
+ POLICY_SET_AUDIT_REQUIREMENTS |
+ POLICY_AUDIT_LOG_ADMIN |
+ POLICY_SERVER_ADMIN |
+ POLICY_LOOKUP_NAMES)
+
+
+POLICY_READ = (
+ STANDARD_RIGHTS_READ |
+ POLICY_VIEW_AUDIT_INFORMATION |
+ POLICY_GET_PRIVATE_INFORMATION)
+
+POLICY_WRITE = (
+ STANDARD_RIGHTS_WRITE |
+ POLICY_TRUST_ADMIN |
+ POLICY_CREATE_ACCOUNT |
+ POLICY_CREATE_SECRET |
+ POLICY_CREATE_PRIVILEGE |
+ POLICY_SET_DEFAULT_QUOTA_LIMITS |
+ POLICY_SET_AUDIT_REQUIREMENTS |
+ POLICY_AUDIT_LOG_ADMIN |
+ POLICY_SERVER_ADMIN)
+
+POLICY_EXECUTE = (
+ STANDARD_RIGHTS_EXECUTE |
+ POLICY_VIEW_LOCAL_INFORMATION |
+ POLICY_LOOKUP_NAMES)
+
+class TokenAccess:
+ TOKEN_QUERY = 0x8
class TokenInformationClass:
TokenUser = 1
@@ -182,7 +267,7 @@ class SECURITY_DESCRIPTOR(ctypes.Structure):
PACL Dacl;
} SECURITY_DESCRIPTOR;
"""
- SECURITY_DESCRIPTOR_CONTROL = USHORT
+ SECURITY_DESCRIPTOR_CONTROL = ctypes.wintypes.USHORT
REVISION = 1
_fields_ = [
@@ -219,8 +304,11 @@ class SECURITY_ATTRIBUTES(ctypes.Structure):
@descriptor.setter
def descriptor(self, value):
- self._descriptor = descriptor
- self.lpSecurityDescriptor = ctypes.addressof(descriptor)
+ self._descriptor = value
+ self.lpSecurityDescriptor = ctypes.addressof(value)
+
+#########################
+# jaraco.windows.security
def GetTokenInformation(token, information_class):
"""
@@ -236,9 +324,6 @@ def GetTokenInformation(token, information_class):
ctypes.byref(data_size)))
return ctypes.cast(data, ctypes.POINTER(TOKEN_USER)).contents
-class TokenAccess:
- TOKEN_QUERY = 0x8
-
def OpenProcessToken(proc_handle, access):
result = ctypes.wintypes.HANDLE()
proc_handle = ctypes.wintypes.HANDLE(proc_handle)
diff --git a/paramiko/agent.py b/paramiko/agent.py
index f928881e..6a8e7fb4 100644
--- a/paramiko/agent.py
+++ b/paramiko/agent.py
@@ -290,6 +290,26 @@ class AgentServerProxy(AgentSSH):
class AgentRequestHandler(object):
+ """
+ Primary/default implementation of SSH agent forwarding functionality.
+
+ Simply instantiate this class, handing it a live command-executing session
+ object, and it will handle forwarding any local SSH agent processes it
+ finds.
+
+ For example::
+
+ # Connect
+ client = SSHClient()
+ client.connect(host, port, username)
+ # Obtain session
+ session = client.get_transport().open_session()
+ # Forward local agent
+ AgentRequestHandler(session)
+ # Commands executed after this point will see the forwarded agent on
+ # the remote end.
+ session.exec_command("git clone https://my.git.repository/")
+ """
def __init__(self, chanClient):
self._conn = None
self.__chanC = chanClient
diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py
index c001aeee..ef4a8c7e 100644
--- a/paramiko/auth_handler.py
+++ b/paramiko/auth_handler.py
@@ -34,7 +34,7 @@ from paramiko.common import cMSG_SERVICE_REQUEST, cMSG_DISCONNECT, \
cMSG_USERAUTH_GSSAPI_ERRTOK, cMSG_USERAUTH_GSSAPI_MIC,\
MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN, \
MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, MSG_USERAUTH_GSSAPI_ERROR, \
- MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC
+ MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC, MSG_NAMES
from paramiko.message import Message
from paramiko.py3compat import bytestring
@@ -510,15 +510,11 @@ class AuthHandler (object):
result = AUTH_FAILED
self._send_auth_result(username, method, result)
raise
- if retval == 0:
- # TODO: Implement client credential saving.
- # The OpenSSH server is able to create a TGT with the delegated
- # client credentials, but this is not supported by GSS-API.
- result = AUTH_SUCCESSFUL
- self.transport.server_object.check_auth_gssapi_with_mic(
- username, result)
- else:
- result = AUTH_FAILED
+ # TODO: Implement client credential saving.
+ # The OpenSSH server is able to create a TGT with the delegated
+ # client credentials, but this is not supported by GSS-API.
+ result = AUTH_SUCCESSFUL
+ self.transport.server_object.check_auth_gssapi_with_mic(username, result)
elif method == "gssapi-keyex" and gss_auth:
mic_token = m.get_string()
sshgss = self.transport.kexgss_ctxt
@@ -534,12 +530,8 @@ class AuthHandler (object):
result = AUTH_FAILED
self._send_auth_result(username, method, result)
raise
- if retval == 0:
- result = AUTH_SUCCESSFUL
- self.transport.server_object.check_auth_gssapi_keyex(username,
- result)
- else:
- result = AUTH_FAILED
+ result = AUTH_SUCCESSFUL
+ self.transport.server_object.check_auth_gssapi_keyex(username, result)
else:
result = self.transport.server_object.check_auth_none(username)
# okay, send result
diff --git a/paramiko/buffered_pipe.py b/paramiko/buffered_pipe.py
index ac35b3e1..d5fe164e 100644
--- a/paramiko/buffered_pipe.py
+++ b/paramiko/buffered_pipe.py
@@ -81,7 +81,7 @@ class BufferedPipe (object):
Feed new data into this pipe. This method is assumed to be called
from a separate thread, so synchronization is done.
- :param data: the data to add, as a `str`
+ :param data: the data to add, as a `str` or `bytes`
"""
self._lock.acquire()
try:
@@ -125,7 +125,7 @@ class BufferedPipe (object):
:param int nbytes: maximum number of bytes to read
:param float timeout:
maximum seconds to wait (or ``None``, the default, to wait forever)
- :return: the read data, as a `str`
+ :return: the read data, as a `bytes`
:raises PipeTimeout:
if a timeout was specified and no data was ready before that
diff --git a/paramiko/channel.py b/paramiko/channel.py
index 7e39a15b..4ce4f286 100644
--- a/paramiko/channel.py
+++ b/paramiko/channel.py
@@ -286,7 +286,8 @@ class Channel (ClosingContextManager):
return an exit status in some cases (like bad servers).
:return:
- ``True`` if `recv_exit_status` will return immediately, else ``False``.
+ ``True`` if `recv_exit_status` will return immediately, else
+ ``False``.
.. versionadded:: 1.7.3
"""
@@ -300,6 +301,17 @@ class Channel (ClosingContextManager):
it does, or until the channel is closed. If no exit status is
provided by the server, -1 is returned.
+ .. warning::
+ In some situations, receiving remote output larger than the current
+ `.Transport` or session's ``window_size`` (e.g. that set by the
+ ``default_window_size`` kwarg for `.Transport.__init__`) will cause
+ `.recv_exit_status` to hang indefinitely if it is called prior to a
+ sufficiently large `~Channel..read` (or if there are no threads
+ calling `~Channel.read` in the background).
+
+ In these cases, ensuring that `.recv_exit_status` is called *after*
+ `~Channel.read` (or, again, using threads) can avoid the hang.
+
:return: the exit code (as an `int`) of the process on the server.
.. versionadded:: 1.2
@@ -587,7 +599,7 @@ class Channel (ClosingContextManager):
is returned, the channel stream has closed.
:param int nbytes: maximum number of bytes to read.
- :return: received data, as a `str`
+ :return: received data, as a `bytes`
:raises socket.timeout:
if no data is ready before the timeout set by `settimeout`.
@@ -975,7 +987,7 @@ class Channel (ClosingContextManager):
else:
ok = server.check_channel_env_request(self, name, value)
elif key == 'exec':
- cmd = m.get_text()
+ cmd = m.get_string()
if server is None:
ok = False
else:
diff --git a/paramiko/client.py b/paramiko/client.py
index e10e9da7..8d899a15 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -256,7 +256,8 @@ class SSHClient (ClosingContextManager):
:param socket sock:
an open socket or socket-like object (such as a `.Channel`) to use
for communication to the target host
- :param bool gss_auth: ``True`` if you want to use GSS-API authentication
+ :param bool gss_auth:
+ ``True`` if you want to use GSS-API authentication
:param bool gss_kex:
Perform GSS-API Key Exchange and user authentication
:param bool gss_deleg_creds: Delegate GSS-API client credentials or not
@@ -397,7 +398,7 @@ class SSHClient (ClosingContextManager):
:raises SSHException: if the server fails to execute the command
"""
- chan = self._transport.open_session()
+ chan = self._transport.open_session(timeout=timeout)
if get_pty:
chan.get_pty()
chan.settimeout(timeout)
@@ -463,7 +464,8 @@ class SSHClient (ClosingContextManager):
"""
saved_exception = None
two_factor = False
- allowed_types = []
+ allowed_types = set()
+ two_factor_types = set(['keyboard-interactive','password'])
# If GSS-API support and GSS-PI Key Exchange was performed, we attempt
# authentication with gssapi-keyex.
@@ -489,8 +491,8 @@ class SSHClient (ClosingContextManager):
if pkey is not None:
try:
self._log(DEBUG, 'Trying SSH key %s' % hexlify(pkey.get_fingerprint()))
- allowed_types = self._transport.auth_publickey(username, pkey)
- two_factor = (allowed_types == ['password'])
+ allowed_types = set(self._transport.auth_publickey(username, pkey))
+ two_factor = (allowed_types & two_factor_types)
if not two_factor:
return
except SSHException as e:
@@ -502,8 +504,8 @@ class SSHClient (ClosingContextManager):
try:
key = pkey_class.from_private_key_file(key_filename, password)
self._log(DEBUG, 'Trying key %s from %s' % (hexlify(key.get_fingerprint()), key_filename))
- self._transport.auth_publickey(username, key)
- two_factor = (allowed_types == ['password'])
+ allowed_types = set(self._transport.auth_publickey(username, key))
+ two_factor = (allowed_types & two_factor_types)
if not two_factor:
return
break
@@ -517,9 +519,9 @@ class SSHClient (ClosingContextManager):
for key in self._agent.get_keys():
try:
self._log(DEBUG, 'Trying SSH agent key %s' % hexlify(key.get_fingerprint()))
- # for 2-factor auth a successfully auth'd key will result in ['password']
- allowed_types = self._transport.auth_publickey(username, key)
- two_factor = (allowed_types == ['password'])
+ # for 2-factor auth a successfully auth'd key password will return an allowed 2fac auth method
+ allowed_types = set(self._transport.auth_publickey(username, key))
+ two_factor = (allowed_types & two_factor_types)
if not two_factor:
return
break
@@ -556,8 +558,8 @@ class SSHClient (ClosingContextManager):
key = pkey_class.from_private_key_file(filename, password)
self._log(DEBUG, 'Trying discovered key %s in %s' % (hexlify(key.get_fingerprint()), filename))
# for 2-factor auth a successfully auth'd key will result in ['password']
- allowed_types = self._transport.auth_publickey(username, key)
- two_factor = (allowed_types == ['password'])
+ allowed_types = set(self._transport.auth_publickey(username, key))
+ two_factor = (allowed_types & two_factor_types)
if not two_factor:
return
break
@@ -571,7 +573,11 @@ class SSHClient (ClosingContextManager):
except SSHException as e:
saved_exception = e
elif two_factor:
- raise SSHException('Two-factor authentication requires a password')
+ try:
+ self._transport.auth_interactive_dumb(username)
+ return
+ except SSHException as e:
+ saved_exception = e
# if we got an auth-failed exception earlier, re-raise it
if saved_exception is not None:
diff --git a/paramiko/config.py b/paramiko/config.py
index 233a87d9..0b1345fd 100644
--- a/paramiko/config.py
+++ b/paramiko/config.py
@@ -53,7 +53,7 @@ class SSHConfig (object):
"""
Read an OpenSSH config from the given file object.
- :param file file_obj: a file-like object to read the config file from
+ :param file_obj: a file-like object to read the config file from
"""
host = {"host": ['*'], "config": {}}
for line in file_obj:
@@ -98,7 +98,7 @@ class SSHConfig (object):
The host-matching rules of OpenSSH's ``ssh_config`` man page are used:
For each parameter, the first obtained value will be used. The
- configuration files contain sections separated by ``Host''
+ configuration files contain sections separated by ``Host``
specifications, and that section is only applied for hosts that match
one of the patterns given in the specification.
diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py
index 6b047959..8827a1db 100644
--- a/paramiko/ecdsakey.py
+++ b/paramiko/ecdsakey.py
@@ -129,13 +129,11 @@ class ECDSAKey (PKey):
@staticmethod
def generate(curve=curves.NIST256p, progress_func=None):
"""
- Generate a new private RSA key. This factory function can be used to
+ Generate a new private ECDSA key. This factory function can be used to
generate a new host key or authentication key.
- :param function progress_func:
- an optional function to call at key points in key generation (used
- by ``pyCrypto.PublicKey``).
- :returns: A new private key (`.RSAKey`) object
+ :param function progress_func: Not used for this type of key.
+ :returns: A new private key (`.ECDSAKey`) object
"""
signing_key = SigningKey.generate(curve)
key = ECDSAKey(vals=(signing_key, signing_key.get_verifying_key()))
diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py
index 84868875..38ac866b 100644
--- a/paramiko/hostkeys.py
+++ b/paramiko/hostkeys.py
@@ -35,6 +35,7 @@ from paramiko.dsskey import DSSKey
from paramiko.rsakey import RSAKey
from paramiko.util import get_logger, constant_time_bytes_eq
from paramiko.ecdsakey import ECDSAKey
+from paramiko.ssh_exception import SSHException
class HostKeys (MutableMapping):
@@ -92,11 +93,14 @@ class HostKeys (MutableMapping):
:raises IOError: if there was an error reading the file
"""
with open(filename, 'r') as f:
- for lineno, line in enumerate(f):
+ for lineno, line in enumerate(f, 1):
line = line.strip()
if (len(line) == 0) or (line[0] == '#'):
continue
- e = HostKeyEntry.from_line(line, lineno)
+ try:
+ e = HostKeyEntry.from_line(line, lineno)
+ except SSHException:
+ continue
if e is not None:
_hostnames = e.hostnames
for h in _hostnames:
diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py
index cb548f33..c980b690 100644
--- a/paramiko/kex_gex.py
+++ b/paramiko/kex_gex.py
@@ -23,7 +23,7 @@ client side, and a **lot** more on the server side.
"""
import os
-from hashlib import sha1
+from hashlib import sha1, sha256
from paramiko import util
from paramiko.common import DEBUG
@@ -44,6 +44,7 @@ class KexGex (object):
min_bits = 1024
max_bits = 8192
preferred_bits = 2048
+ hash_algo = sha1
def __init__(self, transport):
self.transport = transport
@@ -87,7 +88,7 @@ class KexGex (object):
return self._parse_kexdh_gex_reply(m)
elif ptype == _MSG_KEXDH_GEX_REQUEST_OLD:
return self._parse_kexdh_gex_request_old(m)
- raise SSHException('KexGex asked to handle packet type %d' % ptype)
+ raise SSHException('KexGex %s asked to handle packet type %d' % self.name, ptype)
### internals...
@@ -204,7 +205,7 @@ class KexGex (object):
hm.add_mpint(self.e)
hm.add_mpint(self.f)
hm.add_mpint(K)
- H = sha1(hm.asbytes()).digest()
+ 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)
@@ -239,6 +240,10 @@ class KexGex (object):
hm.add_mpint(self.e)
hm.add_mpint(self.f)
hm.add_mpint(K)
- self.transport._set_K_H(K, sha1(hm.asbytes()).digest())
+ self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest())
self.transport._verify_key(host_key, sig)
self.transport._activate_outbound()
+
+class KexGexSHA256(KexGex):
+ name = 'diffie-hellman-group-exchange-sha256'
+ hash_algo = sha256
diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py
index a88f00d2..9eee066c 100644
--- a/paramiko/kex_group1.py
+++ b/paramiko/kex_group1.py
@@ -45,6 +45,7 @@ class KexGroup1(object):
G = 2
name = 'diffie-hellman-group1-sha1'
+ hash_algo = sha1
def __init__(self, transport):
self.transport = transport
diff --git a/paramiko/kex_group14.py b/paramiko/kex_group14.py
index a914aeaf..9f7dd216 100644
--- a/paramiko/kex_group14.py
+++ b/paramiko/kex_group14.py
@@ -22,6 +22,7 @@ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of
"""
from paramiko.kex_group1 import KexGroup1
+from hashlib import sha1
class KexGroup14(KexGroup1):
@@ -31,3 +32,4 @@ class KexGroup14(KexGroup1):
G = 2
name = 'diffie-hellman-group14-sha1'
+ hash_algo = sha1
diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py
index d026807c..69969f8a 100644
--- a/paramiko/kex_gss.py
+++ b/paramiko/kex_gss.py
@@ -37,6 +37,7 @@ This module provides GSS-API / SSPI Key Exchange as defined in :rfc:`4462`.
.. versionadded:: 1.15
"""
+import os
from hashlib import sha1
from paramiko.common import *
@@ -130,7 +131,7 @@ class KexGSSGroup1(object):
larger than q (but this is a tiny tiny subset of potential x).
"""
while 1:
- x_bytes = self.transport.rng.read(128)
+ x_bytes = os.urandom(128)
x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:]
if (x_bytes[:8] != self.b7fffffffffffffff) and \
(x_bytes[:8] != self.b0000000000000000):
@@ -366,7 +367,7 @@ class KexGSSGex(object):
qhbyte <<= 1
qmask >>= 1
while True:
- x_bytes = self.transport.rng.read(byte_count)
+ x_bytes = os.urandom(byte_count)
x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:]
x = util.inflate_long(x_bytes, 1)
if (x > 1) and (x < q):
diff --git a/paramiko/message.py b/paramiko/message.py
index b893e76d..bf4c6b95 100644
--- a/paramiko/message.py
+++ b/paramiko/message.py
@@ -129,7 +129,7 @@ class Message (object):
b = self.get_bytes(1)
return b != zero_byte
- def get_int(self):
+ def get_adaptive_int(self):
"""
Fetch an int from the stream.
@@ -141,20 +141,7 @@ class Message (object):
byte += self.get_bytes(3)
return struct.unpack('>I', byte)[0]
- def get_size(self):
- """
- Fetch an int from the stream.
-
- @return: a 32-bit unsigned integer.
- @rtype: int
- """
- byte = self.get_bytes(1)
- if byte == max_byte:
- return util.inflate_long(self.get_binary())
- byte += self.get_bytes(3)
- return struct.unpack('>I', byte)[0]
-
- def get_size(self):
+ def get_int(self):
"""
Fetch an int from the stream.
@@ -185,7 +172,7 @@ class Message (object):
contain unprintable characters. (It's not unheard of for a string to
contain another byte-stream message.)
"""
- return self.get_bytes(self.get_size())
+ return self.get_bytes(self.get_int())
def get_text(self):
"""
@@ -196,7 +183,7 @@ class Message (object):
@return: a string.
@rtype: string
"""
- return u(self.get_bytes(self.get_size()))
+ return u(self.get_bytes(self.get_int()))
#return self.get_bytes(self.get_size())
def get_binary(self):
@@ -208,7 +195,7 @@ class Message (object):
@return: a string.
@rtype: string
"""
- return self.get_bytes(self.get_size())
+ return self.get_bytes(self.get_int())
def get_list(self):
"""
@@ -248,7 +235,7 @@ class Message (object):
self.packet.write(zero_byte)
return self
- def add_size(self, n):
+ def add_int(self, n):
"""
Add an integer to the stream.
@@ -257,7 +244,7 @@ class Message (object):
self.packet.write(struct.pack('>I', n))
return self
- def add_int(self, n):
+ def add_adaptive_int(self, n):
"""
Add an integer to the stream.
@@ -270,20 +257,6 @@ class Message (object):
self.packet.write(struct.pack('>I', n))
return self
- def add_int(self, n):
- """
- Add an integer to the stream.
-
- @param n: integer to add
- @type n: int
- """
- if n >= Message.big_int:
- self.packet.write(max_byte)
- self.add_string(util.deflate_long(n))
- else:
- self.packet.write(struct.pack('>I', n))
- return self
-
def add_int64(self, n):
"""
Add a 64-bit int to the stream.
@@ -310,7 +283,7 @@ class Message (object):
:param str s: string to add
"""
s = asbytes(s)
- self.add_size(len(s))
+ self.add_int(len(s))
self.packet.write(s)
return self
@@ -329,7 +302,7 @@ class Message (object):
if type(i) is bool:
return self.add_boolean(i)
elif isinstance(i, integer_types):
- return self.add_int(i)
+ return self.add_adaptive_int(i)
elif type(i) is list:
return self.add_list(i)
else:
diff --git a/paramiko/packet.py b/paramiko/packet.py
index f516ff9b..89a514d1 100644
--- a/paramiko/packet.py
+++ b/paramiko/packet.py
@@ -99,6 +99,10 @@ class Packetizer (object):
self.__keepalive_last = time.time()
self.__keepalive_callback = None
+ self.__timer = None
+ self.__handshake_complete = False
+ self.__timer_expired = False
+
def set_log(self, log):
"""
Set the Python log object to use for logging.
@@ -182,6 +186,46 @@ class Packetizer (object):
self.__keepalive_callback = callback
self.__keepalive_last = time.time()
+ def read_timer(self):
+ self.__timer_expired = True
+
+ def start_handshake(self, timeout):
+ """
+ Tells `Packetizer` that the handshake process started.
+ Starts a book keeping timer that can signal a timeout in the
+ handshake process.
+
+ :param float timeout: amount of seconds to wait before timing out
+ """
+ if not self.__timer:
+ self.__timer = threading.Timer(float(timeout), self.read_timer)
+ self.__timer.start()
+
+ def handshake_timed_out(self):
+ """
+ Checks if the handshake has timed out.
+
+ If `start_handshake` wasn't called before the call to this function,
+ the return value will always be `False`. If the handshake completed
+ before a timeout was reached, the return value will be `False`
+
+ :return: handshake time out status, as a `bool`
+ """
+ if not self.__timer:
+ return False
+ if self.__handshake_complete:
+ return False
+ return self.__timer_expired
+
+ def complete_handshake(self):
+ """
+ Tells `Packetizer` that the handshake has completed.
+ """
+ if self.__timer:
+ self.__timer.cancel()
+ self.__timer_expired = False
+ self.__handshake_complete = True
+
def read_all(self, n, check_rekey=False):
"""
Read as close to N bytes as possible, blocking as long as necessary.
@@ -200,6 +244,8 @@ class Packetizer (object):
n -= len(out)
while n > 0:
got_timeout = False
+ if self.handshake_timed_out():
+ raise EOFError()
try:
x = self.__socket.recv(n)
if len(x) == 0:
@@ -344,7 +390,8 @@ class Packetizer (object):
if self.__dump_packets:
self._log(DEBUG, util.format_binary(header, 'IN: '))
packet_size = struct.unpack('>I', header[:4])[0]
- # leftover contains decrypted bytes from the first block (after the length field)
+ # leftover contains decrypted bytes from the first block (after the
+ # length field)
leftover = header[4:]
if (packet_size - len(leftover)) % self.__block_size_in != 0:
raise SSHException('Invalid packet blocking')
diff --git a/paramiko/pkey.py b/paramiko/pkey.py
index 1b4af010..e95d60ba 100644
--- a/paramiko/pkey.py
+++ b/paramiko/pkey.py
@@ -171,8 +171,9 @@ class PKey (object):
is useless on the abstract PKey class.
:param str filename: name of the file to read
- :param str password: an optional password to use to decrypt the key file,
- if it's encrypted
+ :param str password:
+ an optional password to use to decrypt the key file, if it's
+ encrypted
:return: a new `.PKey` based on the given private key
:raises IOError: if there was an error reading the file
@@ -187,18 +188,18 @@ class PKey (object):
def from_private_key(cls, file_obj, password=None):
"""
Create a key object by reading a private key from a file (or file-like)
- object. If the private key is encrypted and ``password`` is not ``None``,
- the given password will be used to decrypt the key (otherwise
+ object. If the private key is encrypted and ``password`` is not
+ ``None``, the given password will be used to decrypt the key (otherwise
`.PasswordRequiredException` is thrown).
- :param file file_obj: the file to read from
+ :param file_obj: the file-like object to read from
:param str password:
an optional password to use to decrypt the key, if it's encrypted
:return: a new `.PKey` based on the given private key
:raises IOError: if there was an error reading the key
- :raises PasswordRequiredException: if the private key file is encrypted,
- and ``password`` is ``None``
+ :raises PasswordRequiredException:
+ if the private key file is encrypted, and ``password`` is ``None``
:raises SSHException: if the key file is invalid
"""
key = cls(file_obj=file_obj, password=password)
@@ -223,7 +224,7 @@ class PKey (object):
Write private key contents into a file (or file-like) object. If the
password is not ``None``, the key is encrypted before writing.
- :param file file_obj: the file object to write into
+ :param file_obj: the file-like object to write into
:param str password: an optional password to use to encrypt the key
:raises IOError: if there was an error writing to the file
@@ -273,7 +274,7 @@ class PKey (object):
start += 1
# find end
end = start
- while (lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----') and (end < len(lines)):
+ while end < len(lines) and lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----':
end += 1
# if we trudged to the end of the file, just try to cope.
try:
@@ -309,8 +310,9 @@ class PKey (object):
a trivially-encoded format (base64) which is completely insecure. If
a password is given, DES-EDE3-CBC is used.
- :param str tag: ``"RSA"`` or ``"DSA"``, the tag used to mark the data block.
- :param file filename: name of the file to write.
+ :param str tag:
+ ``"RSA"`` or ``"DSA"``, the tag used to mark the data block.
+ :param filename: name of the file to write.
:param str data: data blob that makes up the private key.
:param str password: an optional password to use to encrypt the file.
diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py
index 57c096b2..6fafc31d 100644
--- a/paramiko/py3compat.py
+++ b/paramiko/py3compat.py
@@ -3,7 +3,7 @@ import base64
__all__ = ['PY2', 'string_types', 'integer_types', 'text_type', 'bytes_types', 'bytes', 'long', 'input',
'decodebytes', 'encodebytes', 'bytestring', 'byte_ord', 'byte_chr', 'byte_mask',
- 'b', 'u', 'b2s', 'StringIO', 'BytesIO', 'is_callable', 'MAXSIZE', 'next']
+ 'b', 'u', 'b2s', 'StringIO', 'BytesIO', 'is_callable', 'MAXSIZE', 'next', 'builtins']
PY2 = sys.version_info[0] < 3
@@ -18,6 +18,8 @@ if PY2:
decodebytes = base64.decodestring
encodebytes = base64.encodestring
+ import __builtin__ as builtins
+
def bytestring(s): # NOQA
if isinstance(s, unicode):
@@ -102,6 +104,7 @@ if PY2:
else:
import collections
import struct
+ import builtins
string_types = str
text_type = str
bytes = bytes
diff --git a/paramiko/server.py b/paramiko/server.py
index bf5039a2..f79a1748 100644
--- a/paramiko/server.py
+++ b/paramiko/server.py
@@ -22,7 +22,7 @@
import threading
from paramiko import util
-from paramiko.common import DEBUG, ERROR, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, AUTH_FAILED
+from paramiko.common import DEBUG, ERROR, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, AUTH_FAILED, AUTH_SUCCESSFUL
from paramiko.py3compat import string_types
diff --git a/paramiko/sftp_attr.py b/paramiko/sftp_attr.py
index cf48f654..0eaca30b 100644
--- a/paramiko/sftp_attr.py
+++ b/paramiko/sftp_attr.py
@@ -209,12 +209,15 @@ class SFTPAttributes (object):
# not all servers support uid/gid
uid = self.st_uid
gid = self.st_gid
+ size = self.st_size
if uid is None:
uid = 0
if gid is None:
gid = 0
+ if size is None:
+ size = 0
- return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, self.st_size, datestr, filename)
+ return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, size, datestr, filename)
def asbytes(self):
return b(str(self))
diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py
index 6d48e692..57225558 100644
--- a/paramiko/sftp_client.py
+++ b/paramiko/sftp_client.py
@@ -600,7 +600,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
The SFTP operations use pipelining for speed.
- :param file fl: opened file or file-like object to copy
+ :param fl: opened file or file-like object to copy
:param str remotepath: the destination path on the SFTP server
:param int file_size:
optional size parameter passed to callback. If none is specified,
@@ -686,9 +686,10 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
.. versionadded:: 1.10
"""
+ file_size = self.stat(remotepath).st_size
with self.open(remotepath, 'rb') as fr:
- file_size = self.stat(remotepath).st_size
- fr.prefetch()
+ fr.prefetch(file_size)
+
size = 0
while True:
data = fr.read(32768)
@@ -716,7 +717,6 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
.. versionchanged:: 1.7.4
Added the ``callback`` param
"""
- file_size = self.stat(remotepath).st_size
with open(localpath, 'wb') as fl:
size = self.getfo(remotepath, fl, callback)
s = os.stat(localpath)
diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py
index 1d070ee0..3b584dbe 100644
--- a/paramiko/sftp_file.py
+++ b/paramiko/sftp_file.py
@@ -389,7 +389,7 @@ class SFTPFile (BufferedFile):
"""
self.pipelined = pipelined
- def prefetch(self):
+ def prefetch(self, file_size):
"""
Pre-fetch the remaining contents of this file in anticipation of future
`.read` calls. If reading the entire file, pre-fetching can
@@ -403,12 +403,11 @@ class SFTPFile (BufferedFile):
.. versionadded:: 1.5.1
"""
- size = self.stat().st_size
# queue up async reads for the rest of the file
chunks = []
n = self._realpos
- while n < size:
- chunk = min(self.MAX_REQUEST_SIZE, size - n)
+ while n < file_size:
+ chunk = min(self.MAX_REQUEST_SIZE, file_size - n)
chunks.append((n, chunk))
n += chunk
if len(chunks) > 0:
diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py
index d053974a..016a411e 100644
--- a/paramiko/ssh_exception.py
+++ b/paramiko/ssh_exception.py
@@ -107,7 +107,11 @@ class BadHostKeyException (SSHException):
.. versionadded:: 1.6
"""
def __init__(self, hostname, got_key, expected_key):
- SSHException.__init__(self, 'Host key for server %s does not match!' % hostname)
+ SSHException.__init__(self,
+ 'Host key for server %s does not match : got %s expected %s' % (
+ hostname,
+ got_key.get_base64(),
+ expected_key.get_base64()))
self.hostname = hostname
self.key = got_key
self.expected_key = expected_key
@@ -152,6 +156,8 @@ class NoValidConnectionsError(socket.error):
It is implied/assumed that all the errors given to a single instance of
this class are from connecting to the same hostname + port (and thus that
the differences are in the resolution of the hostname - e.g. IPv4 vs v6).
+
+ .. versionadded:: 1.16
"""
def __init__(self, errors):
"""
diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py
index aa28e2ec..e9b13a66 100644
--- a/paramiko/ssh_gss.py
+++ b/paramiko/ssh_gss.py
@@ -360,8 +360,8 @@ class _SSH_GSSAPI(_SSH_GSSAuth):
:param str mic_token: The MIC token received from the client
:param str session_id: The SSH session ID
:param str username: The name of the user who attempts to login
- :return: 0 if the MIC check was successful and 1 if it fails
- :rtype: int
+ :return: None if the MIC check was successful
+ :raises gssapi.GSSException: if the MIC check failed
"""
self._session_id = session_id
self._username = username
@@ -371,11 +371,7 @@ class _SSH_GSSAPI(_SSH_GSSAuth):
self._username,
self._service,
self._auth_method)
- try:
- self._gss_srv_ctxt.verify_mic(mic_field,
- mic_token)
- except gssapi.BadSignature:
- raise Exception("GSS-API MIC check failed.")
+ self._gss_srv_ctxt.verify_mic(mic_field, mic_token)
else:
# for key exchange with gssapi-keyex
# client mode
@@ -534,31 +530,26 @@ class _SSH_SSPI(_SSH_GSSAuth):
:param str mic_token: The MIC token received from the client
:param str session_id: The SSH session ID
:param str username: The name of the user who attempts to login
- :return: 0 if the MIC check was successful
- :rtype: int
+ :return: None if the MIC check was successful
+ :raises sspi.error: if the MIC check failed
"""
self._session_id = session_id
self._username = username
- mic_status = 1
if username is not None:
# server mode
mic_field = self._ssh_build_mic(self._session_id,
self._username,
self._service,
self._auth_method)
- mic_status = self._gss_srv_ctxt.verify(mic_field,
- mic_token)
+ # Verifies data and its signature. If verification fails, an
+ # sspi.error will be raised.
+ self._gss_srv_ctxt.verify(mic_field, mic_token)
else:
# for key exchange with gssapi-keyex
# client mode
- mic_status = self._gss_ctxt.verify(self._session_id,
- mic_token)
- """
- The SSPI method C{verify} has no return value, so if no SSPI error
- is returned, set C{mic_status} to 0.
- """
- mic_status = 0
- return mic_status
+ # Verifies data and its signature. If verification fails, an
+ # sspi.error will be raised.
+ self._gss_ctxt.verify(self._session_id, mic_token)
@property
def credentials_delegated(self):
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 36da3043..18fb103b 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -20,13 +20,14 @@
Core protocol implementation
"""
+from __future__ import print_function
import os
import socket
import sys
import threading
import time
import weakref
-from hashlib import md5, sha1
+from hashlib import md5, sha1, sha256, sha512
import paramiko
from paramiko import util
@@ -47,7 +48,7 @@ from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \
MAX_WINDOW_SIZE, DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE
from paramiko.compress import ZlibCompressor, ZlibDecompressor
from paramiko.dsskey import DSSKey
-from paramiko.kex_gex import KexGex
+from paramiko.kex_gex import KexGex, KexGexSHA256
from paramiko.kex_group1 import KexGroup1
from paramiko.kex_group14 import KexGroup14
from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14, NullHostKey
@@ -94,27 +95,109 @@ class Transport (threading.Thread, ClosingContextManager):
_PROTO_ID = '2.0'
_CLIENT_ID = 'paramiko_%s' % paramiko.__version__
- _preferred_ciphers = ('aes128-ctr', 'aes256-ctr', 'aes128-cbc', 'blowfish-cbc',
- 'aes256-cbc', '3des-cbc', 'arcfour128', 'arcfour256')
- _preferred_macs = ('hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96')
- _preferred_keys = ('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256')
- _preferred_kex = ( 'diffie-hellman-group14-sha1', 'diffie-hellman-group-exchange-sha1' , 'diffie-hellman-group1-sha1')
+ # These tuples of algorithm identifiers are in preference order; do not
+ # reorder without reason!
+ _preferred_ciphers = (
+ 'aes128-ctr',
+ 'aes192-ctr',
+ 'aes256-ctr',
+ 'aes128-cbc',
+ 'blowfish-cbc',
+ 'aes192-cbc',
+ 'aes256-cbc',
+ '3des-cbc',
+ 'arcfour128',
+ 'arcfour256',
+ )
+ _preferred_macs = (
+ 'hmac-sha2-256',
+ 'hmac-sha2-512',
+ 'hmac-md5',
+ 'hmac-sha1-96',
+ 'hmac-md5-96',
+ 'hmac-sha1',
+ )
+ _preferred_keys = (
+ 'ssh-rsa',
+ 'ssh-dss',
+ 'ecdsa-sha2-nistp256',
+ )
+ _preferred_kex = (
+ 'diffie-hellman-group1-sha1',
+ 'diffie-hellman-group14-sha1',
+ 'diffie-hellman-group-exchange-sha1',
+ 'diffie-hellman-group-exchange-sha256',
+ )
_preferred_compression = ('none',)
_cipher_info = {
- 'aes128-ctr': {'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 16},
- 'aes256-ctr': {'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 32},
- 'blowfish-cbc': {'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16},
- 'aes128-cbc': {'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 16},
- 'aes256-cbc': {'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 32},
- '3des-cbc': {'class': DES3, 'mode': DES3.MODE_CBC, 'block-size': 8, 'key-size': 24},
- 'arcfour128': {'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 16},
- 'arcfour256': {'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 32},
+ 'aes128-ctr': {
+ 'class': AES,
+ 'mode': AES.MODE_CTR,
+ 'block-size': 16,
+ 'key-size': 16
+ },
+ 'aes192-ctr': {
+ 'class': AES,
+ 'mode': AES.MODE_CTR,
+ 'block-size': 16,
+ 'key-size': 24
+ },
+ 'aes256-ctr': {
+ 'class': AES,
+ 'mode': AES.MODE_CTR,
+ 'block-size': 16,
+ 'key-size': 32
+ },
+ 'blowfish-cbc': {
+ 'class': Blowfish,
+ 'mode': Blowfish.MODE_CBC,
+ 'block-size': 8,
+ 'key-size': 16
+ },
+ 'aes128-cbc': {
+ 'class': AES,
+ 'mode': AES.MODE_CBC,
+ 'block-size': 16,
+ 'key-size': 16
+ },
+ 'aes192-cbc': {
+ 'class': AES,
+ 'mode': AES.MODE_CBC,
+ 'block-size': 16,
+ 'key-size': 24
+ },
+ 'aes256-cbc': {
+ 'class': AES,
+ 'mode': AES.MODE_CBC,
+ 'block-size': 16,
+ 'key-size': 32
+ },
+ '3des-cbc': {
+ 'class': DES3,
+ 'mode': DES3.MODE_CBC,
+ 'block-size': 8,
+ 'key-size': 24
+ },
+ 'arcfour128': {
+ 'class': ARC4,
+ 'mode': None,
+ 'block-size': 8,
+ 'key-size': 16
+ },
+ 'arcfour256': {
+ 'class': ARC4,
+ 'mode': None,
+ 'block-size': 8,
+ 'key-size': 32
+ },
}
_mac_info = {
'hmac-sha1': {'class': sha1, 'size': 20},
'hmac-sha1-96': {'class': sha1, 'size': 12},
+ 'hmac-sha2-256': {'class': sha256, 'size': 32},
+ 'hmac-sha2-512': {'class': sha512, 'size': 64},
'hmac-md5': {'class': md5, 'size': 16},
'hmac-md5-96': {'class': md5, 'size': 12},
}
@@ -129,6 +212,7 @@ class Transport (threading.Thread, ClosingContextManager):
'diffie-hellman-group1-sha1': KexGroup1,
'diffie-hellman-group14-sha1': KexGroup14,
'diffie-hellman-group-exchange-sha1': KexGex,
+ 'diffie-hellman-group-exchange-sha256': KexGexSHA256,
'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup1,
'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup14,
'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGex
@@ -295,6 +379,8 @@ class Transport (threading.Thread, ClosingContextManager):
self.global_response = None # response Message from an arbitrary global request
self.completion_event = None # user-defined event callbacks
self.banner_timeout = 15 # how long (seconds) to wait for the SSH banner
+ self.handshake_timeout = 15 # how long (seconds) to wait for the handshake to finish after SSH banner sent.
+
# server mode:
self.server_mode = False
@@ -587,7 +673,7 @@ class Transport (threading.Thread, ClosingContextManager):
"""
return self.active
- def open_session(self, window_size=None, max_packet_size=None):
+ def open_session(self, window_size=None, max_packet_size=None, timeout=None):
"""
Request a new channel to the server, of type ``"session"``. This is
just an alias for calling `open_channel` with an argument of
@@ -612,7 +698,8 @@ class Transport (threading.Thread, ClosingContextManager):
"""
return self.open_channel('session',
window_size=window_size,
- max_packet_size=max_packet_size)
+ max_packet_size=max_packet_size,
+ timeout=timeout)
def open_x11_channel(self, src_addr=None):
"""
@@ -659,7 +746,8 @@ class Transport (threading.Thread, ClosingContextManager):
dest_addr=None,
src_addr=None,
window_size=None,
- max_packet_size=None):
+ max_packet_size=None,
+ timeout=None):
"""
Request a new channel to the server. `Channels <.Channel>` are
socket-like objects used for the actual transfer of data across the
@@ -683,17 +771,20 @@ class Transport (threading.Thread, ClosingContextManager):
optional window size for this session.
:param int max_packet_size:
optional max packet size for this session.
+ :param float timeout:
+ optional timeout opening a channel, default 3600s (1h)
:return: a new `.Channel` on success
- :raises SSHException: if the request is rejected or the session ends
- prematurely
+ :raises SSHException: if the request is rejected, the session ends
+ prematurely or there is a timeout openning a channel
.. versionchanged:: 1.15
Added the ``window_size`` and ``max_packet_size`` arguments.
"""
if not self.active:
raise SSHException('SSH session not active')
+ timeout = 3600 if timeout is None else timeout
self.lock.acquire()
try:
window_size = self._sanitize_window_size(window_size)
@@ -722,6 +813,7 @@ class Transport (threading.Thread, ClosingContextManager):
finally:
self.lock.release()
self._send_user_message(m)
+ start_ts = time.time()
while True:
event.wait(0.1)
if not self.active:
@@ -731,6 +823,8 @@ class Transport (threading.Thread, ClosingContextManager):
raise e
if event.is_set():
break
+ elif start_ts + timeout < time.time():
+ raise SSHException('Timeout openning channel.')
chan = self._channels.get(chanid)
if chan is not None:
return chan
@@ -1284,6 +1378,27 @@ class Transport (threading.Thread, ClosingContextManager):
self.auth_handler.auth_interactive(username, handler, my_event, submethods)
return self.auth_handler.wait_for_response(my_event)
+ def auth_interactive_dumb(self, username, handler=None, submethods=''):
+ """
+ Autenticate to the server interactively but dumber.
+ Just print the prompt and / or instructions to stdout and send back
+ the response. This is good for situations where partial auth is
+ achieved by key and then the user has to enter a 2fac token.
+ """
+
+ if not handler:
+ def handler(title, instructions, prompt_list):
+ answers = []
+ if title:
+ print(title.strip())
+ if instructions:
+ print(instructions.strip())
+ for prompt,show_input in prompt_list:
+ print(prompt.strip(),end=' ')
+ answers.append(raw_input())
+ return answers
+ return self.auth_interactive(username, handler, submethods)
+
def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds):
"""
Authenticate to the Server using GSS-API / SSPI.
@@ -1497,13 +1612,23 @@ class Transport (threading.Thread, ClosingContextManager):
m.add_bytes(self.H)
m.add_byte(b(id))
m.add_bytes(self.session_id)
- out = sofar = sha1(m.asbytes()).digest()
+ # Fallback to SHA1 for kex engines that fail to specify a hex
+ # algorithm, or for e.g. transport tests that don't run kexinit.
+ hash_algo = getattr(self.kex_engine, 'hash_algo', None)
+ hash_select_msg = "kex engine %s specified hash_algo %r" % (self.kex_engine.__class__.__name__, hash_algo)
+ if hash_algo is None:
+ hash_algo = sha1
+ hash_select_msg += ", falling back to sha1"
+ if not hasattr(self, '_logged_hash_selection'):
+ self._log(DEBUG, hash_select_msg)
+ setattr(self, '_logged_hash_selection', True)
+ out = sofar = hash_algo(m.asbytes()).digest()
while len(out) < nbytes:
m = Message()
m.add_mpint(self.K)
m.add_bytes(self.H)
m.add_bytes(sofar)
- digest = sha1(m.asbytes()).digest()
+ digest = hash_algo(m.asbytes()).digest()
out += digest
sofar += digest
return out[:nbytes]
@@ -1581,7 +1706,16 @@ class Transport (threading.Thread, ClosingContextManager):
try:
try:
self.packetizer.write_all(b(self.local_version + '\r\n'))
+ self._log(DEBUG, 'Local version/idstring: %s' % self.local_version)
self._check_banner()
+ # The above is actually very much part of the handshake, but
+ # sometimes the banner can be read but the machine is not
+ # responding, for example when the remote ssh daemon is loaded
+ # in to memory but we can not read from the disk/spawn a new
+ # shell.
+ # Make sure we can specify a timeout for the initial handshake.
+ # Re-use the banner timeout for now.
+ self.packetizer.start_handshake(self.handshake_timeout)
self._send_kex_init()
self._expect_packet(MSG_KEXINIT)
@@ -1631,6 +1765,7 @@ class Transport (threading.Thread, ClosingContextManager):
msg.add_byte(cMSG_UNIMPLEMENTED)
msg.add_int(m.seqno)
self._send_message(msg)
+ self.packetizer.complete_handshake()
except SSHException as e:
self._log(ERROR, 'Exception: ' + str(e))
self._log(ERROR, util.tb_strings())
@@ -1679,6 +1814,18 @@ class Transport (threading.Thread, ClosingContextManager):
if self.sys.modules is not None:
raise
+
+ def _log_agreement(self, which, local, remote):
+ # 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 = "{0} agreed: ".format(which)
+ if local == remote:
+ msg += local
+ else:
+ msg += "local={0}, remote={1}".format(local, remote)
+ self._log(DEBUG, msg)
+
### protocol stages
def _negotiate_keys(self, m):
@@ -1716,6 +1863,7 @@ class Transport (threading.Thread, ClosingContextManager):
raise SSHException('Indecipherable protocol version "' + buf + '"')
# save this server version string for later
self.remote_version = buf
+ self._log(DEBUG, 'Remote version/idstring: %s' % buf)
# pull off any attached comment
comment = ''
i = buf.find(' ')
@@ -1744,10 +1892,12 @@ class Transport (threading.Thread, ClosingContextManager):
self.clear_to_send_lock.release()
self.in_kex = True
if self.server_mode:
- if (self._modulus_pack is None) and ('diffie-hellman-group-exchange-sha1' in self._preferred_kex):
+ mp_required_prefix = 'diffie-hellman-group-exchange-sha'
+ kex_mp = [k for k in self._preferred_kex 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
- pkex = list(self.get_security_options().kex)
- pkex.remove('diffie-hellman-group-exchange-sha1')
+ pkex = [k for k in self.get_security_options().kex
+ if not k.startswith(mp_required_prefix)]
self.get_security_options().kex = pkex
available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__,
self._preferred_keys))
@@ -1799,15 +1949,24 @@ class Transport (threading.Thread, ClosingContextManager):
' server lang:' + str(server_lang_list) +
' kex follows?' + str(kex_follows))
- # 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 supports.
+ # 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
+ # supports.
if self.server_mode:
- agreed_kex = list(filter(self._preferred_kex.__contains__, kex_algo_list))
+ agreed_kex = list(filter(
+ self._preferred_kex.__contains__,
+ kex_algo_list
+ ))
else:
- agreed_kex = list(filter(kex_algo_list.__contains__, self._preferred_kex))
+ agreed_kex = list(filter(
+ kex_algo_list.__contains__,
+ self._preferred_kex
+ ))
if len(agreed_kex) == 0:
raise SSHException('Incompatible ssh peer (no acceptable kex algorithm)')
self.kex_engine = self._kex_info[agreed_kex[0]](self)
+ self._log(DEBUG, "Kex agreed: %s" % agreed_kex[0])
if self.server_mode:
available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__,
@@ -1835,7 +1994,9 @@ class Transport (threading.Thread, ClosingContextManager):
raise SSHException('Incompatible ssh server (no acceptable ciphers)')
self.local_cipher = agreed_local_ciphers[0]
self.remote_cipher = agreed_remote_ciphers[0]
- self._log(DEBUG, 'Ciphers agreed: local=%s, remote=%s' % (self.local_cipher, self.remote_cipher))
+ self._log_agreement(
+ 'Cipher', local=self.local_cipher, remote=self.remote_cipher
+ )
if self.server_mode:
agreed_remote_macs = list(filter(self._preferred_macs.__contains__, client_mac_algo_list))
@@ -1847,6 +2008,9 @@ class Transport (threading.Thread, ClosingContextManager):
raise SSHException('Incompatible ssh server (no acceptable macs)')
self.local_mac = agreed_local_macs[0]
self.remote_mac = agreed_remote_macs[0]
+ self._log_agreement(
+ 'MAC', local=self.local_mac, remote=self.remote_mac
+ )
if self.server_mode:
agreed_remote_compression = list(filter(self._preferred_compression.__contains__, client_compress_algo_list))
@@ -1858,10 +2022,11 @@ class Transport (threading.Thread, ClosingContextManager):
raise SSHException('Incompatible ssh server (no acceptable compression) %r %r %r' % (agreed_local_compression, agreed_remote_compression, self._preferred_compression))
self.local_compression = agreed_local_compression[0]
self.remote_compression = agreed_remote_compression[0]
-
- self._log(DEBUG, 'using kex %s; server key type %s; cipher: local %s, remote %s; mac: local %s, remote %s; compression: local %s, remote %s' %
- (agreed_kex[0], self.host_key_type, self.local_cipher, self.remote_cipher, self.local_mac,
- self.remote_mac, self.local_compression, self.remote_compression))
+ self._log_agreement(
+ 'Compression',
+ local=self.local_compression,
+ remote=self.remote_compression
+ )
# save for computing hash later...
# now wait! openssh has a bug (and others might too) where there are
@@ -1882,8 +2047,8 @@ class Transport (threading.Thread, ClosingContextManager):
engine = self._get_cipher(self.remote_cipher, key_in, IV_in)
mac_size = self._mac_info[self.remote_mac]['size']
mac_engine = self._mac_info[self.remote_mac]['class']
- # initial mac keys are done in the hash's natural size (not the potentially truncated
- # transmission size)
+ # initial mac keys are done in the hash's natural size (not the
+ # potentially truncated transmission size)
if self.server_mode:
mac_key = self._compute_key('E', mac_engine().digest_size)
else:
@@ -1909,8 +2074,8 @@ class Transport (threading.Thread, ClosingContextManager):
engine = self._get_cipher(self.local_cipher, key_out, IV_out)
mac_size = self._mac_info[self.local_mac]['size']
mac_engine = self._mac_info[self.local_mac]['class']
- # initial mac keys are done in the hash's natural size (not the potentially truncated
- # transmission size)
+ # initial mac keys are done in the hash's natural size (not the
+ # potentially truncated transmission size)
if self.server_mode:
mac_key = self._compute_key('F', mac_engine().digest_size)
else:
diff --git a/paramiko/util.py b/paramiko/util.py
index d9a29d74..855e5757 100644
--- a/paramiko/util.py
+++ b/paramiko/util.py
@@ -118,7 +118,7 @@ def safe_string(s):
def bit_length(n):
try:
- return n.bitlength()
+ return n.bit_length()
except AttributeError:
norm = deflate_long(n, False)
hbyte = byte_ord(norm[0])