summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml11
-rw-r--r--README8
-rw-r--r--demos/demo_server.py42
-rw-r--r--[-rwxr-xr-x]demos/demo_sftp.py19
-rwxr-xr-xdemos/demo_simple.py22
-rw-r--r--dev-requirements.txt8
-rw-r--r--paramiko/__init__.py1
-rw-r--r--paramiko/_version.py2
-rw-r--r--paramiko/_winapi.py153
-rw-r--r--paramiko/agent.py30
-rw-r--r--paramiko/auth_handler.py203
-rw-r--r--paramiko/ber.py6
-rw-r--r--paramiko/buffered_pipe.py4
-rw-r--r--paramiko/channel.py245
-rw-r--r--paramiko/client.py204
-rw-r--r--paramiko/common.py39
-rw-r--r--paramiko/config.py84
-rw-r--r--paramiko/dsskey.py24
-rw-r--r--paramiko/ecdsakey.py37
-rw-r--r--paramiko/file.py4
-rw-r--r--paramiko/hostkeys.py27
-rw-r--r--paramiko/kex_gex.py20
-rw-r--r--paramiko/kex_group1.py32
-rw-r--r--paramiko/kex_group14.py35
-rw-r--r--paramiko/kex_gss.py607
-rw-r--r--paramiko/message.py18
-rw-r--r--paramiko/packet.py72
-rw-r--r--paramiko/pipe.py1
-rw-r--r--paramiko/pkey.py46
-rw-r--r--paramiko/primes.py30
-rw-r--r--paramiko/proxy.py5
-rw-r--r--paramiko/py3compat.py5
-rw-r--r--paramiko/rsakey.py22
-rw-r--r--paramiko/server.py76
-rw-r--r--paramiko/sftp_attr.py12
-rw-r--r--paramiko/sftp_client.py97
-rw-r--r--paramiko/sftp_file.py6
-rw-r--r--paramiko/sftp_handle.py5
-rw-r--r--paramiko/sftp_server.py22
-rw-r--r--paramiko/ssh_exception.py44
-rw-r--r--paramiko/ssh_gss.py578
-rw-r--r--paramiko/transport.py557
-rw-r--r--paramiko/util.py25
-rw-r--r--setup.py4
-rw-r--r--setup_helper.py62
-rw-r--r--sites/docs/api/agent.rst2
-rw-r--r--sites/docs/api/kex_gss.rst5
-rw-r--r--sites/docs/api/ssh_gss.rst14
-rw-r--r--sites/docs/index.rst2
-rw-r--r--sites/shared_conf.py1
-rw-r--r--sites/www/blog.py140
-rw-r--r--sites/www/blog.rst16
-rw-r--r--sites/www/blog/first-post.rst7
-rw-r--r--sites/www/blog/second-post.rst7
-rw-r--r--sites/www/changelog.rst163
-rw-r--r--sites/www/conf.py12
-rw-r--r--sites/www/contact.rst1
-rw-r--r--sites/www/contributing.rst19
-rw-r--r--sites/www/faq.rst26
-rw-r--r--sites/www/index.rst20
-rw-r--r--sites/www/installing.rst89
-rw-r--r--tasks.py37
-rwxr-xr-xtest.py45
-rw-r--r--tests/stub_sftp.py1
-rw-r--r--tests/test_auth.py4
-rw-r--r--tests/test_buffered_pipe.py10
-rw-r--r--tests/test_client.py194
-rw-r--r--tests/test_gssapi.py135
-rw-r--r--tests/test_hostkeys.py1
-rw-r--r--tests/test_kex.py137
-rw-r--r--tests/test_kex_gss.py131
-rw-r--r--tests/test_message.py8
-rw-r--r--tests/test_packetizer.py55
-rw-r--r--tests/test_pkey.py37
-rwxr-xr-xtests/test_sftp.py56
-rw-r--r--tests/test_ssh_gss.py124
-rw-r--r--tests/test_transport.py196
-rw-r--r--tests/test_util.py153
-rw-r--r--tests/util.py10
-rw-r--r--tox.ini4
81 files changed, 4392 insertions, 1025 deletions
diff --git a/.gitignore b/.gitignore
index e149bb8c..44b45974 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ dist/
paramiko.egg-info/
test.log
docs/
+demos/*.log
!sites/docs
_build
.coverage
diff --git a/.travis.yml b/.travis.yml
index 64f64e60..c2c20a60 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,8 +3,9 @@ sudo: false
python:
- "2.6"
- "2.7"
- - "3.2"
- "3.3"
+ - "3.4"
+ - "3.5"
install:
# Self-install for setup.py-driven deps
- pip install -e .
@@ -12,15 +13,13 @@ install:
- pip install coveralls # For coveralls.io specifically
- pip install -r dev-requirements.txt
script:
- # Main tests, with coverage!
- - invoke coverage
+ # Main tests, w/ coverage! (but skip coverage on 3.2, coverage.py dropped it)
+ - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && inv test --coverage || inv test"
# Ensure documentation & invoke pipeline run OK.
# Run 'docs' first since its objects.inv is referred to by 'www'.
# Also force warnings to be errors since most of them tend to be actual
# problems.
- # Finally, skip them under Python 3.2 due to sphinx shenanigans
- - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && invoke docs -o -W || true"
- - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && invoke www -o -W || true"
+ - invoke docs -o -W www -o -W
notifications:
irc:
channels: "irc.freenode.org#paramiko"
diff --git a/README b/README
index a645b140..57512604 100644
--- a/README
+++ b/README
@@ -72,6 +72,14 @@ Bugs & Support
Please file bug reports at https://github.com/paramiko/paramiko/. There is currently no mailing list but we plan to create a new one ASAP.
+Kerberos Support
+----------------
+
+Paramiko ships with optional Kerberos/GSSAPI support; for info on the extra
+dependencies for this, see the 'GSS-API' section on the 'Installation' page of
+our main website, http://paramiko.org .
+
+
Demo
----
diff --git a/demos/demo_server.py b/demos/demo_server.py
index bb35258b..c4af9b10 100644
--- a/demos/demo_server.py
+++ b/demos/demo_server.py
@@ -66,9 +66,42 @@ class Server (paramiko.ServerInterface):
if (username == 'robey') and (key == self.good_pub_key):
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
+
+ def check_auth_gssapi_with_mic(self, username,
+ gss_authenticated=paramiko.AUTH_FAILED,
+ cc_file=None):
+ """
+ .. note::
+ We are just checking in `AuthHandler` that the given user is a
+ valid krb5 principal! We don't check if the krb5 principal is
+ allowed to log in on the server, because there is no way to do that
+ in python. So if you develop your own SSH server with paramiko for
+ a certain platform like Linux, you should call ``krb5_kuserok()`` in
+ your local kerberos library to make sure that the krb5_principal
+ has an account on the server and is allowed to log in as a user.
+
+ .. seealso::
+ `krb5_kuserok() man page
+ <http://www.unix.com/man-page/all/3/krb5_kuserok/>`_
+ """
+ if gss_authenticated == paramiko.AUTH_SUCCESSFUL:
+ return paramiko.AUTH_SUCCESSFUL
+ return paramiko.AUTH_FAILED
+
+ def check_auth_gssapi_keyex(self, username,
+ gss_authenticated=paramiko.AUTH_FAILED,
+ cc_file=None):
+ if gss_authenticated == paramiko.AUTH_SUCCESSFUL:
+ return paramiko.AUTH_SUCCESSFUL
+ return paramiko.AUTH_FAILED
+
+ def enable_auth_gssapi(self):
+ UseGSSAPI = True
+ GSSAPICleanupCredentials = False
+ return UseGSSAPI
def get_allowed_auths(self, username):
- return 'password,publickey'
+ return 'gssapi-keyex,gssapi-with-mic,password,publickey'
def check_channel_shell_request(self, channel):
self.event.set()
@@ -79,6 +112,8 @@ class Server (paramiko.ServerInterface):
return True
+DoGSSAPIKeyExchange = True
+
# now connect
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -101,7 +136,8 @@ except Exception as e:
print('Got a connection!')
try:
- t = paramiko.Transport(client)
+ t = paramiko.Transport(client, gss_kex=DoGSSAPIKeyExchange)
+ t.set_gss_host(socket.getfqdn(""))
try:
t.load_server_moduli()
except:
@@ -123,7 +159,7 @@ try:
print('Authenticated!')
server.event.wait(10)
- if not server.event.isSet():
+ if not server.event.is_set():
print('*** Client never asked for a shell.')
sys.exit(1)
diff --git a/demos/demo_sftp.py b/demos/demo_sftp.py
index a34f2b19..2cb44701 100755..100644
--- a/demos/demo_sftp.py
+++ b/demos/demo_sftp.py
@@ -34,6 +34,11 @@ from paramiko.py3compat import input
# setup logging
paramiko.util.log_to_file('demo_sftp.log')
+# Paramiko client configuration
+UseGSSAPI = True # enable GSS-API / SSPI authentication
+DoGSSAPIKeyExchange = True
+Port = 22
+
# get hostname
username = ''
if len(sys.argv) > 1:
@@ -45,10 +50,10 @@ else:
if len(hostname) == 0:
print('*** Hostname required.')
sys.exit(1)
-port = 22
+
if hostname.find(':') >= 0:
hostname, portstr = hostname.split(':')
- port = int(portstr)
+ Port = int(portstr)
# get username
@@ -57,7 +62,10 @@ if username == '':
username = input('Username [%s]: ' % default_username)
if len(username) == 0:
username = default_username
-password = getpass.getpass('Password for %s@%s: ' % (username, hostname))
+if not UseGSSAPI:
+ password = getpass.getpass('Password for %s@%s: ' % (username, hostname))
+else:
+ password = None
# get host key, if we know one
@@ -81,8 +89,9 @@ if hostname in host_keys:
# now, connect and use paramiko Transport to negotiate SSH2 across the connection
try:
- t = paramiko.Transport((hostname, port))
- t.connect(username=username, password=password, hostkey=hostkey)
+ t = paramiko.Transport((hostname, Port))
+ t.connect(hostkey, username, password, gss_host=socket.getfqdn(hostname),
+ gss_auth=UseGSSAPI, gss_kex=DoGSSAPIKeyExchange)
sftp = paramiko.SFTPClient.from_transport(t)
# dirlist on remote host
diff --git a/demos/demo_simple.py b/demos/demo_simple.py
index ae631e43..3a17988c 100755
--- a/demos/demo_simple.py
+++ b/demos/demo_simple.py
@@ -36,6 +36,10 @@ except ImportError:
# setup logging
paramiko.util.log_to_file('demo_simple.log')
+# Paramiko client configuration
+UseGSSAPI = True # enable GSS-API / SSPI authentication
+DoGSSAPIKeyExchange = True
+port = 22
# get hostname
username = ''
@@ -48,7 +52,7 @@ else:
if len(hostname) == 0:
print('*** Hostname required.')
sys.exit(1)
-port = 22
+
if hostname.find(':') >= 0:
hostname, portstr = hostname.split(':')
port = int(portstr)
@@ -60,7 +64,8 @@ if username == '':
username = input('Username [%s]: ' % default_username)
if len(username) == 0:
username = default_username
-password = getpass.getpass('Password for %s@%s: ' % (username, hostname))
+if not UseGSSAPI or (not UseGSSAPI and not DoGSSAPIKeyExchange):
+ password = getpass.getpass('Password for %s@%s: ' % (username, hostname))
# now, connect and use paramiko Client to negotiate SSH2 across the connection
@@ -69,7 +74,18 @@ try:
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.WarningPolicy())
print('*** Connecting...')
- client.connect(hostname, port, username, password)
+ if not UseGSSAPI or (not UseGSSAPI and not DoGSSAPIKeyExchange):
+ client.connect(hostname, port, username, password)
+ else:
+ # SSPI works only with the FQDN of the target host
+ hostname = socket.getfqdn(hostname)
+ try:
+ client.connect(hostname, port, username, gss_auth=UseGSSAPI,
+ gss_kex=DoGSSAPIKeyExchange)
+ except Exception:
+ password = getpass.getpass('Password for %s@%s: ' % (username, hostname))
+ client.connect(hostname, port, username, password)
+
chan = client.invoke_shell()
print(repr(client.get_transport()))
print('*** Here we go!\n')
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 7a0ccbc5..90cfd477 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,9 +1,11 @@
# Older junk
tox>=1.4,<1.5
# For newer tasks like building Sphinx docs.
-invoke>=0.7.0,<0.8
-invocations>=0.5.0
+invoke>=0.11.1
+invocations>=0.11.0
sphinx>=1.1.3
alabaster>=0.6.1
releases>=0.5.2
-wheel==0.23.0
+semantic_version>=2.4,<2.5
+wheel==0.24
+twine==1.5
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index 65f6f8a2..9e2ba013 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -30,6 +30,7 @@ __license__ = "GNU Lesser General Public License (LGPL)"
from paramiko.transport import SecurityOptions, Transport
from paramiko.client import SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, WarningPolicy
from paramiko.auth_handler import AuthHandler
+from paramiko.ssh_gss import GSSAuth, GSS_AUTH_AVAILABLE
from paramiko.channel import Channel, ChannelFile
from paramiko.ssh_exception import SSHException, PasswordRequiredException, \
BadAuthenticationType, ChannelException, BadHostKeyException, \
diff --git a/paramiko/_version.py b/paramiko/_version.py
index 0402fcf2..e82b8667 100644
--- a/paramiko/_version.py
+++ b/paramiko/_version.py
@@ -1,2 +1,2 @@
-__version_info__ = (1, 13, 3)
+__version_info__ = (1, 16, 0)
__version__ = '.'.join(map(str, __version_info__))
diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py
index 0d55d291..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,9 +118,12 @@ 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 overrideden.
+ A memory map object which can have security attributes overridden.
"""
def __init__(self, name, length, security_attributes=None):
self.name = name
@@ -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_ = [
@@ -213,12 +298,17 @@ class SECURITY_ATTRIBUTES(ctypes.Structure):
super(SECURITY_ATTRIBUTES, self).__init__(*args, **kwargs)
self.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES)
- def _get_descriptor(self):
+ @property
+ def descriptor(self):
return self._descriptor
- def _set_descriptor(self, descriptor):
- self._descriptor = descriptor
- self.lpSecurityDescriptor = ctypes.addressof(descriptor)
- descriptor = property(_get_descriptor, _set_descriptor)
+
+ @descriptor.setter
+ def descriptor(self, value):
+ self._descriptor = value
+ self.lpSecurityDescriptor = ctypes.addressof(value)
+
+#########################
+# jaraco.windows.security
def GetTokenInformation(token, information_class):
"""
@@ -234,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 e77b7281..6a8e7fb4 100644
--- a/paramiko/agent.py
+++ b/paramiko/agent.py
@@ -32,7 +32,7 @@ from select import select
from paramiko.common import asbytes, io_sleep
from paramiko.py3compat import byte_chr
-from paramiko.ssh_exception import SSHException
+from paramiko.ssh_exception import SSHException, AuthenticationException
from paramiko.message import Message
from paramiko.pkey import PKey
from paramiko.util import retry_on_signal
@@ -43,6 +43,7 @@ cSSH2_AGENTC_SIGN_REQUEST = byte_chr(13)
SSH2_AGENT_SIGN_RESPONSE = 14
+
class AgentSSH(object):
def __init__(self):
self._conn = None
@@ -108,9 +109,12 @@ class AgentProxyThread(threading.Thread):
def run(self):
try:
(r, addr) = self.get_connection()
+ # Found that r should be either a socket from the socket library or None
self.__inr = r
- self.__addr = addr
+ self.__addr = addr # This should be an IP address as a string? or None
self._agent.connect()
+ if not isinstance(self._agent, int) and (self._agent._conn is None or not hasattr(self._agent._conn, 'fileno')):
+ raise AuthenticationException("Unable to connect to SSH agent")
self._communicate()
except:
#XXX Not sure what to do here ... raise or pass ?
@@ -286,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
@@ -365,7 +389,7 @@ class AgentKey(PKey):
def get_name(self):
return self.name
- def sign_ssh_data(self, rng, data):
+ def sign_ssh_data(self, data):
msg = Message()
msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST)
msg.add_string(self.blob)
diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py
index c00ad41c..ef4a8c7e 100644
--- a/paramiko/auth_handler.py
+++ b/paramiko/auth_handler.py
@@ -28,20 +28,27 @@ from paramiko.common import cMSG_SERVICE_REQUEST, cMSG_DISCONNECT, \
cMSG_USERAUTH_INFO_REQUEST, WARNING, AUTH_FAILED, cMSG_USERAUTH_PK_OK, \
cMSG_USERAUTH_INFO_RESPONSE, MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT, \
MSG_USERAUTH_REQUEST, MSG_USERAUTH_SUCCESS, MSG_USERAUTH_FAILURE, \
- MSG_USERAUTH_BANNER, MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE
+ MSG_USERAUTH_BANNER, MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE, \
+ cMSG_USERAUTH_GSSAPI_RESPONSE, cMSG_USERAUTH_GSSAPI_TOKEN, \
+ cMSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, cMSG_USERAUTH_GSSAPI_ERROR, \
+ 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_NAMES
from paramiko.message import Message
from paramiko.py3compat import bytestring
from paramiko.ssh_exception import SSHException, AuthenticationException, \
BadAuthenticationType, PartialAuthentication
from paramiko.server import InteractiveQuery
+from paramiko.ssh_gss import GSSAuth
class AuthHandler (object):
"""
Internal class to handle the mechanics of authentication.
"""
-
+
def __init__(self, transport):
self.transport = weakref.proxy(transport)
self.username = None
@@ -56,7 +63,10 @@ class AuthHandler (object):
# for server mode:
self.auth_username = None
self.auth_fail_count = 0
-
+ # for GSSAPI
+ self.gss_host = None
+ self.gss_deleg_creds = True
+
def is_authenticated(self):
return self.authenticated
@@ -97,7 +107,7 @@ class AuthHandler (object):
self._request_auth()
finally:
self.transport.lock.release()
-
+
def auth_interactive(self, username, handler, event, submethods=''):
"""
response_list = handler(title, instructions, prompt_list)
@@ -112,7 +122,29 @@ class AuthHandler (object):
self._request_auth()
finally:
self.transport.lock.release()
-
+
+ def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds, event):
+ self.transport.lock.acquire()
+ try:
+ self.auth_event = event
+ self.auth_method = 'gssapi-with-mic'
+ self.username = username
+ self.gss_host = gss_host
+ self.gss_deleg_creds = gss_deleg_creds
+ self._request_auth()
+ finally:
+ self.transport.lock.release()
+
+ def auth_gssapi_keyex(self, username, event):
+ self.transport.lock.acquire()
+ try:
+ self.auth_event = event
+ self.auth_method = 'gssapi-keyex'
+ self.username = username
+ self._request_auth()
+ finally:
+ self.transport.lock.release()
+
def abort(self):
if self.auth_event is not None:
self.auth_event.set()
@@ -163,7 +195,7 @@ class AuthHandler (object):
if (e is None) or issubclass(e.__class__, EOFError):
e = AuthenticationException('Authentication failed.')
raise e
- if event.isSet():
+ if event.is_set():
break
if not self.is_authenticated():
e = self.transport.get_exception()
@@ -206,11 +238,82 @@ class AuthHandler (object):
m.add_string(self.private_key.get_name())
m.add_string(self.private_key)
blob = self._get_session_blob(self.private_key, 'ssh-connection', self.username)
- sig = self.private_key.sign_ssh_data(self.transport.rng, blob)
+ sig = self.private_key.sign_ssh_data(blob)
m.add_string(sig)
elif self.auth_method == 'keyboard-interactive':
m.add_string('')
m.add_string(self.submethods)
+ elif self.auth_method == "gssapi-with-mic":
+ sshgss = GSSAuth(self.auth_method, self.gss_deleg_creds)
+ m.add_bytes(sshgss.ssh_gss_oids())
+ # send the supported GSSAPI OIDs to the server
+ self.transport._send_message(m)
+ ptype, m = self.transport.packetizer.read_message()
+ if ptype == MSG_USERAUTH_BANNER:
+ self._parse_userauth_banner(m)
+ ptype, m = self.transport.packetizer.read_message()
+ if ptype == MSG_USERAUTH_GSSAPI_RESPONSE:
+ # Read the mechanism selected by the server. We send just
+ # the Kerberos V5 OID, so the server can only respond with
+ # this OID.
+ mech = m.get_string()
+ m = Message()
+ m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
+ m.add_string(sshgss.ssh_init_sec_context(self.gss_host,
+ mech,
+ self.username,))
+ self.transport._send_message(m)
+ while True:
+ ptype, m = self.transport.packetizer.read_message()
+ if ptype == MSG_USERAUTH_GSSAPI_TOKEN:
+ srv_token = m.get_string()
+ next_token = sshgss.ssh_init_sec_context(self.gss_host,
+ mech,
+ self.username,
+ srv_token)
+ # After this step the GSSAPI should not return any
+ # token. If it does, we keep sending the token to
+ # the server until no more token is returned.
+ if next_token is None:
+ break
+ else:
+ m = Message()
+ m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
+ m.add_string(next_token)
+ self.transport.send_message(m)
+ else:
+ raise SSHException("Received Package: %s" % MSG_NAMES[ptype])
+ m = Message()
+ m.add_byte(cMSG_USERAUTH_GSSAPI_MIC)
+ # send the MIC to the server
+ m.add_string(sshgss.ssh_get_mic(self.transport.session_id))
+ elif ptype == MSG_USERAUTH_GSSAPI_ERRTOK:
+ # RFC 4462 says we are not required to implement GSS-API
+ # error messages.
+ # See RFC 4462 Section 3.8 in
+ # http://www.ietf.org/rfc/rfc4462.txt
+ raise SSHException("Server returned an error token")
+ elif ptype == MSG_USERAUTH_GSSAPI_ERROR:
+ maj_status = m.get_int()
+ min_status = m.get_int()
+ err_msg = m.get_string()
+ lang_tag = m.get_string() # we don't care!
+ raise SSHException("GSS-API Error:\nMajor Status: %s\n\
+ Minor Status: %s\ \nError Message:\
+ %s\n") % (str(maj_status),
+ str(min_status),
+ err_msg)
+ elif ptype == MSG_USERAUTH_FAILURE:
+ self._parse_userauth_failure(m)
+ return
+ else:
+ raise SSHException("Received Package: %s" % MSG_NAMES[ptype])
+ elif self.auth_method == 'gssapi-keyex' and\
+ self.transport.gss_kex_used:
+ kexgss = self.transport.kexgss_ctxt
+ kexgss.set_username(self.username)
+ mic_token = kexgss.ssh_get_mic(self.transport.session_id)
+ m.add_string(mic_token)
elif self.auth_method == 'none':
pass
else:
@@ -278,6 +381,8 @@ class AuthHandler (object):
self._disconnect_no_more_auth()
return
self.auth_username = username
+ # check if GSS-API authentication is enabled
+ gss_auth = self.transport.server_object.enable_auth_gssapi()
if method == 'none':
result = self.transport.server_object.check_auth_none(username)
@@ -343,6 +448,90 @@ class AuthHandler (object):
# make interactive query instead of response
self._interactive_query(result)
return
+ elif method == "gssapi-with-mic" and gss_auth:
+ sshgss = GSSAuth(method)
+ # Read the number of OID mechanisms supported by the client.
+ # OpenSSH sends just one OID. It's the Kerveros V5 OID and that's
+ # the only OID we support.
+ mechs = m.get_int()
+ # We can't accept more than one OID, so if the SSH client sends
+ # more than one, disconnect.
+ if mechs > 1:
+ self.transport._log(INFO,
+ 'Disconnect: Received more than one GSS-API OID mechanism')
+ self._disconnect_no_more_auth()
+ desired_mech = m.get_string()
+ mech_ok = sshgss.ssh_check_mech(desired_mech)
+ # if we don't support the mechanism, disconnect.
+ if not mech_ok:
+ self.transport._log(INFO,
+ 'Disconnect: Received an invalid GSS-API OID mechanism')
+ self._disconnect_no_more_auth()
+ # send the Kerberos V5 GSSAPI OID to the client
+ supported_mech = sshgss.ssh_gss_oids("server")
+ # RFC 4462 says we are not required to implement GSS-API error
+ # messages. See section 3.8 in http://www.ietf.org/rfc/rfc4462.txt
+ while True:
+ m = Message()
+ m.add_byte(cMSG_USERAUTH_GSSAPI_RESPONSE)
+ m.add_bytes(supported_mech)
+ self.transport._send_message(m)
+ ptype, m = self.transport.packetizer.read_message()
+ if ptype == MSG_USERAUTH_GSSAPI_TOKEN:
+ client_token = m.get_string()
+ # use the client token as input to establish a secure
+ # context.
+ try:
+ token = sshgss.ssh_accept_sec_context(self.gss_host,
+ client_token,
+ username)
+ except Exception:
+ result = AUTH_FAILED
+ self._send_auth_result(username, method, result)
+ raise
+ if token is not None:
+ m = Message()
+ m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
+ m.add_string(token)
+ self.transport._send_message(m)
+ else:
+ raise SSHException("Client asked to handle paket %s"
+ %MSG_NAMES[ptype])
+ # check MIC
+ ptype, m = self.transport.packetizer.read_message()
+ if ptype == MSG_USERAUTH_GSSAPI_MIC:
+ break
+ mic_token = m.get_string()
+ try:
+ sshgss.ssh_check_mic(mic_token,
+ self.transport.session_id,
+ username)
+ except Exception:
+ result = AUTH_FAILED
+ self._send_auth_result(username, method, result)
+ raise
+ # 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
+ if sshgss is None:
+ # If there is no valid context, we reject the authentication
+ result = AUTH_FAILED
+ self._send_auth_result(username, method, result)
+ try:
+ sshgss.ssh_check_mic(mic_token,
+ self.transport.session_id,
+ self.auth_username)
+ except Exception:
+ result = AUTH_FAILED
+ self._send_auth_result(username, method, result)
+ raise
+ 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/ber.py b/paramiko/ber.py
index 05152303..a388df07 100644
--- a/paramiko/ber.py
+++ b/paramiko/ber.py
@@ -45,7 +45,7 @@ class BER(object):
def decode(self):
return self.decode_next()
-
+
def decode_next(self):
if self.idx >= len(self.content):
return None
@@ -89,6 +89,7 @@ class BER(object):
# 1: boolean (00 false, otherwise true)
raise BERException('Unknown ber encoding type %d (robey is lazy)' % ident)
+ @staticmethod
def decode_sequence(data):
out = []
ber = BER(data)
@@ -98,7 +99,6 @@ class BER(object):
break
out.append(x)
return out
- decode_sequence = staticmethod(decode_sequence)
def encode_tlv(self, ident, val):
# no need to support ident > 31 here
@@ -125,9 +125,9 @@ class BER(object):
else:
raise BERException('Unknown type for encoding: %s' % repr(type(x)))
+ @staticmethod
def encode_sequence(data):
ber = BER()
for item in data:
ber.encode(item)
return ber.asbytes()
- encode_sequence = staticmethod(encode_sequence)
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 0acd85ef..057b417b 100644
--- a/paramiko/channel.py
+++ b/paramiko/channel.py
@@ -21,9 +21,11 @@ Abstraction for an SSH2 channel.
"""
import binascii
+import os
+import socket
import time
import threading
-import socket
+from functools import wraps
from paramiko import util
from paramiko.common import cMSG_CHANNEL_REQUEST, cMSG_CHANNEL_WINDOW_ADJUST, \
@@ -36,13 +38,30 @@ from paramiko.ssh_exception import SSHException
from paramiko.file import BufferedFile
from paramiko.buffered_pipe import BufferedPipe, PipeTimeout
from paramiko import pipe
+from paramiko.util import ClosingContextManager
+
+def open_only(func):
+ """
+ Decorator for `.Channel` methods which performs an openness check.
-# lower bound on the max packet size we'll accept from the remote host
-MIN_PACKET_SIZE = 1024
+ :raises SSHException:
+ If the wrapped method is called on an unopened `.Channel`.
+ """
+ @wraps(func)
+ def _check(self, *args, **kwds):
+ if (
+ self.closed
+ or self.eof_received
+ or self.eof_sent
+ or not self.active
+ ):
+ raise SSHException('Channel is not open')
+ return func(self, *args, **kwds)
+ return _check
-class Channel (object):
+class Channel (ClosingContextManager):
"""
A secure tunnel across an SSH `.Transport`. A Channel is meant to behave
like a socket, and has an API that should be indistinguishable from the
@@ -55,6 +74,8 @@ class Channel (object):
flow-controlled independently.) Similarly, if the server isn't reading
data you send, calls to `send` may block, unless you set a timeout. This
is exactly like a normal network socket, so it shouldn't be too surprising.
+
+ Instances of this class may be used as context managers.
"""
def __init__(self, chanid):
@@ -95,13 +116,13 @@ class Channel (object):
self.combine_stderr = False
self.exit_status = -1
self.origin_addr = None
-
+
def __del__(self):
try:
self.close()
except:
pass
-
+
def __repr__(self):
"""
Return a string representation of this object, for debugging.
@@ -121,6 +142,7 @@ class Channel (object):
out += '>'
return out
+ @open_only
def get_pty(self, term='vt100', width=80, height=24, width_pixels=0,
height_pixels=0):
"""
@@ -135,12 +157,10 @@ class Channel (object):
:param int height: height (in characters) of the terminal screen
:param int width_pixels: width (in pixels) of the terminal screen
:param int height_pixels: height (in pixels) of the terminal screen
-
+
:raises SSHException:
if the request was rejected or the channel was closed
"""
- if self.closed or self.eof_received or self.eof_sent or not self.active:
- raise SSHException('Channel is not open')
m = Message()
m.add_byte(cMSG_CHANNEL_REQUEST)
m.add_int(self.remote_chanid)
@@ -156,24 +176,23 @@ class Channel (object):
self.transport._send_user_message(m)
self._wait_for_event()
+ @open_only
def invoke_shell(self):
"""
Request an interactive shell session on this channel. If the server
allows it, the channel will then be directly connected to the stdin,
stdout, and stderr of the shell.
-
+
Normally you would call `get_pty` before this, in which case the
shell will operate through the pty, and the channel will be connected
to the stdin and stdout of the pty.
-
+
When the shell exits, the channel will be closed and can't be reused.
You must open a new channel if you wish to open another shell.
-
+
:raises SSHException: if the request was rejected or the channel was
closed
"""
- if self.closed or self.eof_received or self.eof_sent or not self.active:
- raise SSHException('Channel is not open')
m = Message()
m.add_byte(cMSG_CHANNEL_REQUEST)
m.add_int(self.remote_chanid)
@@ -183,12 +202,13 @@ class Channel (object):
self.transport._send_user_message(m)
self._wait_for_event()
+ @open_only
def exec_command(self, command):
"""
Execute a command on the server. If the server allows it, the channel
will then be directly connected to the stdin, stdout, and stderr of
the command being executed.
-
+
When the command finishes executing, the channel will be closed and
can't be reused. You must open a new channel if you wish to execute
another command.
@@ -198,8 +218,6 @@ class Channel (object):
:raises SSHException: if the request was rejected or the channel was
closed
"""
- if self.closed or self.eof_received or self.eof_sent or not self.active:
- raise SSHException('Channel is not open')
m = Message()
m.add_byte(cMSG_CHANNEL_REQUEST)
m.add_int(self.remote_chanid)
@@ -210,12 +228,13 @@ class Channel (object):
self.transport._send_user_message(m)
self._wait_for_event()
+ @open_only
def invoke_subsystem(self, subsystem):
"""
Request a subsystem on the server (for example, ``sftp``). If the
server allows it, the channel will then be directly connected to the
requested subsystem.
-
+
When the subsystem finishes, the channel will be closed and can't be
reused.
@@ -224,8 +243,6 @@ class Channel (object):
:raises SSHException:
if the request was rejected or the channel was closed
"""
- if self.closed or self.eof_received or self.eof_sent or not self.active:
- raise SSHException('Channel is not open')
m = Message()
m.add_byte(cMSG_CHANNEL_REQUEST)
m.add_int(self.remote_chanid)
@@ -236,6 +253,7 @@ class Channel (object):
self.transport._send_user_message(m)
self._wait_for_event()
+ @open_only
def resize_pty(self, width=80, height=24, width_pixels=0, height_pixels=0):
"""
Resize the pseudo-terminal. This can be used to change the width and
@@ -249,8 +267,6 @@ class Channel (object):
:raises SSHException:
if the request was rejected or the channel was closed
"""
- if self.closed or self.eof_received or self.eof_sent or not self.active:
- raise SSHException('Channel is not open')
m = Message()
m.add_byte(cMSG_CHANNEL_REQUEST)
m.add_int(self.remote_chanid)
@@ -268,14 +284,14 @@ class Channel (object):
status. You may use this to poll the process status if you don't
want to block in `recv_exit_status`. Note that the server may not
return an exit status in some cases (like bad servers).
-
+
:return:
``True`` if `recv_exit_status` will return immediately, else ``False``.
.. versionadded:: 1.7.3
"""
- return self.closed or self.status_event.isSet()
-
+ return self.closed or self.status_event.is_set()
+
def recv_exit_status(self):
"""
Return the exit status from the process on the server. This is
@@ -283,13 +299,13 @@ class Channel (object):
If the command hasn't finished yet, this method will wait until
it does, or until the channel is closed. If no exit status is
provided by the server, -1 is returned.
-
+
:return: the exit code (as an `int`) of the process on the server.
-
+
.. versionadded:: 1.2
"""
self.status_event.wait()
- assert self.status_event.isSet()
+ assert self.status_event.is_set()
return self.exit_status
def send_exit_status(self, status):
@@ -298,9 +314,9 @@ class Channel (object):
really only makes sense in server mode.) Many clients expect to
get some sort of status code back from an executed command after
it completes.
-
+
:param int status: the exit code of the process
-
+
.. versionadded:: 1.2
"""
# in many cases, the channel will not still be open here.
@@ -312,20 +328,21 @@ class Channel (object):
m.add_boolean(False)
m.add_int(status)
self.transport._send_user_message(m)
-
+
+ @open_only
def request_x11(self, screen_number=0, auth_protocol=None, auth_cookie=None,
single_connection=False, handler=None):
"""
Request an x11 session on this channel. If the server allows it,
further x11 requests can be made from the server to the client,
when an x11 application is run in a shell session.
-
- From RFC4254::
+
+ From :rfc:`4254`::
It is RECOMMENDED that the 'x11 authentication cookie' that is
sent be a fake, random cookie, and that the cookie be checked and
replaced by the real cookie when a connection request is received.
-
+
If you omit the auth_cookie, a new secure random 128-bit value will be
generated, used, and returned. You will need to use this value to
verify incoming x11 requests and replace them with the actual local
@@ -335,9 +352,9 @@ class Channel (object):
whenever a new x11 connection arrives. The default handler queues up
incoming x11 connections, which may be retrieved using
`.Transport.accept`. The handler's calling signature is::
-
+
handler(channel: Channel, (address: str, port: int))
-
+
:param int screen_number: the x11 screen number (0, 10, etc.)
:param str auth_protocol:
the name of the X11 authentication method used; if none is given,
@@ -353,12 +370,10 @@ class Channel (object):
an optional handler to use for incoming X11 connections
:return: the auth_cookie used
"""
- if self.closed or self.eof_received or self.eof_sent or not self.active:
- raise SSHException('Channel is not open')
if auth_protocol is None:
auth_protocol = 'MIT-MAGIC-COOKIE-1'
if auth_cookie is None:
- auth_cookie = binascii.hexlify(self.transport.rng.read(16))
+ auth_cookie = binascii.hexlify(os.urandom(16))
m = Message()
m.add_byte(cMSG_CHANNEL_REQUEST)
@@ -375,6 +390,7 @@ class Channel (object):
self.transport._set_x11_handler(handler)
return auth_cookie
+ @open_only
def request_forward_agent(self, handler):
"""
Request for a forward SSH Agent on this channel.
@@ -387,9 +403,6 @@ class Channel (object):
:raises: SSHException in case of channel problem.
"""
- if self.closed or self.eof_received or self.eof_sent or not self.active:
- raise SSHException('Channel is not open')
-
m = Message()
m.add_byte(cMSG_CHANNEL_REQUEST)
m.add_int(self.remote_chanid)
@@ -424,33 +437,33 @@ class Channel (object):
def get_id(self):
"""
Return the `int` ID # for this channel.
-
+
The channel ID is unique across a `.Transport` and usually a small
number. It's also the number passed to
`.ServerInterface.check_channel_request` when determining whether to
accept a channel request in server mode.
"""
return self.chanid
-
+
def set_combine_stderr(self, combine):
"""
Set whether stderr should be combined into stdout on this channel.
The default is ``False``, but in some cases it may be convenient to
have both streams combined.
-
+
If this is ``False``, and `exec_command` is called (or ``invoke_shell``
with no pty), output to stderr will not show up through the `recv`
and `recv_ready` calls. You will have to use `recv_stderr` and
`recv_stderr_ready` to get stderr output.
-
+
If this is ``True``, data will never show up via `recv_stderr` or
`recv_stderr_ready`.
-
+
:param bool combine:
``True`` if stderr output should be combined into stdout on this
channel.
:return: the previous setting (a `bool`).
-
+
.. versionadded:: 1.1
"""
data = bytes()
@@ -559,7 +572,7 @@ class Channel (object):
Returns true if data is buffered and ready to be read from this
channel. A ``False`` result does not mean that the channel has closed;
it means you may need to wait before more data arrives.
-
+
:return:
``True`` if a `recv` call on this channel would immediately return
at least one byte; ``False`` otherwise.
@@ -574,8 +587,8 @@ class Channel (object):
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`.
"""
@@ -601,11 +614,11 @@ class Channel (object):
channel's stderr stream. Only channels using `exec_command` or
`invoke_shell` without a pty will ever have data on the stderr
stream.
-
+
:return:
``True`` if a `recv_stderr` call on this channel would immediately
return at least one byte; ``False`` otherwise.
-
+
.. versionadded:: 1.1
"""
return self.in_stderr_buffer.read_ready()
@@ -621,17 +634,17 @@ class Channel (object):
:param int nbytes: maximum number of bytes to read.
:return: received data as a `str`
-
+
:raises socket.timeout: if no data is ready before the timeout set by
`settimeout`.
-
+
.. versionadded:: 1.1
"""
try:
out = self.in_stderr_buffer.read(nbytes, self.timeout)
except PipeTimeout:
raise socket.timeout()
-
+
ack = self._check_add_window(len(out))
# no need to hold the channel lock when sending this
if ack > 0:
@@ -647,11 +660,11 @@ class Channel (object):
"""
Returns true if data can be written to this channel without blocking.
This means the channel is either closed (so any write attempt would
- return immediately) or there is at least one byte of space in the
+ return immediately) or there is at least one byte of space in the
outbound buffer. If there is at least one byte of space in the
outbound buffer, a `send` call will succeed immediately and return
the number of bytes actually written.
-
+
:return:
``True`` if a `send` call on this channel would immediately succeed
or fail
@@ -663,7 +676,7 @@ class Channel (object):
return self.out_window_size > 0
finally:
self.lock.release()
-
+
def send(self, s):
"""
Send data to the channel. Returns the number of bytes sent, or 0 if
@@ -678,23 +691,11 @@ class Channel (object):
:raises socket.timeout: if no data could be sent before the timeout set
by `settimeout`.
"""
- size = len(s)
- self.lock.acquire()
- try:
- size = self._wait_for_send_window(size)
- if size == 0:
- # eof or similar
- return 0
- m = Message()
- m.add_byte(cMSG_CHANNEL_DATA)
- m.add_int(self.remote_chanid)
- m.add_string(s[:size])
- finally:
- self.lock.release()
- # Note: We release self.lock before calling _send_user_message.
- # Otherwise, we can deadlock during re-keying.
- self.transport._send_user_message(m)
- return size
+
+ m = Message()
+ m.add_byte(cMSG_CHANNEL_DATA)
+ m.add_int(self.remote_chanid)
+ return self._send(s, m)
def send_stderr(self, s):
"""
@@ -704,33 +705,22 @@ class Channel (object):
stream is closed. Applications are responsible for checking that all
data has been sent: if only some of the data was transmitted, the
application needs to attempt delivery of the remaining data.
-
+
:param str s: data to send.
:return: number of bytes actually sent, as an `int`.
-
+
:raises socket.timeout:
if no data could be sent before the timeout set by `settimeout`.
-
+
.. versionadded:: 1.1
"""
- size = len(s)
- self.lock.acquire()
- try:
- size = self._wait_for_send_window(size)
- if size == 0:
- # eof or similar
- return 0
- m = Message()
- m.add_byte(cMSG_CHANNEL_EXTENDED_DATA)
- m.add_int(self.remote_chanid)
- m.add_int(1)
- m.add_string(s[:size])
- finally:
- self.lock.release()
- # Note: We release self.lock before calling _send_user_message.
- # Otherwise, we can deadlock during re-keying.
- self.transport._send_user_message(m)
- return size
+
+ m = Message()
+ m.add_byte(cMSG_CHANNEL_EXTENDED_DATA)
+ m.add_int(self.remote_chanid)
+ m.add_int(1)
+ return self._send(s, m)
+
def sendall(self, s):
"""
@@ -751,9 +741,6 @@ class Channel (object):
This is irritating, but identically follows Python's API.
"""
while s:
- if self.closed:
- # this doesn't seem useful, but it is the documented behavior of Socket
- raise socket.error('Socket is closed')
sent = self.send(s)
s = s[sent:]
return None
@@ -764,9 +751,9 @@ class Channel (object):
results. Unlike `send_stderr`, this method continues to send data
from the given string until all data has been sent or an error occurs.
Nothing is returned.
-
+
:param str s: data to send to the client as "stderr" output.
-
+
:raises socket.timeout:
if sending stalled for longer than the timeout set by `settimeout`.
:raises socket.error:
@@ -775,8 +762,6 @@ class Channel (object):
.. versionadded:: 1.1
"""
while s:
- if self.closed:
- raise socket.error('Socket is closed')
sent = self.send_stderr(s)
s = s[sent:]
return None
@@ -796,18 +781,18 @@ class Channel (object):
Return a file-like object associated with this channel's stderr
stream. Only channels using `exec_command` or `invoke_shell`
without a pty will ever have data on the stderr stream.
-
+
The optional ``mode`` and ``bufsize`` arguments are interpreted the
same way as by the built-in ``file()`` function in Python. For a
client, it only makes sense to open this file for reading. For a
server, it only makes sense to open this file for writing.
-
+
:return: `.ChannelFile` object which can be used for Python file I/O.
.. versionadded:: 1.1
"""
return ChannelStderrFile(*([self] + list(params)))
-
+
def fileno(self):
"""
Returns an OS-level file descriptor which can be used for polling, but
@@ -821,7 +806,7 @@ class Channel (object):
open at the same time.)
:return: an OS-level file descriptor (`int`)
-
+
.. warning::
This method causes channel reads to be slightly less efficient.
"""
@@ -860,7 +845,7 @@ class Channel (object):
self.lock.release()
if m is not None:
self.transport._send_user_message(m)
-
+
def shutdown_read(self):
"""
Shutdown the receiving side of this socket, closing the stream in
@@ -868,11 +853,11 @@ class Channel (object):
channel will fail instantly. This is a convenience method, equivalent
to ``shutdown(0)``, for people who don't make it a habit to
memorize unix constants from the 1970s.
-
+
.. versionadded:: 1.2
"""
self.shutdown(0)
-
+
def shutdown_write(self):
"""
Shutdown the sending side of this socket, closing the stream in
@@ -880,7 +865,7 @@ class Channel (object):
channel will fail instantly. This is a convenience method, equivalent
to ``shutdown(1)``, for people who don't make it a habit to
memorize unix constants from the 1970s.
-
+
.. versionadded:: 1.2
"""
self.shutdown(1)
@@ -898,14 +883,15 @@ class Channel (object):
self.in_window_threshold = window_size // 10
self.in_window_sofar = 0
self._log(DEBUG, 'Max packet in: %d bytes' % max_packet_size)
-
+
def _set_remote_channel(self, chanid, window_size, max_packet_size):
self.remote_chanid = chanid
self.out_window_size = window_size
- self.out_max_packet_size = max(max_packet_size, MIN_PACKET_SIZE)
+ self.out_max_packet_size = self.transport. \
+ _sanitize_packet_size(max_packet_size)
self.active = 1
- self._log(DEBUG, 'Max packet out: %d bytes' % max_packet_size)
-
+ self._log(DEBUG, 'Max packet out: %d bytes' % self.out_max_packet_size)
+
def _request_success(self, m):
self._log(DEBUG, 'Sesch channel %d request ok' % self.chanid)
self.event_ready = True
@@ -940,7 +926,7 @@ class Channel (object):
self._feed(s)
else:
self.in_stderr_buffer.feed(s)
-
+
def _window_adjust(self, m):
nbytes = m.get_int()
self.lock.acquire()
@@ -1063,6 +1049,25 @@ class Channel (object):
### internals...
+ def _send(self, s, m):
+ size = len(s)
+ self.lock.acquire()
+ try:
+ if self.closed:
+ # this doesn't seem useful, but it is the documented behavior of Socket
+ raise socket.error('Socket is closed')
+ size = self._wait_for_send_window(size)
+ if size == 0:
+ # eof or similar
+ return 0
+ m.add_string(s[:size])
+ finally:
+ self.lock.release()
+ # Note: We release self.lock before calling _send_user_message.
+ # Otherwise, we can deadlock during re-keying.
+ self.transport._send_user_message(m)
+ return size
+
def _log(self, level, msg, *args):
self.logger.log(level, "[chan " + self._name + "] " + msg, *args)
@@ -1072,7 +1077,7 @@ class Channel (object):
def _wait_for_event(self):
self.event.wait()
- assert self.event.isSet()
+ assert self.event.is_set()
if self.event_ready:
return
e = self.transport.get_exception()
@@ -1182,7 +1187,7 @@ class Channel (object):
if self.ultra_debug:
self._log(DEBUG, 'window down to %d' % self.out_window_size)
return size
-
+
class ChannelFile (BufferedFile):
"""
@@ -1222,7 +1227,7 @@ class ChannelStderrFile (ChannelFile):
def _read(self, size):
return self.channel.recv_stderr(size)
-
+
def _write(self, data):
self.channel.sendall_stderr(data)
return len(data)
diff --git a/paramiko/client.py b/paramiko/client.py
index c1bf4735..5a215a81 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -25,21 +25,25 @@ import getpass
import os
import socket
import warnings
+from errno import ECONNREFUSED, EHOSTUNREACH
from paramiko.agent import Agent
from paramiko.common import DEBUG
from paramiko.config import SSH_PORT
from paramiko.dsskey import DSSKey
+from paramiko.ecdsakey import ECDSAKey
from paramiko.hostkeys import HostKeys
from paramiko.py3compat import string_types
from paramiko.resource import ResourceManager
from paramiko.rsakey import RSAKey
-from paramiko.ssh_exception import SSHException, BadHostKeyException
+from paramiko.ssh_exception import (
+ SSHException, BadHostKeyException, NoValidConnectionsError
+)
from paramiko.transport import Transport
-from paramiko.util import retry_on_signal
+from paramiko.util import retry_on_signal, ClosingContextManager
-class SSHClient (object):
+class SSHClient (ClosingContextManager):
"""
A high-level representation of a session with an SSH server. This class
wraps `.Transport`, `.Channel`, and `.SFTPClient` to take care of most
@@ -54,6 +58,8 @@ class SSHClient (object):
checking. The default mechanism is to try to use local key files or an
SSH agent (if one is running).
+ Instances of this class may be used as context managers.
+
.. versionadded:: 1.6
"""
@@ -169,9 +175,46 @@ class SSHClient (object):
"""
self._policy = policy
- def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=None,
- key_filename=None, timeout=None, allow_agent=True, look_for_keys=True,
- compress=False, sock=None):
+ def _families_and_addresses(self, hostname, port):
+ """
+ Yield pairs of address families and addresses to try for connecting.
+
+ :param str hostname: the server to connect to
+ :param int port: the server port to connect to
+ :returns: Yields an iterable of ``(family, address)`` tuples
+ """
+ guess = True
+ addrinfos = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
+ for (family, socktype, proto, canonname, sockaddr) in addrinfos:
+ if socktype == socket.SOCK_STREAM:
+ yield family, sockaddr
+ guess = False
+
+ # some OS like AIX don't indicate SOCK_STREAM support, so just guess. :(
+ # We only do this if we did not get a single result marked as socktype == SOCK_STREAM.
+ if guess:
+ for family, _, _, _, sockaddr in addrinfos:
+ yield family, sockaddr
+
+ def connect(
+ self,
+ hostname,
+ port=SSH_PORT,
+ username=None,
+ password=None,
+ pkey=None,
+ key_filename=None,
+ timeout=None,
+ allow_agent=True,
+ look_for_keys=True,
+ compress=False,
+ sock=None,
+ gss_auth=False,
+ gss_kex=False,
+ gss_deleg_creds=True,
+ gss_host=None,
+ banner_timeout=None
+ ):
"""
Connect to an SSH server and authenticate to it. The server's host key
is checked against the system host keys (see `load_system_host_keys`)
@@ -184,7 +227,8 @@ class SSHClient (object):
- The ``pkey`` or ``key_filename`` passed in (if any)
- Any key we can find through an SSH agent
- - Any "id_rsa" or "id_dsa" key discoverable in ``~/.ssh/``
+ - Any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in
+ ``~/.ssh/``
- Plain username/password auth, if a password was given
If a private key requires a password to unlock it, and a password is
@@ -201,8 +245,10 @@ class SSHClient (object):
:param str key_filename:
the filename, or list of filenames, of optional private key(s) to
try for authentication
- :param float timeout: an optional timeout (in seconds) for the TCP connect
- :param bool allow_agent: set to False to disable connecting to the SSH agent
+ :param float timeout:
+ an optional timeout (in seconds) for the TCP connect
+ :param bool allow_agent:
+ set to False to disable connecting to the SSH agent
:param bool look_for_keys:
set to False to disable searching for discoverable private key
files in ``~/.ssh/``
@@ -210,6 +256,14 @@ class SSHClient (object):
: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_kex:
+ Perform GSS-API Key Exchange and user authentication
+ :param bool gss_deleg_creds: Delegate GSS-API client credentials or not
+ :param str gss_host:
+ The targets name in the kerberos database. default: hostname
+ :param float banner_timeout: an optional timeout (in seconds) to wait
+ for the SSH banner to be presented.
:raises BadHostKeyException: if the server's host key could not be
verified
@@ -217,28 +271,56 @@ class SSHClient (object):
:raises SSHException: if there was any other error connecting or
establishing an SSH session
:raises socket.error: if a socket error occurred while connecting
+
+ .. versionchanged:: 1.15
+ Added the ``banner_timeout``, ``gss_auth``, ``gss_kex``,
+ ``gss_deleg_creds`` and ``gss_host`` arguments.
"""
if not sock:
- for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
- if socktype == socket.SOCK_STREAM:
- af = family
- addr = sockaddr
- break
- else:
- # some OS like AIX don't indicate SOCK_STREAM support, so just guess. :(
- af, _, _, _, addr = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
- sock = socket.socket(af, socket.SOCK_STREAM)
- if timeout is not None:
+ errors = {}
+ # Try multiple possible address families (e.g. IPv4 vs IPv6)
+ to_try = list(self._families_and_addresses(hostname, port))
+ for af, addr in to_try:
try:
- sock.settimeout(timeout)
- except:
- pass
- retry_on_signal(lambda: sock.connect(addr))
-
- t = self._transport = Transport(sock)
+ sock = socket.socket(af, socket.SOCK_STREAM)
+ if timeout is not None:
+ try:
+ sock.settimeout(timeout)
+ except:
+ pass
+ retry_on_signal(lambda: sock.connect(addr))
+ # Break out of the loop on success
+ break
+ except socket.error as e:
+ # Raise anything that isn't a straight up connection error
+ # (such as a resolution error)
+ if e.errno not in (ECONNREFUSED, EHOSTUNREACH):
+ raise
+ # Capture anything else so we know how the run looks once
+ # iteration is complete. Retain info about which attempt
+ # this was.
+ errors[addr] = e
+
+ # Make sure we explode usefully if no address family attempts
+ # succeeded. We've no way of knowing which error is the "right"
+ # one, so we construct a hybrid exception containing all the real
+ # ones, of a subclass that client code should still be watching for
+ # (socket.error)
+ if len(errors) == len(to_try):
+ raise NoValidConnectionsError(errors)
+
+ t = self._transport = Transport(sock, gss_kex=gss_kex, gss_deleg_creds=gss_deleg_creds)
t.use_compression(compress=compress)
+ if gss_kex and gss_host is None:
+ t.set_gss_host(hostname)
+ elif gss_kex and gss_host is not None:
+ t.set_gss_host(gss_host)
+ else:
+ pass
if self._log_channel is not None:
t.set_log_channel(self._log_channel)
+ if banner_timeout is not None:
+ t.banner_timeout = banner_timeout
t.start_client()
ResourceManager.register(self, t)
@@ -249,17 +331,25 @@ class SSHClient (object):
server_hostkey_name = hostname
else:
server_hostkey_name = "[%s]:%d" % (hostname, port)
- our_server_key = self._system_host_keys.get(server_hostkey_name, {}).get(keytype, None)
- if our_server_key is None:
- our_server_key = self._host_keys.get(server_hostkey_name, {}).get(keytype, None)
- if our_server_key is None:
- # will raise exception if the key is rejected; let that fall out
- self._policy.missing_host_key(self, server_hostkey_name, server_key)
- # if the callback returns, assume the key is ok
- our_server_key = server_key
-
- if server_key != our_server_key:
- raise BadHostKeyException(hostname, server_key, our_server_key)
+
+ # If GSS-API Key Exchange is performed we are not required to check the
+ # host key, because the host is authenticated via GSS-API / SSPI as
+ # well as our client.
+ if not self._transport.use_gss_kex:
+ our_server_key = self._system_host_keys.get(server_hostkey_name,
+ {}).get(keytype, None)
+ if our_server_key is None:
+ our_server_key = self._host_keys.get(server_hostkey_name,
+ {}).get(keytype, None)
+ if our_server_key is None:
+ # will raise exception if the key is rejected; let that fall out
+ self._policy.missing_host_key(self, server_hostkey_name,
+ server_key)
+ # if the callback returns, assume the key is ok
+ our_server_key = server_key
+
+ if server_key != our_server_key:
+ raise BadHostKeyException(hostname, server_key, our_server_key)
if username is None:
username = getpass.getuser()
@@ -270,7 +360,10 @@ class SSHClient (object):
key_filenames = [key_filename]
else:
key_filenames = key_filename
- self._auth(username, password, pkey, key_filenames, allow_agent, look_for_keys)
+ if gss_host is None:
+ gss_host = hostname
+ self._auth(username, password, pkey, key_filenames, allow_agent,
+ look_for_keys, gss_auth, gss_kex, gss_deleg_creds, gss_host)
def close(self):
"""
@@ -304,7 +397,7 @@ class SSHClient (object):
: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)
@@ -354,13 +447,15 @@ class SSHClient (object):
"""
return self._transport
- def _auth(self, username, password, pkey, key_filenames, allow_agent, look_for_keys):
+ def _auth(self, username, password, pkey, key_filenames, allow_agent,
+ look_for_keys, gss_auth, gss_kex, gss_deleg_creds, gss_host):
"""
Try, in order:
- The key passed in, if one was passed in.
- Any key we can find through an SSH agent (if allowed).
- - Any "id_rsa" or "id_dsa" key discoverable in ~/.ssh/ (if allowed).
+ - Any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in ~/.ssh/
+ (if allowed).
- Plain username/password auth, if a password was given.
(The password might be needed to unlock a private key, or for
@@ -370,6 +465,27 @@ class SSHClient (object):
two_factor = False
allowed_types = []
+ # If GSS-API support and GSS-PI Key Exchange was performed, we attempt
+ # authentication with gssapi-keyex.
+ if gss_kex and self._transport.gss_kex_used:
+ try:
+ self._transport.auth_gssapi_keyex(username)
+ return
+ except Exception as e:
+ saved_exception = e
+
+ # Try GSS-API authentication (gssapi-with-mic) only if GSS-API Key
+ # Exchange is not performed, because if we use GSS-API for the key
+ # exchange, there is already a fully established GSS-API context, so
+ # why should we do that again?
+ if gss_auth:
+ try:
+ self._transport.auth_gssapi_with_mic(username, gss_host,
+ gss_deleg_creds)
+ return
+ except Exception as e:
+ saved_exception = e
+
if pkey is not None:
try:
self._log(DEBUG, 'Trying SSH key %s' % hexlify(pkey.get_fingerprint()))
@@ -382,7 +498,7 @@ class SSHClient (object):
if not two_factor:
for key_filename in key_filenames:
- for pkey_class in (RSAKey, DSSKey):
+ for pkey_class in (RSAKey, DSSKey, ECDSAKey):
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))
@@ -414,17 +530,23 @@ class SSHClient (object):
keyfiles = []
rsa_key = os.path.expanduser('~/.ssh/id_rsa')
dsa_key = os.path.expanduser('~/.ssh/id_dsa')
+ ecdsa_key = os.path.expanduser('~/.ssh/id_ecdsa')
if os.path.isfile(rsa_key):
keyfiles.append((RSAKey, rsa_key))
if os.path.isfile(dsa_key):
keyfiles.append((DSSKey, dsa_key))
+ if os.path.isfile(ecdsa_key):
+ keyfiles.append((ECDSAKey, ecdsa_key))
# look in ~/ssh/ for windows users:
rsa_key = os.path.expanduser('~/ssh/id_rsa')
dsa_key = os.path.expanduser('~/ssh/id_dsa')
+ ecdsa_key = os.path.expanduser('~/ssh/id_ecdsa')
if os.path.isfile(rsa_key):
keyfiles.append((RSAKey, rsa_key))
if os.path.isfile(dsa_key):
keyfiles.append((DSSKey, dsa_key))
+ if os.path.isfile(ecdsa_key):
+ keyfiles.append((ECDSAKey, ecdsa_key))
if not look_for_keys:
keyfiles = []
diff --git a/paramiko/common.py b/paramiko/common.py
index 9a5e2ee1..0b0cc2a7 100644
--- a/paramiko/common.py
+++ b/paramiko/common.py
@@ -29,6 +29,9 @@ MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \
MSG_USERAUTH_BANNER = range(50, 54)
MSG_USERAUTH_PK_OK = 60
MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE = range(60, 62)
+MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN = range(60, 62)
+MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, MSG_USERAUTH_GSSAPI_ERROR,\
+MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC = range(63, 67)
MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE = range(80, 83)
MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \
MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA, \
@@ -50,6 +53,12 @@ cMSG_USERAUTH_BANNER = byte_chr(MSG_USERAUTH_BANNER)
cMSG_USERAUTH_PK_OK = byte_chr(MSG_USERAUTH_PK_OK)
cMSG_USERAUTH_INFO_REQUEST = byte_chr(MSG_USERAUTH_INFO_REQUEST)
cMSG_USERAUTH_INFO_RESPONSE = byte_chr(MSG_USERAUTH_INFO_RESPONSE)
+cMSG_USERAUTH_GSSAPI_RESPONSE = byte_chr(MSG_USERAUTH_GSSAPI_RESPONSE)
+cMSG_USERAUTH_GSSAPI_TOKEN = byte_chr(MSG_USERAUTH_GSSAPI_TOKEN)
+cMSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE = byte_chr(MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE)
+cMSG_USERAUTH_GSSAPI_ERROR = byte_chr(MSG_USERAUTH_GSSAPI_ERROR)
+cMSG_USERAUTH_GSSAPI_ERRTOK = byte_chr(MSG_USERAUTH_GSSAPI_ERRTOK)
+cMSG_USERAUTH_GSSAPI_MIC = byte_chr(MSG_USERAUTH_GSSAPI_MIC)
cMSG_GLOBAL_REQUEST = byte_chr(MSG_GLOBAL_REQUEST)
cMSG_REQUEST_SUCCESS = byte_chr(MSG_REQUEST_SUCCESS)
cMSG_REQUEST_FAILURE = byte_chr(MSG_REQUEST_FAILURE)
@@ -80,6 +89,8 @@ MSG_NAMES = {
32: 'kex32',
33: 'kex33',
34: 'kex34',
+ 40: 'kex40',
+ 41: 'kex41',
MSG_USERAUTH_REQUEST: 'userauth-request',
MSG_USERAUTH_FAILURE: 'userauth-failure',
MSG_USERAUTH_SUCCESS: 'userauth-success',
@@ -99,7 +110,13 @@ MSG_NAMES = {
MSG_CHANNEL_CLOSE: 'channel-close',
MSG_CHANNEL_REQUEST: 'channel-request',
MSG_CHANNEL_SUCCESS: 'channel-success',
- MSG_CHANNEL_FAILURE: 'channel-failure'
+ MSG_CHANNEL_FAILURE: 'channel-failure',
+ MSG_USERAUTH_GSSAPI_RESPONSE: 'userauth-gssapi-response',
+ MSG_USERAUTH_GSSAPI_TOKEN: 'userauth-gssapi-token',
+ MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE: 'userauth-gssapi-exchange-complete',
+ MSG_USERAUTH_GSSAPI_ERROR: 'userauth-gssapi-error',
+ MSG_USERAUTH_GSSAPI_ERRTOK: 'userauth-gssapi-error-token',
+ MSG_USERAUTH_GSSAPI_MIC: 'userauth-gssapi-mic'
}
@@ -126,11 +143,6 @@ CONNECTION_FAILED_CODE = {
DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \
DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14
-from Crypto import Random
-
-# keep a crypto-strong PRNG nearby
-rng = Random.new()
-
zero_byte = byte_chr(0)
one_byte = byte_chr(1)
four_byte = byte_chr(4)
@@ -176,3 +188,18 @@ CRITICAL = logging.CRITICAL
# Common IO/select/etc sleep period, in seconds
io_sleep = 0.01
+
+DEFAULT_WINDOW_SIZE = 64 * 2 ** 15
+DEFAULT_MAX_PACKET_SIZE = 2 ** 15
+
+# lower bound on the max packet size we'll accept from the remote host
+# Minimum packet size is 32768 bytes according to
+# http://www.ietf.org/rfc/rfc4254.txt
+MIN_WINDOW_SIZE = 2 ** 15
+
+# However, according to http://www.ietf.org/rfc/rfc4253.txt it is perfectly
+# legal to accept a size much smaller, as OpenSSH client does as size 16384.
+MIN_PACKET_SIZE = 2 ** 12
+
+# Max windows size according to http://www.ietf.org/rfc/rfc4254.txt
+MAX_WINDOW_SIZE = 2**32 -1
diff --git a/paramiko/config.py b/paramiko/config.py
index c21de936..0b1345fd 100644
--- a/paramiko/config.py
+++ b/paramiko/config.py
@@ -24,10 +24,10 @@ Configuration file (aka ``ssh_config``) support.
import fnmatch
import os
import re
+import shlex
import socket
SSH_PORT = 22
-proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I)
class SSHConfig (object):
@@ -41,6 +41,8 @@ class SSHConfig (object):
.. versionadded:: 1.6
"""
+ SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)')
+
def __init__(self):
"""
Create a new OpenSSH config object.
@@ -51,46 +53,43 @@ 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:
line = line.rstrip('\r\n').lstrip()
if not line or line.startswith('#'):
continue
- if '=' in line:
- # Ensure ProxyCommand gets properly split
- if line.lower().strip().startswith('proxycommand'):
- match = proxy_re.match(line)
- key, value = match.group(1).lower(), match.group(2)
- else:
- key, value = line.split('=', 1)
- key = key.strip().lower()
- else:
- # find first whitespace, and split there
- i = 0
- while (i < len(line)) and not line[i].isspace():
- i += 1
- if i == len(line):
- raise Exception('Unparsable line: %r' % line)
- key = line[:i].lower()
- value = line[i:].lstrip()
+ match = re.match(self.SETTINGS_REGEX, line)
+ if not match:
+ raise Exception("Unparsable line %s" % line)
+ key = match.group(1).lower()
+ value = match.group(2)
+
if key == 'host':
self._config.append(host)
- value = value.split()
- host = {key: value, 'config': {}}
- #identityfile, localforward, remoteforward keys are special cases, since they are allowed to be
- # specified multiple times and they should be tried in order
- # of specification.
-
- elif key in ['identityfile', 'localforward', 'remoteforward']:
- if key in host['config']:
- host['config'][key].append(value)
- else:
- host['config'][key] = [value]
- elif key not in host['config']:
- host['config'][key] = value
+ host = {
+ 'host': self._get_hosts(value),
+ 'config': {}
+ }
+ elif key == 'proxycommand' and value.lower() == 'none':
+ # Proxycommands of none should not be added as an actual value. (Issue #415)
+ continue
+ else:
+ if value.startswith('"') and value.endswith('"'):
+ value = value[1:-1]
+
+ #identityfile, localforward, remoteforward keys are special cases, since they are allowed to be
+ # specified multiple times and they should be tried in order
+ # of specification.
+ if key in ['identityfile', 'localforward', 'remoteforward']:
+ if key in host['config']:
+ host['config'][key].append(value)
+ else:
+ host['config'][key] = [value]
+ elif key not in host['config']:
+ host['config'][key] = value
self._config.append(host)
def lookup(self, hostname):
@@ -99,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.
@@ -132,6 +131,16 @@ class SSHConfig (object):
ret = self._expand_variables(ret, hostname)
return ret
+ def get_hostnames(self):
+ """
+ Return the set of literal hostnames defined in the SSH config (both
+ explicit hostnames and wildcard entries).
+ """
+ hosts = set()
+ for entry in self._config:
+ hosts.update(entry['host'])
+ return hosts
+
def _allowed(self, hosts, hostname):
match = False
for host in hosts:
@@ -212,6 +221,15 @@ class SSHConfig (object):
config[k] = config[k].replace(find, str(replace))
return config
+ def _get_hosts(self, host):
+ """
+ Return a list of host_names from host value.
+ """
+ try:
+ return shlex.split(host)
+ except ValueError:
+ raise Exception("Unparsable host %s" % host)
+
class LazyFqdn(object):
"""
diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py
index 6a46d326..d7dd6275 100644
--- a/paramiko/dsskey.py
+++ b/paramiko/dsskey.py
@@ -20,11 +20,13 @@
DSS keys.
"""
+import os
+from hashlib import sha1
+
from Crypto.PublicKey import DSA
-from Crypto.Hash import SHA
from paramiko import util
-from paramiko.common import zero_byte, rng
+from paramiko.common import zero_byte
from paramiko.py3compat import long
from paramiko.ssh_exception import SSHException
from paramiko.message import Message
@@ -91,17 +93,17 @@ class DSSKey (PKey):
def get_bits(self):
return self.size
-
+
def can_sign(self):
return self.x is not None
- def sign_ssh_data(self, rng, data):
- digest = SHA.new(data).digest()
+ def sign_ssh_data(self, data):
+ digest = sha1(data).digest()
dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x)))
# generate a suitable k
qsize = len(util.deflate_long(self.q, 0))
while True:
- k = util.inflate_long(rng.read(qsize), 1)
+ k = util.inflate_long(os.urandom(qsize), 1)
if (k > 2) and (k < self.q):
break
r, s = dss.sign(util.inflate_long(digest, 1), k)
@@ -130,7 +132,7 @@ class DSSKey (PKey):
# pull out (r, s) which are NOT encoded as mpints
sigR = util.inflate_long(sig[:20], 1)
sigS = util.inflate_long(sig[20:], 1)
- sigM = util.inflate_long(SHA.new(data).digest(), 1)
+ sigM = util.inflate_long(sha1(data).digest(), 1)
dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q)))
return dss.verify(sigM, (sigR, sigS))
@@ -152,6 +154,7 @@ class DSSKey (PKey):
def write_private_key(self, file_obj, password=None):
self._write_private_key('DSA', file_obj, self._encode_key(), password)
+ @staticmethod
def generate(bits=1024, progress_func=None):
"""
Generate a new private DSS key. This factory function can be used to
@@ -163,22 +166,21 @@ class DSSKey (PKey):
by ``pyCrypto.PublicKey``).
:return: new `.DSSKey` private key
"""
- dsa = DSA.generate(bits, rng.read, progress_func)
+ dsa = DSA.generate(bits, os.urandom, progress_func)
key = DSSKey(vals=(dsa.p, dsa.q, dsa.g, dsa.y))
key.x = dsa.x
return key
- generate = staticmethod(generate)
### internals...
def _from_private_key_file(self, filename, password):
data = self._read_private_key_file('DSA', filename, password)
self._decode_key(data)
-
+
def _from_private_key(self, file_obj, password):
data = self._read_private_key('DSA', file_obj, password)
self._decode_key(data)
-
+
def _decode_key(self, data):
# private key file contains:
# DSAPrivateKey = { version = 0, p, q, g, y, x }
diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py
index 7376b81f..8827a1db 100644
--- a/paramiko/ecdsakey.py
+++ b/paramiko/ecdsakey.py
@@ -21,11 +21,11 @@ ECDSA keys
"""
import binascii
+from hashlib import sha256
+
from ecdsa import SigningKey, VerifyingKey, der, curves
-from Crypto.Hash import SHA256
-from ecdsa.test_pyecdsa import ECDSA
-from paramiko.common import four_byte, one_byte
+from paramiko.common import four_byte, one_byte
from paramiko.message import Message
from paramiko.pkey import PKey
from paramiko.py3compat import byte_chr, u
@@ -38,7 +38,8 @@ class ECDSAKey (PKey):
data.
"""
- def __init__(self, msg=None, data=None, filename=None, password=None, vals=None, file_obj=None):
+ def __init__(self, msg=None, data=None, filename=None, password=None,
+ vals=None, file_obj=None, validate_point=True):
self.verifying_key = None
self.signing_key = None
if file_obj is not None:
@@ -50,7 +51,7 @@ class ECDSAKey (PKey):
if (msg is None) and (data is not None):
msg = Message(data)
if vals is not None:
- self.verifying_key, self.signing_key = vals
+ self.signing_key, self.verifying_key = vals
else:
if msg is None:
raise SSHException('Key object may not be empty')
@@ -65,7 +66,8 @@ class ECDSAKey (PKey):
raise SSHException('Point compression is being used: %s' %
binascii.hexlify(pointinfo))
self.verifying_key = VerifyingKey.from_string(pointinfo[1:],
- curve=curves.NIST256p)
+ curve=curves.NIST256p,
+ validate_point=validate_point)
self.size = 256
def asbytes(self):
@@ -97,10 +99,9 @@ class ECDSAKey (PKey):
def can_sign(self):
return self.signing_key is not None
- def sign_ssh_data(self, rpool, data):
- digest = SHA256.new(data).digest()
- sig = self.signing_key.sign_digest(digest, entropy=rpool.read,
- sigencode=self._sigencode)
+ def sign_ssh_data(self, data):
+ sig = self.signing_key.sign_deterministic(
+ data, sigencode=self._sigencode, hashfunc=sha256)
m = Message()
m.add_string('ecdsa-sha2-nistp256')
m.add_string(sig)
@@ -113,7 +114,7 @@ class ECDSAKey (PKey):
# verify the signature by SHA'ing the data and encrypting it
# using the public key.
- hash_obj = SHA256.new(data).digest()
+ hash_obj = sha256(data).digest()
return self.verifying_key.verify_digest(sig, hash_obj,
sigdecode=self._sigdecode)
@@ -125,20 +126,18 @@ class ECDSAKey (PKey):
key = self.signing_key or self.verifying_key
self._write_private_key('EC', file_obj, key.to_der(), password)
- def generate(bits, progress_func=None):
+ @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 = ECDSA.generate()
+ signing_key = SigningKey.generate(curve)
key = ECDSAKey(vals=(signing_key, signing_key.get_verifying_key()))
return key
- generate = staticmethod(generate)
### internals...
diff --git a/paramiko/file.py b/paramiko/file.py
index 1c99abeb..e3b0a16a 100644
--- a/paramiko/file.py
+++ b/paramiko/file.py
@@ -19,8 +19,10 @@ from paramiko.common import linefeed_byte_value, crlf, cr_byte, linefeed_byte, \
cr_byte_value
from paramiko.py3compat import BytesIO, PY2, u, b, bytes_types
+from paramiko.util import ClosingContextManager
-class BufferedFile (object):
+
+class BufferedFile (ClosingContextManager):
"""
Reusable base class to implement Python-style file buffering around a
simpler stream.
diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py
index 7c6a5aa5..38ac866b 100644
--- a/paramiko/hostkeys.py
+++ b/paramiko/hostkeys.py
@@ -18,8 +18,11 @@
import binascii
-from Crypto.Hash import SHA, HMAC
-from paramiko.common import rng
+import os
+
+from hashlib import sha1
+from hmac import HMAC
+
from paramiko.py3compat import b, u, encodebytes, decodebytes
try:
@@ -32,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):
@@ -89,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:
@@ -252,6 +259,7 @@ class HostKeys (MutableMapping):
ret.append(self.lookup(k))
return ret
+ @staticmethod
def hash_host(hostname, salt=None):
"""
Return a "hashed" form of the hostname, as used by OpenSSH when storing
@@ -262,16 +270,15 @@ class HostKeys (MutableMapping):
:return: the hashed hostname as a `str`
"""
if salt is None:
- salt = rng.read(SHA.digest_size)
+ salt = os.urandom(sha1().digest_size)
else:
if salt.startswith('|1|'):
salt = salt.split('|')[2]
salt = decodebytes(b(salt))
- assert len(salt) == SHA.digest_size
- hmac = HMAC.HMAC(salt, b(hostname), SHA).digest()
+ assert len(salt) == sha1().digest_size
+ hmac = HMAC(salt, b(hostname), sha1).digest()
hostkey = '|1|%s|%s' % (u(encodebytes(salt)), u(encodebytes(hmac)))
return hostkey.replace('\n', '')
- hash_host = staticmethod(hash_host)
class InvalidHostKey(Exception):
@@ -291,6 +298,7 @@ class HostKeyEntry:
self.hostnames = hostnames
self.key = key
+ @classmethod
def from_line(cls, line, lineno=None):
"""
Parses the given line of text to find the names for the host,
@@ -324,7 +332,7 @@ class HostKeyEntry:
elif keytype == 'ssh-dss':
key = DSSKey(data=decodebytes(key))
elif keytype == 'ecdsa-sha2-nistp256':
- key = ECDSAKey(data=decodebytes(key))
+ key = ECDSAKey(data=decodebytes(key), validate_point=False)
else:
log.info("Unable to handle key of type %s" % (keytype,))
return None
@@ -333,7 +341,6 @@ class HostKeyEntry:
raise InvalidHostKey(line, e)
return cls(names, key)
- from_line = classmethod(from_line)
def to_line(self):
"""
diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py
index a7cfb7c9..c980b690 100644
--- a/paramiko/kex_gex.py
+++ b/paramiko/kex_gex.py
@@ -22,7 +22,8 @@ generator "g" are provided by the server. A bit more work is required on the
client side, and a **lot** more on the server side.
"""
-from Crypto.Hash import SHA
+import os
+from hashlib import sha1, sha256
from paramiko import util
from paramiko.common import DEBUG
@@ -43,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
@@ -86,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...
@@ -101,7 +103,7 @@ class KexGex (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):
@@ -203,10 +205,10 @@ class KexGex (object):
hm.add_mpint(self.e)
hm.add_mpint(self.f)
hm.add_mpint(K)
- H = SHA.new(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(self.transport.rng, H)
+ sig = self.transport.get_server_key().sign_ssh_data(H)
# send reply
m = Message()
m.add_byte(c_MSG_KEXDH_GEX_REPLY)
@@ -215,7 +217,7 @@ class KexGex (object):
m.add_string(sig)
self.transport._send_message(m)
self.transport._activate_outbound()
-
+
def _parse_kexdh_gex_reply(self, m):
host_key = m.get_string()
self.f = m.get_mpint()
@@ -238,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, SHA.new(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 3dfb7f18..9eee066c 100644
--- a/paramiko/kex_group1.py
+++ b/paramiko/kex_group1.py
@@ -21,7 +21,8 @@ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of
1024 bit key halves, using a known "p" prime and "g" generator.
"""
-from Crypto.Hash import SHA
+import os
+from hashlib import sha1
from paramiko import util
from paramiko.common import max_byte, zero_byte
@@ -33,17 +34,18 @@ from paramiko.ssh_exception import SSHException
_MSG_KEXDH_INIT, _MSG_KEXDH_REPLY = range(30, 32)
c_MSG_KEXDH_INIT, c_MSG_KEXDH_REPLY = [byte_chr(c) for c in range(30, 32)]
-# draft-ietf-secsh-transport-09.txt, page 17
-P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF
-G = 2
-
b7fffffffffffffff = byte_chr(0x7f) + max_byte * 7
b0000000000000000 = zero_byte * 8
class KexGroup1(object):
+ # draft-ietf-secsh-transport-09.txt, page 17
+ P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF
+ G = 2
+
name = 'diffie-hellman-group1-sha1'
+ hash_algo = sha1
def __init__(self, transport):
self.transport = transport
@@ -55,11 +57,11 @@ class KexGroup1(object):
self._generate_x()
if self.transport.server_mode:
# compute f = g^x mod p, but don't send it yet
- self.f = pow(G, self.x, P)
+ self.f = pow(self.G, self.x, self.P)
self.transport._expect_packet(_MSG_KEXDH_INIT)
return
# compute e = g^x mod p (where g=2), and send it
- self.e = pow(G, self.x, P)
+ self.e = pow(self.G, self.x, self.P)
m = Message()
m.add_byte(c_MSG_KEXDH_INIT)
m.add_mpint(self.e)
@@ -82,7 +84,7 @@ class KexGroup1(object):
# potential x where the first 63 bits are 1, because some of those will be
# 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] != b7fffffffffffffff and
x_bytes[:8] != b0000000000000000):
@@ -93,10 +95,10 @@ class KexGroup1(object):
# client mode
host_key = m.get_string()
self.f = m.get_mpint()
- if (self.f < 1) or (self.f > P - 1):
+ if (self.f < 1) or (self.f > self.P - 1):
raise SSHException('Server kex "f" is out of range')
sig = m.get_binary()
- K = pow(self.f, self.x, P)
+ K = pow(self.f, self.x, self.P)
# okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
hm = Message()
hm.add(self.transport.local_version, self.transport.remote_version,
@@ -105,16 +107,16 @@ class KexGroup1(object):
hm.add_mpint(self.e)
hm.add_mpint(self.f)
hm.add_mpint(K)
- self.transport._set_K_H(K, SHA.new(hm.asbytes()).digest())
+ self.transport._set_K_H(K, sha1(hm.asbytes()).digest())
self.transport._verify_key(host_key, sig)
self.transport._activate_outbound()
def _parse_kexdh_init(self, m):
# server mode
self.e = m.get_mpint()
- if (self.e < 1) or (self.e > P - 1):
+ if (self.e < 1) or (self.e > self.P - 1):
raise SSHException('Client kex "e" is out of range')
- K = pow(self.e, self.x, P)
+ K = pow(self.e, self.x, self.P)
key = self.transport.get_server_key().asbytes()
# okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
hm = Message()
@@ -124,10 +126,10 @@ class KexGroup1(object):
hm.add_mpint(self.e)
hm.add_mpint(self.f)
hm.add_mpint(K)
- H = SHA.new(hm.asbytes()).digest()
+ H = sha1(hm.asbytes()).digest()
self.transport._set_K_H(K, H)
# sign it
- sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H)
+ sig = self.transport.get_server_key().sign_ssh_data(H)
# send reply
m = Message()
m.add_byte(c_MSG_KEXDH_REPLY)
diff --git a/paramiko/kex_group14.py b/paramiko/kex_group14.py
new file mode 100644
index 00000000..9f7dd216
--- /dev/null
+++ b/paramiko/kex_group14.py
@@ -0,0 +1,35 @@
+# Copyright (C) 2013 Torsten Landschoff <torsten@debian.org>
+#
+# 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 distrubuted in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+"""
+Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of
+2048 bit key halves, using a known "p" prime and "g" generator.
+"""
+
+from paramiko.kex_group1 import KexGroup1
+from hashlib import sha1
+
+
+class KexGroup14(KexGroup1):
+
+ # http://tools.ietf.org/html/rfc3526#section-3
+ P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF
+ G = 2
+
+ name = 'diffie-hellman-group14-sha1'
+ hash_algo = sha1
diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py
new file mode 100644
index 00000000..69969f8a
--- /dev/null
+++ b/paramiko/kex_gss.py
@@ -0,0 +1,607 @@
+# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
+# Copyright (C) 2013-2014 science + computing ag
+# Author: Sebastian Deiss <sebastian.deiss@t-online.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.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+
+"""
+This module provides GSS-API / SSPI Key Exchange as defined in :rfc:`4462`.
+
+.. note:: Credential delegation is not supported in server mode.
+
+.. note::
+ `RFC 4462 Section 2.2
+ <https://tools.ietf.org/html/rfc4462.html#section-2.2>`_ says we are not
+ required to implement GSS-API error messages. Thus, in many methods within
+ this module, if an error occurs an exception will be thrown and the
+ connection will be terminated.
+
+.. seealso:: :doc:`/api/ssh_gss`
+
+.. versionadded:: 1.15
+"""
+
+import os
+from hashlib import sha1
+
+from paramiko.common import *
+from paramiko import util
+from paramiko.message import Message
+from paramiko.py3compat import byte_chr, long, byte_mask, byte_ord
+from paramiko.ssh_exception import SSHException
+
+
+MSG_KEXGSS_INIT, MSG_KEXGSS_CONTINUE, MSG_KEXGSS_COMPLETE, MSG_KEXGSS_HOSTKEY,\
+MSG_KEXGSS_ERROR = range(30, 35)
+MSG_KEXGSS_GROUPREQ, MSG_KEXGSS_GROUP = range(40, 42)
+c_MSG_KEXGSS_INIT, c_MSG_KEXGSS_CONTINUE, c_MSG_KEXGSS_COMPLETE,\
+c_MSG_KEXGSS_HOSTKEY, c_MSG_KEXGSS_ERROR = [byte_chr(c) for c in range(30, 35)]
+c_MSG_KEXGSS_GROUPREQ, c_MSG_KEXGSS_GROUP = [byte_chr(c) for c in range(40, 42)]
+
+
+class KexGSSGroup1(object):
+ """
+ GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange as defined in `RFC
+ 4462 Section 2 <https://tools.ietf.org/html/rfc4462.html#section-2>`_
+ """
+ # draft-ietf-secsh-transport-09.txt, page 17
+ P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF
+ G = 2
+ b7fffffffffffffff = byte_chr(0x7f) + max_byte * 7
+ b0000000000000000 = zero_byte * 8
+ NAME = "gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g=="
+
+ def __init__(self, transport):
+ self.transport = transport
+ self.kexgss = self.transport.kexgss_ctxt
+ self.gss_host = None
+ self.x = 0
+ self.e = 0
+ self.f = 0
+
+ def start_kex(self):
+ """
+ Start the GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange.
+ """
+ self.transport.gss_kex_used = True
+ self._generate_x()
+ if self.transport.server_mode:
+ # compute f = g^x mod p, but don't send it yet
+ self.f = pow(self.G, self.x, self.P)
+ self.transport._expect_packet(MSG_KEXGSS_INIT)
+ return
+ # compute e = g^x mod p (where g=2), and send it
+ self.e = pow(self.G, self.x, self.P)
+ # Initialize GSS-API Key Exchange
+ self.gss_host = self.transport.gss_host
+ m = Message()
+ m.add_byte(c_MSG_KEXGSS_INIT)
+ m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host))
+ m.add_mpint(self.e)
+ self.transport._send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_HOSTKEY,
+ MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE,
+ MSG_KEXGSS_ERROR)
+
+ def parse_next(self, ptype, m):
+ """
+ Parse the next packet.
+
+ :param char ptype: The type of the incomming packet
+ :param `.Message` m: The paket content
+ """
+ if self.transport.server_mode and (ptype == MSG_KEXGSS_INIT):
+ return self._parse_kexgss_init(m)
+ elif not self.transport.server_mode and (ptype == MSG_KEXGSS_HOSTKEY):
+ return self._parse_kexgss_hostkey(m)
+ elif self.transport.server_mode and (ptype == MSG_KEXGSS_CONTINUE):
+ return self._parse_kexgss_continue(m)
+ elif not self.transport.server_mode and (ptype == MSG_KEXGSS_COMPLETE):
+ return self._parse_kexgss_complete(m)
+ elif ptype == MSG_KEXGSS_ERROR:
+ return self._parse_kexgss_error(m)
+ raise SSHException('GSS KexGroup1 asked to handle packet type %d'
+ % ptype)
+
+ # ## internals...
+
+ def _generate_x(self):
+ """
+ generate an "x" (1 < x < q), where q is (p-1)/2.
+ p is a 128-byte (1024-bit) number, where the first 64 bits are 1.
+ therefore q can be approximated as a 2^1023. we drop the subset of
+ potential x where the first 63 bits are 1, because some of those will be
+ larger than q (but this is a tiny tiny subset of potential x).
+ """
+ while 1:
+ 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):
+ break
+ self.x = util.inflate_long(x_bytes)
+
+ def _parse_kexgss_hostkey(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_HOSTKEY message (client mode).
+
+ :param `.Message` m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message
+ """
+ # client mode
+ host_key = m.get_string()
+ self.transport.host_key = host_key
+ sig = m.get_string()
+ self.transport._verify_key(host_key, sig)
+ self.transport._expect_packet(MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE)
+
+ def _parse_kexgss_continue(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_CONTINUE message.
+
+ :param `.Message` m: The content of the SSH2_MSG_KEXGSS_CONTINUE message
+ """
+ if not self.transport.server_mode:
+ srv_token = m.get_string()
+ m = Message()
+ m.add_byte(c_MSG_KEXGSS_CONTINUE)
+ m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host,
+ recv_token=srv_token))
+ self.transport.send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE,
+ MSG_KEXGSS_ERROR)
+ else:
+ pass
+
+ def _parse_kexgss_complete(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_COMPLETE message (client mode).
+
+ :param `.Message` m: The content of the SSH2_MSG_KEXGSS_COMPLETE message
+ """
+ # client mode
+ if self.transport.host_key is None:
+ self.transport.host_key = NullHostKey()
+ self.f = m.get_mpint()
+ if (self.f < 1) or (self.f > self.P - 1):
+ raise SSHException('Server kex "f" is out of range')
+ mic_token = m.get_string()
+ # This must be TRUE, if there is a GSS-API token in this message.
+ bool = m.get_boolean()
+ srv_token = None
+ if bool:
+ srv_token = m.get_string()
+ K = pow(self.f, self.x, self.P)
+ # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
+ hm = Message()
+ hm.add(self.transport.local_version, self.transport.remote_version,
+ self.transport.local_kex_init, self.transport.remote_kex_init)
+ hm.add_string(self.transport.host_key.__str__())
+ hm.add_mpint(self.e)
+ hm.add_mpint(self.f)
+ hm.add_mpint(K)
+ self.transport._set_K_H(K, sha1(str(hm)).digest())
+ if srv_token is not None:
+ self.kexgss.ssh_init_sec_context(target=self.gss_host,
+ recv_token=srv_token)
+ self.kexgss.ssh_check_mic(mic_token,
+ self.transport.session_id)
+ else:
+ self.kexgss.ssh_check_mic(mic_token,
+ self.transport.session_id)
+ self.transport._activate_outbound()
+
+ def _parse_kexgss_init(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_INIT message (server mode).
+
+ :param `.Message` m: The content of the SSH2_MSG_KEXGSS_INIT message
+ """
+ # server mode
+ client_token = m.get_string()
+ self.e = m.get_mpint()
+ if (self.e < 1) or (self.e > self.P - 1):
+ raise SSHException('Client kex "e" is out of range')
+ K = pow(self.e, self.x, self.P)
+ self.transport.host_key = NullHostKey()
+ key = self.transport.host_key.__str__()
+ # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
+ hm = Message()
+ hm.add(self.transport.remote_version, self.transport.local_version,
+ self.transport.remote_kex_init, self.transport.local_kex_init)
+ hm.add_string(key)
+ hm.add_mpint(self.e)
+ hm.add_mpint(self.f)
+ hm.add_mpint(K)
+ H = sha1(hm.asbytes()).digest()
+ self.transport._set_K_H(K, H)
+ srv_token = self.kexgss.ssh_accept_sec_context(self.gss_host,
+ client_token)
+ m = Message()
+ if self.kexgss._gss_srv_ctxt_status:
+ mic_token = self.kexgss.ssh_get_mic(self.transport.session_id,
+ gss_kex=True)
+ m.add_byte(c_MSG_KEXGSS_COMPLETE)
+ m.add_mpint(self.f)
+ m.add_string(mic_token)
+ if srv_token is not None:
+ m.add_boolean(True)
+ m.add_string(srv_token)
+ else:
+ m.add_boolean(False)
+ self.transport._send_message(m)
+ self.transport._activate_outbound()
+ else:
+ m.add_byte(c_MSG_KEXGSS_CONTINUE)
+ m.add_string(srv_token)
+ self.transport._send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE,
+ MSG_KEXGSS_ERROR)
+
+ def _parse_kexgss_error(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_ERROR message (client mode).
+ The server may send a GSS-API error message. if it does, we display
+ the error by throwing an exception (client mode).
+
+ :param `.Message` m: The content of the SSH2_MSG_KEXGSS_ERROR message
+ :raise SSHException: Contains GSS-API major and minor status as well as
+ the error message and the language tag of the
+ message
+ """
+ maj_status = m.get_int()
+ min_status = m.get_int()
+ err_msg = m.get_string()
+ lang_tag = m.get_string() # we don't care about the language!
+ raise SSHException("GSS-API Error:\nMajor Status: %s\nMinor Status: %s\
+ \nError Message: %s\n") % (str(maj_status),
+ str(min_status),
+ err_msg)
+
+
+class KexGSSGroup14(KexGSSGroup1):
+ """
+ GSS-API / SSPI Authenticated Diffie-Hellman Group14 Key Exchange as defined
+ in `RFC 4462 Section 2
+ <https://tools.ietf.org/html/rfc4462.html#section-2>`_
+ """
+ P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF
+ G = 2
+ NAME = "gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g=="
+
+
+class KexGSSGex(object):
+ """
+ GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange as defined in
+ `RFC 4462 Section 2 <https://tools.ietf.org/html/rfc4462.html#section-2>`_
+ """
+ NAME = "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g=="
+ min_bits = 1024
+ max_bits = 8192
+ preferred_bits = 2048
+
+ def __init__(self, transport):
+ self.transport = transport
+ self.kexgss = self.transport.kexgss_ctxt
+ self.gss_host = None
+ self.p = None
+ self.q = None
+ self.g = None
+ self.x = None
+ self.e = None
+ self.f = None
+ self.old_style = False
+
+ def start_kex(self):
+ """
+ Start the GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange
+ """
+ self.transport.gss_kex_used = True
+ if self.transport.server_mode:
+ self.transport._expect_packet(MSG_KEXGSS_GROUPREQ)
+ return
+ # request a bit range: we accept (min_bits) to (max_bits), but prefer
+ # (preferred_bits). according to the spec, we shouldn't pull the
+ # minimum up above 1024.
+ self.gss_host = self.transport.gss_host
+ m = Message()
+ m.add_byte(c_MSG_KEXGSS_GROUPREQ)
+ m.add_int(self.min_bits)
+ m.add_int(self.preferred_bits)
+ m.add_int(self.max_bits)
+ self.transport._send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_GROUP)
+
+ def parse_next(self, ptype, m):
+ """
+ Parse the next packet.
+
+ :param char ptype: The type of the incomming packet
+ :param `.Message` m: The paket content
+ """
+ if ptype == MSG_KEXGSS_GROUPREQ:
+ return self._parse_kexgss_groupreq(m)
+ elif ptype == MSG_KEXGSS_GROUP:
+ return self._parse_kexgss_group(m)
+ elif ptype == MSG_KEXGSS_INIT:
+ return self._parse_kexgss_gex_init(m)
+ elif ptype == MSG_KEXGSS_HOSTKEY:
+ return self._parse_kexgss_hostkey(m)
+ elif ptype == MSG_KEXGSS_CONTINUE:
+ return self._parse_kexgss_continue(m)
+ elif ptype == MSG_KEXGSS_COMPLETE:
+ return self._parse_kexgss_complete(m)
+ elif ptype == MSG_KEXGSS_ERROR:
+ return self._parse_kexgss_error(m)
+ raise SSHException('KexGex asked to handle packet type %d' % ptype)
+
+ # ## internals...
+
+ def _generate_x(self):
+ # generate an "x" (1 < x < (p-1)/2).
+ q = (self.p - 1) // 2
+ qnorm = util.deflate_long(q, 0)
+ qhbyte = byte_ord(qnorm[0])
+ byte_count = len(qnorm)
+ qmask = 0xff
+ while not (qhbyte & 0x80):
+ qhbyte <<= 1
+ qmask >>= 1
+ while True:
+ 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):
+ break
+ self.x = x
+
+ def _parse_kexgss_groupreq(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_GROUPREQ message (server mode).
+
+ :param `.Message` m: The content of the SSH2_MSG_KEXGSS_GROUPREQ message
+ """
+ minbits = m.get_int()
+ preferredbits = m.get_int()
+ maxbits = m.get_int()
+ # smoosh the user's preferred size into our own limits
+ if preferredbits > self.max_bits:
+ preferredbits = self.max_bits
+ if preferredbits < self.min_bits:
+ preferredbits = self.min_bits
+ # fix min/max if they're inconsistent. technically, we could just pout
+ # and hang up, but there's no harm in giving them the benefit of the
+ # doubt and just picking a bitsize for them.
+ if minbits > preferredbits:
+ minbits = preferredbits
+ if maxbits < preferredbits:
+ maxbits = preferredbits
+ # now save a copy
+ self.min_bits = minbits
+ self.preferred_bits = preferredbits
+ self.max_bits = maxbits
+ # generate prime
+ pack = self.transport._get_modulus_pack()
+ if pack is None:
+ raise SSHException('Can\'t do server-side gex with no modulus pack')
+ self.transport._log(DEBUG, 'Picking p (%d <= %d <= %d bits)' % (minbits, preferredbits, maxbits))
+ self.g, self.p = pack.get_modulus(minbits, preferredbits, maxbits)
+ m = Message()
+ m.add_byte(c_MSG_KEXGSS_GROUP)
+ m.add_mpint(self.p)
+ m.add_mpint(self.g)
+ self.transport._send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_INIT)
+
+ def _parse_kexgss_group(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_GROUP message (client mode).
+
+ :param `Message` m: The content of the SSH2_MSG_KEXGSS_GROUP message
+ """
+ self.p = m.get_mpint()
+ self.g = m.get_mpint()
+ # reject if p's bit length < 1024 or > 8192
+ bitlen = util.bit_length(self.p)
+ if (bitlen < 1024) or (bitlen > 8192):
+ raise SSHException('Server-generated gex p (don\'t ask) is out of range (%d bits)' % bitlen)
+ self.transport._log(DEBUG, 'Got server p (%d bits)' % bitlen)
+ self._generate_x()
+ # now compute e = g^x mod p
+ self.e = pow(self.g, self.x, self.p)
+ m = Message()
+ m.add_byte(c_MSG_KEXGSS_INIT)
+ m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host))
+ m.add_mpint(self.e)
+ self.transport._send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_HOSTKEY,
+ MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE,
+ MSG_KEXGSS_ERROR)
+
+ def _parse_kexgss_gex_init(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_INIT message (server mode).
+
+ :param `Message` m: The content of the SSH2_MSG_KEXGSS_INIT message
+ """
+ client_token = m.get_string()
+ self.e = m.get_mpint()
+ if (self.e < 1) or (self.e > self.p - 1):
+ raise SSHException('Client kex "e" is out of range')
+ self._generate_x()
+ self.f = pow(self.g, self.x, self.p)
+ K = pow(self.e, self.x, self.p)
+ self.transport.host_key = NullHostKey()
+ key = self.transport.host_key.__str__()
+ # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K)
+ hm = Message()
+ hm.add(self.transport.remote_version, self.transport.local_version,
+ self.transport.remote_kex_init, self.transport.local_kex_init,
+ key)
+ hm.add_int(self.min_bits)
+ hm.add_int(self.preferred_bits)
+ hm.add_int(self.max_bits)
+ hm.add_mpint(self.p)
+ hm.add_mpint(self.g)
+ hm.add_mpint(self.e)
+ hm.add_mpint(self.f)
+ hm.add_mpint(K)
+ H = sha1(hm.asbytes()).digest()
+ self.transport._set_K_H(K, H)
+ srv_token = self.kexgss.ssh_accept_sec_context(self.gss_host,
+ client_token)
+ m = Message()
+ if self.kexgss._gss_srv_ctxt_status:
+ mic_token = self.kexgss.ssh_get_mic(self.transport.session_id,
+ gss_kex=True)
+ m.add_byte(c_MSG_KEXGSS_COMPLETE)
+ m.add_mpint(self.f)
+ m.add_string(mic_token)
+ if srv_token is not None:
+ m.add_boolean(True)
+ m.add_string(srv_token)
+ else:
+ m.add_boolean(False)
+ self.transport._send_message(m)
+ self.transport._activate_outbound()
+ else:
+ m.add_byte(c_MSG_KEXGSS_CONTINUE)
+ m.add_string(srv_token)
+ self.transport._send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE,
+ MSG_KEXGSS_ERROR)
+
+ def _parse_kexgss_hostkey(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_HOSTKEY message (client mode).
+
+ :param `Message` m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message
+ """
+ # client mode
+ host_key = m.get_string()
+ self.transport.host_key = host_key
+ sig = m.get_string()
+ self.transport._verify_key(host_key, sig)
+ self.transport._expect_packet(MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE)
+
+ def _parse_kexgss_continue(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_CONTINUE message.
+
+ :param `Message` m: The content of the SSH2_MSG_KEXGSS_CONTINUE message
+ """
+ if not self.transport.server_mode:
+ srv_token = m.get_string()
+ m = Message()
+ m.add_byte(c_MSG_KEXGSS_CONTINUE)
+ m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host,
+ recv_token=srv_token))
+ self.transport.send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE,
+ MSG_KEXGSS_ERROR)
+ else:
+ pass
+
+ def _parse_kexgss_complete(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_COMPLETE message (client mode).
+
+ :param `Message` m: The content of the SSH2_MSG_KEXGSS_COMPLETE message
+ """
+ if self.transport.host_key is None:
+ self.transport.host_key = NullHostKey()
+ self.f = m.get_mpint()
+ mic_token = m.get_string()
+ # This must be TRUE, if there is a GSS-API token in this message.
+ bool = m.get_boolean()
+ srv_token = None
+ if bool:
+ srv_token = m.get_string()
+ if (self.f < 1) or (self.f > self.p - 1):
+ raise SSHException('Server kex "f" is out of range')
+ K = pow(self.f, self.x, self.p)
+ # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K)
+ hm = Message()
+ hm.add(self.transport.local_version, self.transport.remote_version,
+ self.transport.local_kex_init, self.transport.remote_kex_init,
+ self.transport.host_key.__str__())
+ if not self.old_style:
+ hm.add_int(self.min_bits)
+ hm.add_int(self.preferred_bits)
+ if not self.old_style:
+ hm.add_int(self.max_bits)
+ hm.add_mpint(self.p)
+ hm.add_mpint(self.g)
+ hm.add_mpint(self.e)
+ hm.add_mpint(self.f)
+ hm.add_mpint(K)
+ H = sha1(hm.asbytes()).digest()
+ self.transport._set_K_H(K, H)
+ if srv_token is not None:
+ self.kexgss.ssh_init_sec_context(target=self.gss_host,
+ recv_token=srv_token)
+ self.kexgss.ssh_check_mic(mic_token,
+ self.transport.session_id)
+ else:
+ self.kexgss.ssh_check_mic(mic_token,
+ self.transport.session_id)
+ self.transport._activate_outbound()
+
+ def _parse_kexgss_error(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_ERROR message (client mode).
+ The server may send a GSS-API error message. if it does, we display
+ the error by throwing an exception (client mode).
+
+ :param `Message` m: The content of the SSH2_MSG_KEXGSS_ERROR message
+ :raise SSHException: Contains GSS-API major and minor status as well as
+ the error message and the language tag of the
+ message
+ """
+ maj_status = m.get_int()
+ min_status = m.get_int()
+ err_msg = m.get_string()
+ lang_tag = m.get_string() # we don't care about the language!
+ raise SSHException("GSS-API Error:\nMajor Status: %s\nMinor Status: %s\
+ \nError Message: %s\n") % (str(maj_status),
+ str(min_status),
+ err_msg)
+
+
+class NullHostKey(object):
+ """
+ This class represents the Null Host Key for GSS-API Key Exchange as defined
+ in `RFC 4462 Section 5
+ <https://tools.ietf.org/html/rfc4462.html#section-5>`_
+ """
+ def __init__(self):
+ self.key = ""
+
+ def __str__(self):
+ return self.key
+
+ def get_name(self):
+ return self.key
diff --git a/paramiko/message.py b/paramiko/message.py
index da6acf8e..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,7 +141,7 @@ class Message (object):
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.
@@ -172,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):
"""
@@ -183,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):
@@ -195,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):
"""
@@ -235,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.
@@ -244,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.
@@ -283,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
@@ -302,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 0f51df5e..2be2bb2b 100644
--- a/paramiko/packet.py
+++ b/paramiko/packet.py
@@ -21,25 +21,21 @@ Packet handling
"""
import errno
+import os
import socket
import struct
import threading
import time
+from hmac import HMAC
from paramiko import util
from paramiko.common import linefeed_byte, cr_byte_value, asbytes, MSG_NAMES, \
- DEBUG, xffffffff, zero_byte, rng
+ DEBUG, xffffffff, zero_byte
from paramiko.py3compat import u, byte_ord
from paramiko.ssh_exception import SSHException, ProxyCommandFailure
from paramiko.message import Message
-try:
- from r_hmac import HMAC
-except ImportError:
- from Crypto.Hash.HMAC import HMAC
-
-
def compute_hmac(key, message, digest_class):
return HMAC(key, message, digest_class).digest()
@@ -103,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.
@@ -186,6 +186,45 @@ 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 time out 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.
@@ -204,6 +243,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:
@@ -235,6 +276,7 @@ class Packetizer (object):
def write_all(self, out):
self.__keepalive_last = time.time()
+ iteration_with_zero_as_return_value = 0
while len(out) > 0:
retry_write = False
try:
@@ -258,6 +300,15 @@ class Packetizer (object):
n = 0
if self.__closed:
n = -1
+ else:
+ if n == 0 and iteration_with_zero_as_return_value > 10:
+ # We shouldn't retry the write, but we didn't
+ # manage to send anything over the socket. This might be an
+ # indication that we have lost contact with the remote side,
+ # but are yet to receive an EOFError or other socket errors.
+ # Let's give it some iteration to try and catch up.
+ n = -1
+ iteration_with_zero_as_return_value += 1
if n < 0:
raise EOFError()
if n == len(out):
@@ -338,7 +389,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')
@@ -359,7 +411,7 @@ class Packetizer (object):
raise SSHException('Mismatched MAC')
padding = byte_ord(packet[0])
payload = packet[1:packet_size - padding]
-
+
if self.__dump_packets:
self._log(DEBUG, 'Got payload (%d bytes, %d padding)' % (packet_size, padding))
@@ -455,7 +507,7 @@ class Packetizer (object):
# don't waste random bytes for the padding
packet += (zero_byte * padding)
else:
- packet += rng.read(padding)
+ packet += os.urandom(padding)
return packet
def _trigger_rekey(self):
diff --git a/paramiko/pipe.py b/paramiko/pipe.py
index b0cfcf24..4f62d7c5 100644
--- a/paramiko/pipe.py
+++ b/paramiko/pipe.py
@@ -28,6 +28,7 @@ will trigger as readable in `select <select.select>`.
import sys
import os
import socket
+from paramiko.py3compat import b
def make_pipe():
diff --git a/paramiko/pkey.py b/paramiko/pkey.py
index c8f84e0a..e95d60ba 100644
--- a/paramiko/pkey.py
+++ b/paramiko/pkey.py
@@ -23,12 +23,12 @@ Common API for all public keys.
import base64
from binascii import hexlify, unhexlify
import os
+from hashlib import md5
-from Crypto.Hash import MD5
from Crypto.Cipher import DES3, AES
from paramiko import util
-from paramiko.common import o600, rng, zero_byte
+from paramiko.common import o600, zero_byte
from paramiko.py3compat import u, encodebytes, decodebytes, b
from paramiko.ssh_exception import SSHException, PasswordRequiredException
@@ -126,7 +126,7 @@ class PKey (object):
a 16-byte `string <str>` (binary) of the MD5 fingerprint, in SSH
format.
"""
- return MD5.new(self.asbytes()).digest()
+ return md5(self.asbytes()).digest()
def get_base64(self):
"""
@@ -138,12 +138,11 @@ class PKey (object):
"""
return u(encodebytes(self.asbytes())).replace('\n', '')
- def sign_ssh_data(self, rng, data):
+ def sign_ssh_data(self, data):
"""
Sign a blob of data with this private key, and return a `.Message`
representing an SSH signature message.
- :param .Crypto.Util.rng.RandomPool rng: a secure random number generator.
:param str data: the data to sign.
:return: an SSH signature `message <.Message>`.
"""
@@ -161,6 +160,7 @@ class PKey (object):
"""
return False
+ @classmethod
def from_private_key_file(cls, filename, password=None):
"""
Create a key object by reading a private key file. If the private
@@ -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
@@ -182,28 +183,27 @@ class PKey (object):
"""
key = cls(filename=filename, password=password)
return key
- from_private_key_file = classmethod(from_private_key_file)
+ @classmethod
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)
return key
- from_private_key = classmethod(from_private_key)
def write_private_key_file(self, filename, password=None):
"""
@@ -224,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
@@ -274,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:
@@ -300,7 +300,7 @@ class PKey (object):
keysize = self._CIPHER_TABLE[encryption_type]['keysize']
mode = self._CIPHER_TABLE[encryption_type]['mode']
salt = unhexlify(b(saltstr))
- key = util.generate_key_bytes(MD5, salt, password, keysize)
+ key = util.generate_key_bytes(md5, salt, password, keysize)
return cipher.new(key, mode, salt).decrypt(data)
def _write_private_key_file(self, tag, filename, data, password=None):
@@ -310,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.
@@ -325,17 +326,16 @@ class PKey (object):
def _write_private_key(self, tag, f, data, password=None):
f.write('-----BEGIN %s PRIVATE KEY-----\n' % tag)
if password is not None:
- # since we only support one cipher here, use it
cipher_name = list(self._CIPHER_TABLE.keys())[0]
cipher = self._CIPHER_TABLE[cipher_name]['cipher']
keysize = self._CIPHER_TABLE[cipher_name]['keysize']
blocksize = self._CIPHER_TABLE[cipher_name]['blocksize']
mode = self._CIPHER_TABLE[cipher_name]['mode']
- salt = rng.read(16)
- key = util.generate_key_bytes(MD5, salt, password, keysize)
+ salt = os.urandom(blocksize)
+ key = util.generate_key_bytes(md5, salt, password, keysize)
if len(data) % blocksize != 0:
n = blocksize - len(data) % blocksize
- #data += rng.read(n)
+ #data += os.urandom(n)
# that would make more sense ^, but it confuses openssh.
data += zero_byte * n
data = cipher.new(key, mode, salt).encrypt(data)
diff --git a/paramiko/primes.py b/paramiko/primes.py
index 58d158c8..7415c182 100644
--- a/paramiko/primes.py
+++ b/paramiko/primes.py
@@ -20,32 +20,15 @@
Utility functions for dealing with primes.
"""
-from Crypto.Util import number
+import os
from paramiko import util
from paramiko.py3compat import byte_mask, long
from paramiko.ssh_exception import SSHException
+from paramiko.common import *
-def _generate_prime(bits, rng):
- """primtive attempt at prime generation"""
- hbyte_mask = pow(2, bits % 8) - 1
- while True:
- # loop catches the case where we increment n into a higher bit-range
- x = rng.read((bits + 7) // 8)
- if hbyte_mask > 0:
- x = byte_mask(x[0], hbyte_mask) + x[1:]
- n = util.inflate_long(x, 1)
- n |= 1
- n |= (1 << (bits - 1))
- while not number.isPrime(n):
- n += 2
- if util.bit_length(n) == bits:
- break
- return n
-
-
-def _roll_random(rng, n):
+def _roll_random(n):
"""returns a random # from 0 to N-1"""
bits = util.bit_length(n - 1)
byte_count = (bits + 7) // 8
@@ -58,7 +41,7 @@ def _roll_random(rng, n):
# fits, so i can't guarantee that this loop will ever finish, but the odds
# of it looping forever should be infinitesimal.
while True:
- x = rng.read(byte_count)
+ x = os.urandom(byte_count)
if hbyte_mask > 0:
x = byte_mask(x[0], hbyte_mask) + x[1:]
num = util.inflate_long(x, 1)
@@ -73,11 +56,10 @@ class ModulusPack (object):
on systems that have such a file.
"""
- def __init__(self, rpool):
+ def __init__(self):
# pack is a hash of: bits -> [ (generator, modulus) ... ]
self.pack = {}
self.discarded = []
- self.rng = rpool
def _parse_modulus(self, line):
timestamp, mod_type, tests, tries, size, generator, modulus = line.split()
@@ -147,5 +129,5 @@ class ModulusPack (object):
if min > good:
good = bitsizes[-1]
# now pick a random modulus of this bitsize
- n = _roll_random(self.rng, len(self.pack[good]))
+ n = _roll_random(len(self.pack[good]))
return self.pack[good][n]
diff --git a/paramiko/proxy.py b/paramiko/proxy.py
index 25666be5..ca602c4c 100644
--- a/paramiko/proxy.py
+++ b/paramiko/proxy.py
@@ -27,9 +27,10 @@ import socket
import time
from paramiko.ssh_exception import ProxyCommandFailure
+from paramiko.util import ClosingContextManager
-class ProxyCommand(object):
+class ProxyCommand(ClosingContextManager):
"""
Wraps a subprocess running ProxyCommand-driven programs.
@@ -37,6 +38,8 @@ class ProxyCommand(object):
`.Transport` and `.Packetizer` classes. Using this class instead of a
regular socket makes it possible to talk with a Popen'd command that will
proxy traffic between the client and a server hosted in another machine.
+
+ Instances of this class may be used as context managers.
"""
def __init__(self, command_line):
"""
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/rsakey.py b/paramiko/rsakey.py
index c93f3218..4ebd8354 100644
--- a/paramiko/rsakey.py
+++ b/paramiko/rsakey.py
@@ -20,11 +20,13 @@
RSA keys.
"""
+import os
+from hashlib import sha1
+
from Crypto.PublicKey import RSA
-from Crypto.Hash import SHA
from paramiko import util
-from paramiko.common import rng, max_byte, zero_byte, one_byte
+from paramiko.common import max_byte, zero_byte, one_byte
from paramiko.message import Message
from paramiko.ber import BER, BERException
from paramiko.pkey import PKey
@@ -90,8 +92,8 @@ class RSAKey (PKey):
def can_sign(self):
return self.d is not None
- def sign_ssh_data(self, rpool, data):
- digest = SHA.new(data).digest()
+ def sign_ssh_data(self, data):
+ digest = sha1(data).digest()
rsa = RSA.construct((long(self.n), long(self.e), long(self.d)))
sig = util.deflate_long(rsa.sign(self._pkcs1imify(digest), bytes())[0], 0)
m = Message()
@@ -106,7 +108,7 @@ class RSAKey (PKey):
# verify the signature by SHA'ing the data and encrypting it using the
# public key. some wackiness ensues where we "pkcs1imify" the 20-byte
# hash into a string as long as the RSA key.
- hash_obj = util.inflate_long(self._pkcs1imify(SHA.new(data).digest()), True)
+ hash_obj = util.inflate_long(self._pkcs1imify(sha1(data).digest()), True)
rsa = RSA.construct((long(self.n), long(self.e)))
return rsa.verify(hash_obj, (sig,))
@@ -125,10 +127,11 @@ class RSAKey (PKey):
def write_private_key_file(self, filename, password=None):
self._write_private_key_file('RSA', filename, self._encode_key(), password)
-
+
def write_private_key(self, file_obj, password=None):
self._write_private_key('RSA', file_obj, self._encode_key(), password)
+ @staticmethod
def generate(bits, progress_func=None):
"""
Generate a new private RSA key. This factory function can be used to
@@ -140,13 +143,12 @@ class RSAKey (PKey):
by ``pyCrypto.PublicKey``).
:return: new `.RSAKey` private key
"""
- rsa = RSA.generate(bits, rng.read, progress_func)
+ rsa = RSA.generate(bits, os.urandom, progress_func)
key = RSAKey(vals=(rsa.e, rsa.n))
key.d = rsa.d
key.p = rsa.p
key.q = rsa.q
return key
- generate = staticmethod(generate)
### internals...
@@ -162,11 +164,11 @@ class RSAKey (PKey):
def _from_private_key_file(self, filename, password):
data = self._read_private_key_file('RSA', filename, password)
self._decode_key(data)
-
+
def _from_private_key(self, file_obj, password):
data = self._read_private_key('RSA', file_obj, password)
self._decode_key(data)
-
+
def _decode_key(self, data):
# private key file contains:
# RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p }
diff --git a/paramiko/server.py b/paramiko/server.py
index 2f630dfb..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
@@ -229,6 +229,80 @@ class ServerInterface (object):
:rtype: int or `.InteractiveQuery`
"""
return AUTH_FAILED
+
+ def check_auth_gssapi_with_mic(self, username,
+ gss_authenticated=AUTH_FAILED,
+ cc_file=None):
+ """
+ Authenticate the given user to the server if he is a valid krb5
+ principal.
+
+ :param str username: The username of the authenticating client
+ :param int gss_authenticated: The result of the krb5 authentication
+ :param str cc_filename: The krb5 client credentials cache filename
+ :return: `.AUTH_FAILED` if the user is not authenticated otherwise
+ `.AUTH_SUCCESSFUL`
+ :rtype: int
+ :note: Kerberos credential delegation is not supported.
+ :see: `.ssh_gss`
+ :note: : We are just checking in L{AuthHandler} that the given user is
+ a valid krb5 principal!
+ We don't check if the krb5 principal is allowed to log in on
+ the server, because there is no way to do that in python. So
+ if you develop your own SSH server with paramiko for a cetain
+ plattform like Linux, you should call C{krb5_kuserok()} in your
+ local kerberos library to make sure that the krb5_principal has
+ an account on the server and is allowed to log in as a user.
+ :see: `http://www.unix.com/man-page/all/3/krb5_kuserok/`
+ """
+ if gss_authenticated == AUTH_SUCCESSFUL:
+ return AUTH_SUCCESSFUL
+ return AUTH_FAILED
+
+ def check_auth_gssapi_keyex(self, username,
+ gss_authenticated=AUTH_FAILED,
+ cc_file=None):
+ """
+ Authenticate the given user to the server if he is a valid krb5
+ principal and GSS-API Key Exchange was performed.
+ If GSS-API Key Exchange was not performed, this authentication method
+ won't be available.
+
+ :param str username: The username of the authenticating client
+ :param int gss_authenticated: The result of the krb5 authentication
+ :param str cc_filename: The krb5 client credentials cache filename
+ :return: `.AUTH_FAILED` if the user is not authenticated otherwise
+ `.AUTH_SUCCESSFUL`
+ :rtype: int
+ :note: Kerberos credential delegation is not supported.
+ :see: `.ssh_gss` `.kex_gss`
+ :note: : We are just checking in L{AuthHandler} that the given user is
+ a valid krb5 principal!
+ We don't check if the krb5 principal is allowed to log in on
+ the server, because there is no way to do that in python. So
+ if you develop your own SSH server with paramiko for a cetain
+ plattform like Linux, you should call C{krb5_kuserok()} in your
+ local kerberos library to make sure that the krb5_principal has
+ an account on the server and is allowed to log in as a user.
+ :see: `http://www.unix.com/man-page/all/3/krb5_kuserok/`
+ """
+ if gss_authenticated == AUTH_SUCCESSFUL:
+ return AUTH_SUCCESSFUL
+ return AUTH_FAILED
+
+ def enable_auth_gssapi(self):
+ """
+ Overwrite this function in your SSH server to enable GSSAPI
+ authentication.
+ The default implementation always returns false.
+
+ :return: True if GSSAPI authentication is enabled otherwise false
+ :rtype: Boolean
+ :see: : `.ssh_gss`
+ """
+ UseGSSAPI = False
+ GSSAPICleanupCredentials = False
+ return UseGSSAPI
def check_port_forward_request(self, address, port):
"""
diff --git a/paramiko/sftp_attr.py b/paramiko/sftp_attr.py
index d12eff8d..0eaca30b 100644
--- a/paramiko/sftp_attr.py
+++ b/paramiko/sftp_attr.py
@@ -60,6 +60,7 @@ class SFTPAttributes (object):
self.st_mtime = None
self.attr = {}
+ @classmethod
def from_stat(cls, obj, filename=None):
"""
Create an `.SFTPAttributes` object from an existing ``stat`` object (an
@@ -79,13 +80,12 @@ class SFTPAttributes (object):
if filename is not None:
attr.filename = filename
return attr
- from_stat = classmethod(from_stat)
def __repr__(self):
return '<SFTPAttributes: %s>' % self._debug_str()
### internals...
-
+ @classmethod
def _from_msg(cls, msg, filename=None, longname=None):
attr = cls()
attr._unpack(msg)
@@ -94,7 +94,6 @@ class SFTPAttributes (object):
if longname is not None:
attr.longname = longname
return attr
- _from_msg = classmethod(_from_msg)
def _unpack(self, msg):
self._flags = msg.get_int()
@@ -159,6 +158,7 @@ class SFTPAttributes (object):
out += ']'
return out
+ @staticmethod
def _rwx(n, suid, sticky=False):
if suid:
suid = 2
@@ -168,7 +168,6 @@ class SFTPAttributes (object):
else:
out += '-xSs'[suid + (n & 1)]
return out
- _rwx = staticmethod(_rwx)
def __str__(self):
"""create a unix-style long description of the file (like ls -l)"""
@@ -210,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 99a29e36..55302ffd 100644
--- a/paramiko/sftp_client.py
+++ b/paramiko/sftp_client.py
@@ -39,6 +39,7 @@ from paramiko.sftp import BaseSFTP, CMD_OPENDIR, CMD_HANDLE, SFTPError, CMD_READ
from paramiko.sftp_attr import SFTPAttributes
from paramiko.ssh_exception import SSHException
from paramiko.sftp_file import SFTPFile
+from paramiko.util import ClosingContextManager
def _to_unicode(s):
@@ -58,12 +59,14 @@ def _to_unicode(s):
b_slash = b'/'
-class SFTPClient(BaseSFTP):
+class SFTPClient(BaseSFTP, ClosingContextManager):
"""
SFTP client object.
-
+
Used to open an SFTP session across an open SSH `.Transport` and perform
remote file operations.
+
+ Instances of this class may be used as context managers.
"""
def __init__(self, sock):
"""
@@ -98,21 +101,35 @@ class SFTPClient(BaseSFTP):
raise SSHException('EOF during negotiation')
self._log(INFO, 'Opened sftp connection (server version %d)' % server_version)
- def from_transport(cls, t):
+ @classmethod
+ def from_transport(cls, t, window_size=None, max_packet_size=None):
"""
Create an SFTP client channel from an open `.Transport`.
+ Setting the window and packet sizes might affect the transfer speed.
+ The default settings in the `.Transport` class are the same as in
+ OpenSSH and should work adequately for both files transfers and
+ interactive sessions.
+
:param .Transport t: an open `.Transport` which is already authenticated
+ :param int window_size:
+ optional window size for the `.SFTPClient` session.
+ :param int max_packet_size:
+ optional max packet size for the `.SFTPClient` session..
+
:return:
a new `.SFTPClient` object, referring to an sftp session (channel)
across the transport
+
+ .. versionchanged:: 1.15
+ Added the ``window_size`` and ``max_packet_size`` arguments.
"""
- chan = t.open_session()
+ chan = t.open_session(window_size=window_size,
+ max_packet_size=max_packet_size)
if chan is None:
return None
chan.invoke_subsystem('sftp')
return cls(chan)
- from_transport = classmethod(from_transport)
def _log(self, level, msg, *args):
if isinstance(msg, list):
@@ -196,6 +213,71 @@ class SFTPClient(BaseSFTP):
self._request(CMD_CLOSE, handle)
return filelist
+ def listdir_iter(self, path='.', read_aheads=50):
+ """
+ Generator version of `.listdir_attr`.
+
+ See the API docs for `.listdir_attr` for overall details.
+
+ This function adds one more kwarg on top of `.listdir_attr`:
+ ``read_aheads``, an integer controlling how many
+ ``SSH_FXP_READDIR`` requests are made to the server. The default of 50
+ should suffice for most file listings as each request/response cycle
+ may contain multiple files (dependant on server implementation.)
+
+ .. versionadded:: 1.15
+ """
+ path = self._adjust_cwd(path)
+ self._log(DEBUG, 'listdir(%r)' % path)
+ t, msg = self._request(CMD_OPENDIR, path)
+
+ if t != CMD_HANDLE:
+ raise SFTPError('Expected handle')
+
+ handle = msg.get_string()
+
+ nums = list()
+ while True:
+ try:
+ # Send out a bunch of readdir requests so that we can read the
+ # responses later on Section 6.7 of the SSH file transfer RFC
+ # explains this
+ # http://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
+ for i in range(read_aheads):
+ num = self._async_request(type(None), CMD_READDIR, handle)
+ nums.append(num)
+
+
+ # For each of our sent requests
+ # Read and parse the corresponding packets
+ # If we're at the end of our queued requests, then fire off
+ # some more requests
+ # Exit the loop when we've reached the end of the directory
+ # handle
+ for num in nums:
+ t, pkt_data = self._read_packet()
+ msg = Message(pkt_data)
+ new_num = msg.get_int()
+ if num == new_num:
+ if t == CMD_STATUS:
+ self._convert_status(msg)
+ count = msg.get_int()
+ for i in range(count):
+ filename = msg.get_text()
+ longname = msg.get_text()
+ attr = SFTPAttributes._from_msg(
+ msg, filename, longname)
+ if (filename != '.') and (filename != '..'):
+ yield attr
+
+ # If we've hit the end of our queued requests, reset nums.
+ nums = list()
+
+ except EOFError:
+ self._request(CMD_CLOSE, handle)
+ return
+
+
def open(self, filename, mode='r', bufsize=-1):
"""
Open a file on the remote server. The arguments are the same as for
@@ -507,6 +589,7 @@ class SFTPClient(BaseSFTP):
.. versionadded:: 1.4
"""
+ # TODO: make class initialize with self._cwd set to self.normalize('.')
return self._cwd and u(self._cwd)
def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True):
@@ -517,7 +600,7 @@ class SFTPClient(BaseSFTP):
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,
@@ -578,7 +661,7 @@ class SFTPClient(BaseSFTP):
.. versionadded:: 1.4
.. versionchanged:: 1.7.4
- ``callback`` and rich attribute return value added.
+ ``callback`` and rich attribute return value added.
.. versionchanged:: 1.7.7
``confirm`` param added.
"""
diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py
index 03d67b33..d0a37da3 100644
--- a/paramiko/sftp_file.py
+++ b/paramiko/sftp_file.py
@@ -488,9 +488,3 @@ class SFTPFile (BufferedFile):
x = self._saved_exception
self._saved_exception = None
raise x
-
- def __enter__(self):
- return self
-
- def __exit__(self, type, value, traceback):
- self.close()
diff --git a/paramiko/sftp_handle.py b/paramiko/sftp_handle.py
index 92dd9cfe..edceb5ad 100644
--- a/paramiko/sftp_handle.py
+++ b/paramiko/sftp_handle.py
@@ -22,9 +22,10 @@ Abstraction of an SFTP file handle (for server mode).
import os
from paramiko.sftp import SFTP_OP_UNSUPPORTED, SFTP_OK
+from paramiko.util import ClosingContextManager
-class SFTPHandle (object):
+class SFTPHandle (ClosingContextManager):
"""
Abstract object representing a handle to an open file (or folder) in an
SFTP server implementation. Each handle has a string representation used
@@ -32,6 +33,8 @@ class SFTPHandle (object):
Server implementations can (and should) subclass SFTPHandle to implement
features of a file handle, like `stat` or `chattr`.
+
+ Instances of this class may be used as context managers.
"""
def __init__(self, flags=0):
"""
diff --git a/paramiko/sftp_server.py b/paramiko/sftp_server.py
index dadfd026..ce287e8f 100644
--- a/paramiko/sftp_server.py
+++ b/paramiko/sftp_server.py
@@ -22,9 +22,9 @@ Server-mode SFTP support.
import os
import errno
-
-from Crypto.Hash import MD5, SHA
import sys
+from hashlib import md5, sha1
+
from paramiko import util
from paramiko.sftp import BaseSFTP, Message, SFTP_FAILURE, \
SFTP_PERMISSION_DENIED, SFTP_NO_SUCH_FILE
@@ -45,8 +45,8 @@ from paramiko.sftp import CMD_HANDLE, SFTP_DESC, CMD_STATUS, SFTP_EOF, CMD_NAME,
CMD_READLINK, CMD_SYMLINK, CMD_REALPATH, CMD_EXTENDED, SFTP_OP_UNSUPPORTED
_hash_class = {
- 'sha1': SHA,
- 'md5': MD5,
+ 'sha1': sha1,
+ 'md5': md5,
}
@@ -82,14 +82,14 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
self.file_table = {}
self.folder_table = {}
self.server = sftp_si(server, *largs, **kwargs)
-
+
def _log(self, level, msg):
if issubclass(type(msg), list):
for m in msg:
super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + m)
else:
super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + msg)
-
+
def start_subsystem(self, name, transport, channel):
self.sock = channel
self._log(DEBUG, 'Started sftp server on channel %s' % repr(channel))
@@ -129,6 +129,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
self.file_table = {}
self.folder_table = {}
+ @staticmethod
def convert_errno(e):
"""
Convert an errno value (as from an ``OSError`` or ``IOError``) into a
@@ -146,8 +147,8 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
return SFTP_NO_SUCH_FILE
else:
return SFTP_FAILURE
- convert_errno = staticmethod(convert_errno)
+ @staticmethod
def set_file_attr(filename, attr):
"""
Change a file's attributes on the local filesystem. The contents of
@@ -157,7 +158,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
This is meant to be a handy helper function for translating SFTP file
requests into local file operations.
-
+
:param str filename:
name of the file to alter (should usually be an absolute path).
:param .SFTPAttributes attr: attributes to change.
@@ -173,7 +174,6 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
if attr._flags & attr.FLAG_SIZE:
with open(filename, 'w+') as f:
f.truncate(attr.st_size)
- set_file_attr = staticmethod(set_file_attr)
### internals...
@@ -281,7 +281,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
# don't try to read more than about 64KB at a time
chunklen = min(blocklen, 65536)
count = 0
- hash_obj = alg.new()
+ hash_obj = alg()
while count < blocklen:
data = f.read(offset, chunklen)
if not isinstance(data, bytes_types):
@@ -298,7 +298,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
msg.add_string(algname)
msg.add_bytes(sum_out)
self._send_packet(CMD_EXTENDED_REPLY, msg)
-
+
def _convert_pflags(self, pflags):
"""convert SFTP-style open() flags to Python's os.open() flags"""
if (pflags & SFTP_FLAG_READ) and (pflags & SFTP_FLAG_WRITE):
diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py
index b99e42b3..02f3e52e 100644
--- a/paramiko/ssh_exception.py
+++ b/paramiko/ssh_exception.py
@@ -16,6 +16,8 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+import socket
+
class SSHException (Exception):
"""
@@ -105,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
@@ -129,3 +135,39 @@ class ProxyCommandFailure (SSHException):
self.error = error
# for unpickling
self.args = (command, error, )
+
+
+class NoValidConnectionsError(socket.error):
+ """
+ Multiple connection attempts were made and no families succeeded.
+
+ This exception class wraps multiple "real" underlying connection errors,
+ all of which represent failed connection attempts. Because these errors are
+ not guaranteed to all be of the same error type (i.e. different errno,
+ `socket.error` subclass, message, etc) we expose a single unified error
+ message and a ``None`` errno so that instances of this class match most
+ normal handling of `socket.error` objects.
+
+ To see the wrapped exception objects, access the ``errors`` attribute.
+ ``errors`` is a dict whose keys are address tuples (e.g. ``('127.0.0.1',
+ 22)``) and whose values are the exception encountered trying to connect to
+ that address.
+
+ 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).
+ """
+ def __init__(self, errors):
+ """
+ :param dict errors:
+ The errors dict to store, as described by class docstring.
+ """
+ addrs = errors.keys()
+ body = ', '.join([x[0] for x in addrs[:-1]])
+ tail = addrs[-1][0]
+ msg = "Unable to connect to port {0} on {1} or {2}"
+ super(NoValidConnectionsError, self).__init__(
+ None, # stand-in for errno
+ msg.format(addrs[0][1], body, tail)
+ )
+ self.errors = errors
diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py
new file mode 100644
index 00000000..e9b13a66
--- /dev/null
+++ b/paramiko/ssh_gss.py
@@ -0,0 +1,578 @@
+# Copyright (C) 2013-2014 science + computing ag
+# Author: Sebastian Deiss <sebastian.deiss@t-online.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.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+
+"""
+This module provides GSS-API / SSPI authentication as defined in :rfc:`4462`.
+
+.. note:: Credential delegation is not supported in server mode.
+
+.. seealso:: :doc:`/api/kex_gss`
+
+.. versionadded:: 1.15
+"""
+
+import struct
+import os
+import sys
+
+"""
+:var bool GSS_AUTH_AVAILABLE:
+ Constraint that indicates if GSS-API / SSPI is available.
+"""
+GSS_AUTH_AVAILABLE = True
+
+try:
+ from pyasn1.type.univ import ObjectIdentifier
+ from pyasn1.codec.der import encoder, decoder
+except ImportError:
+ GSS_AUTH_AVAILABLE = False
+ class ObjectIdentifier(object):
+ def __init__(self, *args):
+ raise NotImplementedError("Module pyasn1 not importable")
+
+ class decoder(object):
+ def decode(self):
+ raise NotImplementedError("Module pyasn1 not importable")
+
+ class encoder(object):
+ def encode(self):
+ raise NotImplementedError("Module pyasn1 not importable")
+
+from paramiko.common import MSG_USERAUTH_REQUEST
+from paramiko.ssh_exception import SSHException
+
+"""
+:var str _API: Constraint for the used API
+"""
+_API = "MIT"
+
+try:
+ import gssapi
+except (ImportError, OSError):
+ try:
+ import sspicon
+ import sspi
+ _API = "SSPI"
+ except ImportError:
+ GSS_AUTH_AVAILABLE = False
+ _API = None
+
+
+def GSSAuth(auth_method, gss_deleg_creds=True):
+ """
+ Provide SSH2 GSS-API / SSPI authentication.
+
+ :param str auth_method: The name of the SSH authentication mechanism
+ (gssapi-with-mic or gss-keyex)
+ :param bool gss_deleg_creds: Delegate client credentials or not.
+ We delegate credentials by default.
+ :return: Either an `._SSH_GSSAPI` (Unix) object or an
+ `_SSH_SSPI` (Windows) object
+ :rtype: Object
+
+ :raise ImportError: If no GSS-API / SSPI module could be imported.
+
+ :see: `RFC 4462 <http://www.ietf.org/rfc/rfc4462.txt>`_
+ :note: Check for the available API and return either an `._SSH_GSSAPI`
+ (MIT GSSAPI) object or an `._SSH_SSPI` (MS SSPI) object. If you
+ get python-gssapi working on Windows, python-gssapi
+ will be used and a `._SSH_GSSAPI` object will be returned.
+ If there is no supported API available,
+ ``None`` will be returned.
+ """
+ if _API == "MIT":
+ return _SSH_GSSAPI(auth_method, gss_deleg_creds)
+ elif _API == "SSPI" and os.name == "nt":
+ return _SSH_SSPI(auth_method, gss_deleg_creds)
+ else:
+ raise ImportError("Unable to import a GSS-API / SSPI module!")
+
+
+class _SSH_GSSAuth(object):
+ """
+ Contains the shared variables and methods of `._SSH_GSSAPI` and
+ `._SSH_SSPI`.
+ """
+ def __init__(self, auth_method, gss_deleg_creds):
+ """
+ :param str auth_method: The name of the SSH authentication mechanism
+ (gssapi-with-mic or gss-keyex)
+ :param bool gss_deleg_creds: Delegate client credentials or not
+ """
+ self._auth_method = auth_method
+ self._gss_deleg_creds = gss_deleg_creds
+ self._gss_host = None
+ self._username = None
+ self._session_id = None
+ self._service = "ssh-connection"
+ """
+ OpenSSH supports Kerberos V5 mechanism only for GSS-API authentication,
+ so we also support the krb5 mechanism only.
+ """
+ self._krb5_mech = "1.2.840.113554.1.2.2"
+
+ # client mode
+ self._gss_ctxt = None
+ self._gss_ctxt_status = False
+
+ # server mode
+ self._gss_srv_ctxt = None
+ self._gss_srv_ctxt_status = False
+ self.cc_file = None
+
+ def set_service(self, service):
+ """
+ This is just a setter to use a non default service.
+ I added this method, because RFC 4462 doesn't specify "ssh-connection"
+ as the only service value.
+
+ :param str service: The desired SSH service
+ :rtype: Void
+ """
+ if service.find("ssh-"):
+ self._service = service
+
+ def set_username(self, username):
+ """
+ Setter for C{username}. If GSS-API Key Exchange is performed, the
+ username is not set by C{ssh_init_sec_context}.
+
+ :param str username: The name of the user who attempts to login
+ :rtype: Void
+ """
+ self._username = username
+
+ def ssh_gss_oids(self, mode="client"):
+ """
+ This method returns a single OID, because we only support the
+ Kerberos V5 mechanism.
+
+ :param str mode: Client for client mode and server for server mode
+ :return: A byte sequence containing the number of supported
+ OIDs, the length of the OID and the actual OID encoded with
+ DER
+ :rtype: Bytes
+ :note: In server mode we just return the OID length and the DER encoded
+ OID.
+ """
+ OIDs = self._make_uint32(1)
+ krb5_OID = encoder.encode(ObjectIdentifier(self._krb5_mech))
+ OID_len = self._make_uint32(len(krb5_OID))
+ if mode == "server":
+ return OID_len + krb5_OID
+ return OIDs + OID_len + krb5_OID
+
+ def ssh_check_mech(self, desired_mech):
+ """
+ Check if the given OID is the Kerberos V5 OID (server mode).
+
+ :param str desired_mech: The desired GSS-API mechanism of the client
+ :return: ``True`` if the given OID is supported, otherwise C{False}
+ :rtype: Boolean
+ """
+ mech, __ = decoder.decode(desired_mech)
+ if mech.__str__() != self._krb5_mech:
+ return False
+ return True
+
+ # Internals
+ #--------------------------------------------------------------------------
+ def _make_uint32(self, integer):
+ """
+ Create a 32 bit unsigned integer (The byte sequence of an integer).
+
+ :param int integer: The integer value to convert
+ :return: The byte sequence of an 32 bit integer
+ :rtype: Bytes
+ """
+ return struct.pack("!I", integer)
+
+ def _ssh_build_mic(self, session_id, username, service, auth_method):
+ """
+ Create the SSH2 MIC filed for gssapi-with-mic.
+
+ :param str session_id: The SSH session ID
+ :param str username: The name of the user who attempts to login
+ :param str service: The requested SSH service
+ :param str auth_method: The requested SSH authentication mechanism
+ :return: The MIC as defined in RFC 4462. The contents of the
+ MIC field are:
+ string session_identifier,
+ byte SSH_MSG_USERAUTH_REQUEST,
+ string user-name,
+ string service (ssh-connection),
+ string authentication-method
+ (gssapi-with-mic or gssapi-keyex)
+ :rtype: Bytes
+ """
+ mic = self._make_uint32(len(session_id))
+ mic += session_id
+ mic += struct.pack('B', MSG_USERAUTH_REQUEST)
+ mic += self._make_uint32(len(username))
+ mic += username.encode()
+ mic += self._make_uint32(len(service))
+ mic += service.encode()
+ mic += self._make_uint32(len(auth_method))
+ mic += auth_method.encode()
+ return mic
+
+
+class _SSH_GSSAPI(_SSH_GSSAuth):
+ """
+ Implementation of the GSS-API MIT Kerberos Authentication for SSH2.
+
+ :see: `.GSSAuth`
+ """
+ def __init__(self, auth_method, gss_deleg_creds):
+ """
+ :param str auth_method: The name of the SSH authentication mechanism
+ (gssapi-with-mic or gss-keyex)
+ :param bool gss_deleg_creds: Delegate client credentials or not
+ """
+ _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds)
+
+ if self._gss_deleg_creds:
+ self._gss_flags = (gssapi.C_PROT_READY_FLAG,
+ gssapi.C_INTEG_FLAG,
+ gssapi.C_MUTUAL_FLAG,
+ gssapi.C_DELEG_FLAG)
+ else:
+ self._gss_flags = (gssapi.C_PROT_READY_FLAG,
+ gssapi.C_INTEG_FLAG,
+ gssapi.C_MUTUAL_FLAG)
+
+ def ssh_init_sec_context(self, target, desired_mech=None,
+ username=None, recv_token=None):
+ """
+ Initialize a GSS-API context.
+
+ :param str username: The name of the user who attempts to login
+ :param str target: The hostname of the target to connect to
+ :param str desired_mech: The negotiated GSS-API mechanism
+ ("pseudo negotiated" mechanism, because we
+ support just the krb5 mechanism :-))
+ :param str recv_token: The GSS-API token received from the Server
+ :raise SSHException: Is raised if the desired mechanism of the client
+ is not supported
+ :return: A ``String`` if the GSS-API has returned a token or ``None`` if
+ no token was returned
+ :rtype: String or None
+ """
+ self._username = username
+ self._gss_host = target
+ targ_name = gssapi.Name("host@" + self._gss_host,
+ gssapi.C_NT_HOSTBASED_SERVICE)
+ ctx = gssapi.Context()
+ ctx.flags = self._gss_flags
+ if desired_mech is None:
+ krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech)
+ else:
+ mech, __ = decoder.decode(desired_mech)
+ if mech.__str__() != self._krb5_mech:
+ raise SSHException("Unsupported mechanism OID.")
+ else:
+ krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech)
+ token = None
+ try:
+ if recv_token is None:
+ self._gss_ctxt = gssapi.InitContext(peer_name=targ_name,
+ mech_type=krb5_mech,
+ req_flags=ctx.flags)
+ token = self._gss_ctxt.step(token)
+ else:
+ token = self._gss_ctxt.step(recv_token)
+ except gssapi.GSSException:
+ raise gssapi.GSSException("{0} Target: {1}".format(sys.exc_info()[1],
+ self._gss_host))
+ self._gss_ctxt_status = self._gss_ctxt.established
+ return token
+
+ def ssh_get_mic(self, session_id, gss_kex=False):
+ """
+ Create the MIC token for a SSH2 message.
+
+ :param str session_id: The SSH session ID
+ :param bool gss_kex: Generate the MIC for GSS-API Key Exchange or not
+ :return: gssapi-with-mic:
+ Returns the MIC token from GSS-API for the message we created
+ with ``_ssh_build_mic``.
+ gssapi-keyex:
+ Returns the MIC token from GSS-API with the SSH session ID as
+ message.
+ :rtype: String
+ :see: `._ssh_build_mic`
+ """
+ self._session_id = session_id
+ if not gss_kex:
+ mic_field = self._ssh_build_mic(self._session_id,
+ self._username,
+ self._service,
+ self._auth_method)
+ mic_token = self._gss_ctxt.get_mic(mic_field)
+ else:
+ # for key exchange with gssapi-keyex
+ mic_token = self._gss_srv_ctxt.get_mic(self._session_id)
+ return mic_token
+
+ def ssh_accept_sec_context(self, hostname, recv_token, username=None):
+ """
+ Accept a GSS-API context (server mode).
+
+ :param str hostname: The servers hostname
+ :param str username: The name of the user who attempts to login
+ :param str recv_token: The GSS-API Token received from the server,
+ if it's not the initial call.
+ :return: A ``String`` if the GSS-API has returned a token or ``None``
+ if no token was returned
+ :rtype: String or None
+ """
+ # hostname and username are not required for GSSAPI, but for SSPI
+ self._gss_host = hostname
+ self._username = username
+ if self._gss_srv_ctxt is None:
+ self._gss_srv_ctxt = gssapi.AcceptContext()
+ token = self._gss_srv_ctxt.step(recv_token)
+ self._gss_srv_ctxt_status = self._gss_srv_ctxt.established
+ return token
+
+ def ssh_check_mic(self, mic_token, session_id, username=None):
+ """
+ Verify the MIC token for a SSH2 message.
+
+ :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: None if the MIC check was successful
+ :raises gssapi.GSSException: if the MIC check failed
+ """
+ self._session_id = session_id
+ self._username = username
+ if self._username is not None:
+ # server mode
+ mic_field = self._ssh_build_mic(self._session_id,
+ self._username,
+ self._service,
+ self._auth_method)
+ self._gss_srv_ctxt.verify_mic(mic_field, mic_token)
+ else:
+ # for key exchange with gssapi-keyex
+ # client mode
+ self._gss_ctxt.verify_mic(self._session_id,
+ mic_token)
+
+ @property
+ def credentials_delegated(self):
+ """
+ Checks if credentials are delegated (server mode).
+
+ :return: ``True`` if credentials are delegated, otherwise ``False``
+ :rtype: bool
+ """
+ if self._gss_srv_ctxt.delegated_cred is not None:
+ return True
+ return False
+
+ def save_client_creds(self, client_token):
+ """
+ Save the Client token in a file. This is used by the SSH server
+ to store the client credentials if credentials are delegated
+ (server mode).
+
+ :param str client_token: The GSS-API token received form the client
+ :raise NotImplementedError: Credential delegation is currently not
+ supported in server mode
+ """
+ raise NotImplementedError
+
+
+class _SSH_SSPI(_SSH_GSSAuth):
+ """
+ Implementation of the Microsoft SSPI Kerberos Authentication for SSH2.
+
+ :see: `.GSSAuth`
+ """
+ def __init__(self, auth_method, gss_deleg_creds):
+ """
+ :param str auth_method: The name of the SSH authentication mechanism
+ (gssapi-with-mic or gss-keyex)
+ :param bool gss_deleg_creds: Delegate client credentials or not
+ """
+ _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds)
+
+ if self._gss_deleg_creds:
+ self._gss_flags = sspicon.ISC_REQ_INTEGRITY |\
+ sspicon.ISC_REQ_MUTUAL_AUTH |\
+ sspicon.ISC_REQ_DELEGATE
+ else:
+ self._gss_flags = sspicon.ISC_REQ_INTEGRITY |\
+ sspicon.ISC_REQ_MUTUAL_AUTH
+
+ def ssh_init_sec_context(self, target, desired_mech=None,
+ username=None, recv_token=None):
+ """
+ Initialize a SSPI context.
+
+ :param str username: The name of the user who attempts to login
+ :param str target: The FQDN of the target to connect to
+ :param str desired_mech: The negotiated SSPI mechanism
+ ("pseudo negotiated" mechanism, because we
+ support just the krb5 mechanism :-))
+ :param recv_token: The SSPI token received from the Server
+ :raise SSHException: Is raised if the desired mechanism of the client
+ is not supported
+ :return: A ``String`` if the SSPI has returned a token or ``None`` if
+ no token was returned
+ :rtype: String or None
+ """
+ self._username = username
+ self._gss_host = target
+ error = 0
+ targ_name = "host/" + self._gss_host
+ if desired_mech is not None:
+ mech, __ = decoder.decode(desired_mech)
+ if mech.__str__() != self._krb5_mech:
+ raise SSHException("Unsupported mechanism OID.")
+ try:
+ if recv_token is None:
+ self._gss_ctxt = sspi.ClientAuth("Kerberos",
+ scflags=self._gss_flags,
+ targetspn=targ_name)
+ error, token = self._gss_ctxt.authorize(recv_token)
+ token = token[0].Buffer
+ except:
+ raise Exception("{0}, Target: {1}".format(sys.exc_info()[1],
+ self._gss_host))
+ if error == 0:
+ """
+ if the status is GSS_COMPLETE (error = 0) the context is fully
+ established an we can set _gss_ctxt_status to True.
+ """
+ self._gss_ctxt_status = True
+ token = None
+ """
+ You won't get another token if the context is fully established,
+ so i set token to None instead of ""
+ """
+ return token
+
+ def ssh_get_mic(self, session_id, gss_kex=False):
+ """
+ Create the MIC token for a SSH2 message.
+
+ :param str session_id: The SSH session ID
+ :param bool gss_kex: Generate the MIC for Key Exchange with SSPI or not
+ :return: gssapi-with-mic:
+ Returns the MIC token from SSPI for the message we created
+ with ``_ssh_build_mic``.
+ gssapi-keyex:
+ Returns the MIC token from SSPI with the SSH session ID as
+ message.
+ :rtype: String
+ :see: `._ssh_build_mic`
+ """
+ self._session_id = session_id
+ if not gss_kex:
+ mic_field = self._ssh_build_mic(self._session_id,
+ self._username,
+ self._service,
+ self._auth_method)
+ mic_token = self._gss_ctxt.sign(mic_field)
+ else:
+ # for key exchange with gssapi-keyex
+ mic_token = self._gss_srv_ctxt.sign(self._session_id)
+ return mic_token
+
+ def ssh_accept_sec_context(self, hostname, username, recv_token):
+ """
+ Accept a SSPI context (server mode).
+
+ :param str hostname: The servers FQDN
+ :param str username: The name of the user who attempts to login
+ :param str recv_token: The SSPI Token received from the server,
+ if it's not the initial call.
+ :return: A ``String`` if the SSPI has returned a token or ``None`` if
+ no token was returned
+ :rtype: String or None
+ """
+ self._gss_host = hostname
+ self._username = username
+ targ_name = "host/" + self._gss_host
+ self._gss_srv_ctxt = sspi.ServerAuth("Kerberos", spn=targ_name)
+ error, token = self._gss_srv_ctxt.authorize(recv_token)
+ token = token[0].Buffer
+ if error == 0:
+ self._gss_srv_ctxt_status = True
+ token = None
+ return token
+
+ def ssh_check_mic(self, mic_token, session_id, username=None):
+ """
+ Verify the MIC token for a SSH2 message.
+
+ :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: None if the MIC check was successful
+ :raises sspi.error: if the MIC check failed
+ """
+ self._session_id = session_id
+ self._username = username
+ if username is not None:
+ # server mode
+ mic_field = self._ssh_build_mic(self._session_id,
+ self._username,
+ self._service,
+ self._auth_method)
+ # 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
+ # 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):
+ """
+ Checks if credentials are delegated (server mode).
+
+ :return: ``True`` if credentials are delegated, otherwise ``False``
+ :rtype: Boolean
+ """
+ return (
+ self._gss_flags & sspicon.ISC_REQ_DELEGATE
+ ) and (
+ self._gss_srv_ctxt_status or (self._gss_flags)
+ )
+
+ def save_client_creds(self, client_token):
+ """
+ Save the Client token in a file. This is used by the SSH server
+ to store the client credentails if credentials are delegated
+ (server mode).
+
+ :param str client_token: The SSPI token received form the client
+ :raise NotImplementedError: Credential delegation is currently not
+ supported in server mode
+ """
+ raise NotImplementedError
diff --git a/paramiko/transport.py b/paramiko/transport.py
index cbbdb79f..c5054dea 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -20,17 +20,20 @@
Core protocol implementation
"""
+import os
import socket
import sys
import threading
import time
import weakref
+from hashlib import md5, sha1, sha256, sha512
import paramiko
from paramiko import util
from paramiko.auth_handler import AuthHandler
+from paramiko.ssh_gss import GSSAuth
from paramiko.channel import Channel
-from paramiko.common import rng, xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \
+from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \
cMSG_GLOBAL_REQUEST, DEBUG, MSG_KEXINIT, MSG_IGNORE, MSG_DISCONNECT, \
MSG_DEBUG, ERROR, WARNING, cMSG_UNIMPLEMENTED, INFO, cMSG_KEXINIT, \
cMSG_NEWKEYS, MSG_NEWKEYS, cMSG_REQUEST_SUCCESS, cMSG_REQUEST_FAILURE, \
@@ -40,11 +43,14 @@ from paramiko.common import rng, xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \
MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, MSG_CHANNEL_OPEN, \
MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE, MSG_CHANNEL_DATA, \
MSG_CHANNEL_EXTENDED_DATA, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_REQUEST, \
- MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE
+ MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MIN_WINDOW_SIZE, MIN_PACKET_SIZE, \
+ 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
from paramiko.message import Message
from paramiko.packet import Packetizer, NeedRekeyException
from paramiko.primes import ModulusPack
@@ -55,11 +61,9 @@ from paramiko.server import ServerInterface
from paramiko.sftp_client import SFTPClient
from paramiko.ssh_exception import (SSHException, BadAuthenticationType,
ChannelException, ProxyCommandFailure)
-from paramiko.util import retry_on_signal
+from paramiko.util import retry_on_signal, ClosingContextManager, clamp_value
-from Crypto import Random
from Crypto.Cipher import Blowfish, AES, DES3, ARC4
-from Crypto.Hash import SHA, MD5
try:
from Crypto.Util import Counter
except ImportError:
@@ -77,40 +81,124 @@ import atexit
atexit.register(_join_lingering_threads)
-class Transport (threading.Thread):
+class Transport (threading.Thread, ClosingContextManager):
"""
An SSH Transport attaches to a stream (usually a socket), negotiates an
encrypted session, authenticates, and then creates stream tunnels, called
`channels <.Channel>`, across the session. Multiple channels can be
multiplexed across a single session (and often are, in the case of port
forwardings).
+
+ Instances of this class may be used as context managers.
"""
_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-group1-sha1', 'diffie-hellman-group-exchange-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': SHA, 'size': 20},
- 'hmac-sha1-96': {'class': SHA, 'size': 12},
- 'hmac-md5': {'class': MD5, 'size': 16},
- 'hmac-md5-96': {'class': MD5, 'size': 12},
+ '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},
}
_key_info = {
@@ -121,7 +209,12 @@ class Transport (threading.Thread):
_kex_info = {
'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
}
_compression_info = {
@@ -135,7 +228,12 @@ class Transport (threading.Thread):
_modulus_pack = None
- def __init__(self, sock):
+ def __init__(self,
+ sock,
+ default_window_size=DEFAULT_WINDOW_SIZE,
+ default_max_packet_size=DEFAULT_MAX_PACKET_SIZE,
+ gss_kex=False,
+ gss_deleg_creds=True):
"""
Create a new SSH session over an existing socket, or socket-like
object. This only creates the `.Transport` object; it doesn't begin the
@@ -161,8 +259,24 @@ class Transport (threading.Thread):
address and used for communication. Exceptions from the ``socket``
call may be thrown in this case.
+ .. note::
+ Modifying the the window and packet sizes might have adverse
+ effects on your channels created from this transport. The default
+ values are the same as in the OpenSSH code base and have been
+ battle tested.
+
:param socket sock:
a socket or socket-like object to create the session over.
+ :param int default_window_size:
+ sets the default window size on the transport. (defaults to
+ 2097152)
+ :param int default_max_packet_size:
+ sets the default max packet size on the transport. (defaults to
+ 32768)
+
+ .. versionchanged:: 1.15
+ Added the ``default_window_size`` and ``default_max_packet_size``
+ arguments.
"""
self.active = False
@@ -194,7 +308,6 @@ class Transport (threading.Thread):
# okay, normal socket-ish flow here...
threading.Thread.__init__(self)
self.setDaemon(True)
- self.rng = rng
self.sock = sock
# Python < 2.3 doesn't have the settimeout method - RogerB
try:
@@ -217,6 +330,21 @@ class Transport (threading.Thread):
self.host_key_type = None
self.host_key = None
+ # GSS-API / SSPI Key Exchange
+ self.use_gss_kex = gss_kex
+ # This will be set to True if GSS-API Key Exchange was performed
+ self.gss_kex_used = False
+ self.kexgss_ctxt = None
+ self.gss_host = None
+ if self.use_gss_kex:
+ self.kexgss_ctxt = GSSAuth("gssapi-keyex", gss_deleg_creds)
+ self._preferred_kex = ('gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==',
+ 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==',
+ 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==',
+ 'diffie-hellman-group-exchange-sha1',
+ 'diffie-hellman-group14-sha1',
+ 'diffie-hellman-group1-sha1')
+
# state used during negotiation
self.kex_engine = None
self.H = None
@@ -233,8 +361,8 @@ class Transport (threading.Thread):
self.channel_events = {} # (id -> Event)
self.channels_seen = {} # (id -> True)
self._channel_counter = 0
- self.window_size = 65536
- self.max_packet_size = 34816
+ self.default_max_packet_size = default_max_packet_size
+ self.default_window_size = default_window_size
self._forward_agent_handler = None
self._x11_handler = None
self._tcp_handler = None
@@ -250,6 +378,8 @@ class Transport (threading.Thread):
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
@@ -289,6 +419,7 @@ class Transport (threading.Thread):
.. versionadded:: 1.5.3
"""
+ self.sock.close()
self.close()
def get_security_options(self):
@@ -300,6 +431,17 @@ class Transport (threading.Thread):
"""
return SecurityOptions(self)
+ def set_gss_host(self, gss_host):
+ """
+ Setter for C{gss_host} if GSS-API Key Exchange is performed.
+
+ :param str gss_host: The targets name in the kerberos database
+ Default: The name of the host to connect to
+ :rtype: Void
+ """
+ # We need the FQDN to get this working with SSPI
+ self.gss_host = socket.getfqdn(gss_host)
+
def start_client(self, event=None):
"""
Negotiate a new SSH2 session as a client. This is the first step after
@@ -320,9 +462,10 @@ class Transport (threading.Thread):
.. note:: `connect` is a simpler method for connecting as a client.
- .. note:: After calling this method (or `start_server` or `connect`),
- you should no longer directly read from or write to the original
- socket object.
+ .. note::
+ After calling this method (or `start_server` or `connect`), you
+ should no longer directly read from or write to the original socket
+ object.
:param .threading.Event event:
an event to trigger when negotiation is complete (optional)
@@ -340,7 +483,6 @@ class Transport (threading.Thread):
# synchronous, wait for a result
self.completion_event = event = threading.Event()
self.start()
- Random.atfork()
while True:
event.wait(0.1)
if not self.active:
@@ -348,7 +490,7 @@ class Transport (threading.Thread):
if e is not None:
raise e
raise SSHException('Negotiation failed.')
- if event.isSet():
+ if event.is_set():
break
def start_server(self, event=None, server=None):
@@ -413,7 +555,7 @@ class Transport (threading.Thread):
if e is not None:
raise e
raise SSHException('Negotiation failed.')
- if event.isSet():
+ if event.is_set():
break
def add_server_key(self, key):
@@ -451,6 +593,7 @@ class Transport (threading.Thread):
pass
return None
+ @staticmethod
def load_server_moduli(filename=None):
"""
(optional)
@@ -476,7 +619,7 @@ class Transport (threading.Thread):
.. note:: This has no effect when used in client mode.
"""
- Transport._modulus_pack = ModulusPack(rng)
+ Transport._modulus_pack = ModulusPack()
# places to look for the openssh "moduli" file
file_list = ['/etc/ssh/moduli', '/usr/local/etc/moduli']
if filename is not None:
@@ -490,7 +633,6 @@ class Transport (threading.Thread):
# none succeeded
Transport._modulus_pack = None
return False
- load_server_moduli = staticmethod(load_server_moduli)
def close(self):
"""
@@ -530,18 +672,33 @@ class Transport (threading.Thread):
"""
return self.active
- def open_session(self):
+ 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
``"session"``.
+ .. note:: Modifying the the window and packet sizes might have adverse
+ effects on the session created. The default values are the same
+ as in the OpenSSH code base and have been battle tested.
+
+ :param int window_size:
+ optional window size for this session.
+ :param int max_packet_size:
+ optional max packet size for this session.
+
:return: a new `.Channel`
:raises SSHException: if the request is rejected or the session ends
prematurely
+
+ .. versionchanged:: 1.15
+ Added the ``window_size`` and ``max_packet_size`` arguments.
"""
- return self.open_channel('session')
+ return self.open_channel('session',
+ window_size=window_size,
+ max_packet_size=max_packet_size,
+ timeout=timeout)
def open_x11_channel(self, src_addr=None):
"""
@@ -583,13 +740,23 @@ class Transport (threading.Thread):
"""
return self.open_channel('forwarded-tcpip', dest_addr, src_addr)
- def open_channel(self, kind, dest_addr=None, src_addr=None):
+ def open_channel(self,
+ kind,
+ dest_addr=None,
+ src_addr=None,
+ window_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
session. You may only request a channel after negotiating encryption
(using `connect` or `start_client`) and authenticating.
+ .. note:: Modifying the the window and packet sizes might have adverse
+ effects on the channel created. The default values are the same
+ as in the OpenSSH code base and have been battle tested.
+
:param str kind:
the kind of channel requested (usually ``"session"``,
``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"``)
@@ -599,22 +766,35 @@ class Transport (threading.Thread):
``"direct-tcpip"`` (ignored for other channel types)
:param src_addr: the source address of this port forwarding, if
``kind`` is ``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"``
+ :param int window_size:
+ 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)
+ max_packet_size = self._sanitize_packet_size(max_packet_size)
chanid = self._next_channel()
m = Message()
m.add_byte(cMSG_CHANNEL_OPEN)
m.add_string(kind)
m.add_int(chanid)
- m.add_int(self.window_size)
- m.add_int(self.max_packet_size)
+ m.add_int(window_size)
+ m.add_int(max_packet_size)
if (kind == 'forwarded-tcpip') or (kind == 'direct-tcpip'):
m.add_string(dest_addr[0])
m.add_int(dest_addr[1])
@@ -628,10 +808,11 @@ class Transport (threading.Thread):
self.channel_events[chanid] = event = threading.Event()
self.channels_seen[chanid] = True
chan._set_transport(self)
- chan._set_window(self.window_size, self.max_packet_size)
+ chan._set_window(window_size, max_packet_size)
finally:
self.lock.release()
self._send_user_message(m)
+ start_ts = time.time()
while True:
event.wait(0.1)
if not self.active:
@@ -639,8 +820,10 @@ class Transport (threading.Thread):
if e is None:
e = SSHException('Unable to open channel.')
raise e
- if event.isSet():
+ 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
@@ -672,6 +855,7 @@ class Transport (threading.Thread):
:param callable handler:
optional handler for incoming forwarded connections, of the form
``func(Channel, (str, int), (str, int))``.
+
:return: the port number (`int`) allocated by the server
:raises SSHException: if the server refused the TCP forward request
@@ -733,8 +917,8 @@ class Transport (threading.Thread):
m = Message()
m.add_byte(cMSG_IGNORE)
if byte_count is None:
- byte_count = (byte_ord(rng.read(1)) % 32) + 10
- m.add_bytes(rng.read(byte_count))
+ byte_count = (byte_ord(os.urandom(1)) % 32) + 10
+ m.add_bytes(os.urandom(byte_count))
self._send_user_message(m)
def renegotiate_keys(self):
@@ -758,7 +942,7 @@ class Transport (threading.Thread):
if e is not None:
raise e
raise SSHException('Negotiation failed.')
- if self.completion_event.isSet():
+ if self.completion_event.is_set():
break
return
@@ -809,7 +993,7 @@ class Transport (threading.Thread):
self.completion_event.wait(0.1)
if not self.active:
return None
- if self.completion_event.isSet():
+ if self.completion_event.is_set():
break
return self.global_response
@@ -838,7 +1022,8 @@ class Transport (threading.Thread):
self.lock.release()
return chan
- def connect(self, hostkey=None, username='', password=None, pkey=None):
+ def connect(self, hostkey=None, username='', password=None, pkey=None,
+ gss_host=None, gss_auth=False, gss_kex=False, gss_deleg_creds=True):
"""
Negotiate an SSH2 session, and optionally verify the server's host key
and authenticate using a password or private key. This is a shortcut
@@ -868,6 +1053,14 @@ class Transport (threading.Thread):
:param .PKey pkey:
a private key to use for authentication, if you want to use private
key authentication; otherwise ``None``.
+ :param str gss_host:
+ The target's name in the kerberos database. Default: hostname
+ :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:
+ Whether to delegate GSS-API client credentials.
:raises SSHException: if the SSH2 negotiation fails, the host key
supplied by the server is incorrect, or authentication fails.
@@ -878,7 +1071,9 @@ class Transport (threading.Thread):
self.start_client()
# check host key if we were given one
- if hostkey is not None:
+ # If GSS-API Key Exchange was performed, we are not required to check
+ # the host key.
+ if (hostkey is not None) and not gss_kex:
key = self.get_remote_server_key()
if (key.get_name() != hostkey.get_name()) or (key.asbytes() != hostkey.asbytes()):
self._log(DEBUG, 'Bad host key from server')
@@ -887,13 +1082,19 @@ class Transport (threading.Thread):
raise SSHException('Bad host key from server')
self._log(DEBUG, 'Host key verified (%s)' % hostkey.get_name())
- if (pkey is not None) or (password is not None):
- if password is not None:
- self._log(DEBUG, 'Attempting password auth...')
- self.auth_password(username, password)
- else:
+ if (pkey is not None) or (password is not None) or gss_auth or gss_kex:
+ if gss_auth:
+ self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-with-mic)')
+ self.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds)
+ elif gss_kex:
+ self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-keyex)')
+ self.auth_gssapi_keyex(username)
+ elif pkey is not None:
self._log(DEBUG, 'Attempting public-key auth...')
self.auth_publickey(username, pkey)
+ else:
+ self._log(DEBUG, 'Attempting password auth...')
+ self.auth_password(username, password)
return
@@ -1176,6 +1377,55 @@ class Transport (threading.Thread):
self.auth_handler.auth_interactive(username, handler, my_event, submethods)
return self.auth_handler.wait_for_response(my_event)
+ def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds):
+ """
+ Authenticate to the Server using GSS-API / SSPI.
+
+ :param str username: The username to authenticate as
+ :param str gss_host: The target host
+ :param bool gss_deleg_creds: Delegate credentials or not
+ :return: list of auth types permissible for the next stage of
+ authentication (normally empty)
+ :rtype: list
+ :raise BadAuthenticationType: if gssapi-with-mic isn't
+ allowed by the server (and no event was passed in)
+ :raise AuthenticationException: if the authentication failed (and no
+ event was passed in)
+ :raise SSHException: if there was a network error
+ """
+ if (not self.active) or (not self.initial_kex_done):
+ # we should never try to authenticate unless we're on a secure link
+ raise SSHException('No existing session')
+ my_event = threading.Event()
+ self.auth_handler = AuthHandler(self)
+ self.auth_handler.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds, my_event)
+ return self.auth_handler.wait_for_response(my_event)
+
+ def auth_gssapi_keyex(self, username):
+ """
+ Authenticate to the Server with GSS-API / SSPI if GSS-API Key Exchange
+ was the used key exchange method.
+
+ :param str username: The username to authenticate as
+ :param str gss_host: The target host
+ :param bool gss_deleg_creds: Delegate credentials or not
+ :return: list of auth types permissible for the next stage of
+ authentication (normally empty)
+ :rtype: list
+ :raise BadAuthenticationType: if GSS-API Key Exchange was not performed
+ (and no event was passed in)
+ :raise AuthenticationException: if the authentication failed (and no
+ event was passed in)
+ :raise SSHException: if there was a network error
+ """
+ if (not self.active) or (not self.initial_kex_done):
+ # we should never try to authenticate unless we're on a secure link
+ raise SSHException('No existing session')
+ my_event = threading.Event()
+ self.auth_handler = AuthHandler(self)
+ self.auth_handler.auth_gssapi_keyex(username, my_event)
+ return self.auth_handler.wait_for_response(my_event)
+
def set_log_channel(self, name):
"""
Set the channel for this transport's logging. The default is
@@ -1260,7 +1510,7 @@ class Transport (threading.Thread):
def stop_thread(self):
self.active = False
self.packetizer.close()
- while self.isAlive():
+ while self.is_alive() and (self is not threading.current_thread()):
self.join(10)
### internals...
@@ -1304,7 +1554,7 @@ class Transport (threading.Thread):
self._log(DEBUG, 'Dropping user packet because connection is dead.')
return
self.clear_to_send_lock.acquire()
- if self.clear_to_send.isSet():
+ if self.clear_to_send.is_set():
break
self.clear_to_send_lock.release()
if time.time() > start + self.clear_to_send_timeout:
@@ -1340,13 +1590,23 @@ class Transport (threading.Thread):
m.add_bytes(self.H)
m.add_byte(b(id))
m.add_bytes(self.session_id)
- out = sofar = SHA.new(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 = SHA.new(m.asbytes()).digest()
+ digest = hash_algo(m.asbytes()).digest()
out += digest
sofar += digest
return out[:nbytes]
@@ -1394,6 +1654,17 @@ class Transport (threading.Thread):
finally:
self.lock.release()
+ def _sanitize_window_size(self, window_size):
+ if window_size is None:
+ window_size = self.default_window_size
+ return clamp_value(MIN_WINDOW_SIZE, window_size, MAX_WINDOW_SIZE)
+
+ def _sanitize_packet_size(self, max_packet_size):
+ if max_packet_size is None:
+ max_packet_size = self.default_max_packet_size
+ return clamp_value(MIN_PACKET_SIZE, max_packet_size, MAX_WINDOW_SIZE)
+
+
def run(self):
# (use the exposed "run" method, because if we specify a thread target
# of a private method, threading.Thread will keep a reference to it
@@ -1404,10 +1675,6 @@ class Transport (threading.Thread):
# interpreter shutdown.
self.sys = sys
- # Required to prevent RNG errors when running inside many subprocess
- # containers.
- Random.atfork()
-
# active=True occurs before the thread is launched, to avoid a race
_active_threads.append(self)
if self.server_mode:
@@ -1417,7 +1684,16 @@ class Transport (threading.Thread):
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)
@@ -1442,7 +1718,7 @@ class Transport (threading.Thread):
if ptype not in self._expected_packet:
raise SSHException('Expecting packet from %r, got %d' % (self._expected_packet, ptype))
self._expected_packet = tuple()
- if (ptype >= 30) and (ptype <= 39):
+ if (ptype >= 30) and (ptype <= 41):
self.kex_engine.parse_next(ptype, m)
continue
@@ -1467,6 +1743,7 @@ class Transport (threading.Thread):
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())
@@ -1515,6 +1792,18 @@ class Transport (threading.Thread):
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):
@@ -1552,6 +1841,7 @@ class Transport (threading.Thread):
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(' ')
@@ -1580,10 +1870,12 @@ class Transport (threading.Thread):
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))
@@ -1592,7 +1884,7 @@ class Transport (threading.Thread):
m = Message()
m.add_byte(cMSG_KEXINIT)
- m.add_bytes(rng.read(16))
+ m.add_bytes(os.urandom(16))
m.add_list(self._preferred_kex)
m.add_list(available_server_keys)
m.add_list(self._preferred_ciphers)
@@ -1635,15 +1927,24 @@ class Transport (threading.Thread):
' 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__,
@@ -1671,7 +1972,9 @@ class Transport (threading.Thread):
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))
@@ -1683,6 +1986,9 @@ class Transport (threading.Thread):
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))
@@ -1694,10 +2000,11 @@ class Transport (threading.Thread):
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
@@ -1718,12 +2025,12 @@ class Transport (threading.Thread):
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)
+ mac_key = self._compute_key('E', mac_engine().digest_size)
else:
- mac_key = self._compute_key('F', mac_engine.digest_size)
+ mac_key = self._compute_key('F', mac_engine().digest_size)
self.packetizer.set_inbound_cipher(engine, block_size, mac_engine, mac_size, mac_key)
compress_in = self._compression_info[self.remote_compression][1]
if (compress_in is not None) and ((self.remote_compression != 'zlib@openssh.com') or self.authenticated):
@@ -1745,12 +2052,12 @@ class Transport (threading.Thread):
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)
+ mac_key = self._compute_key('F', mac_engine().digest_size)
else:
- mac_key = self._compute_key('E', mac_engine.digest_size)
+ mac_key = self._compute_key('E', mac_engine().digest_size)
sdctr = self.local_cipher.endswith('-ctr')
self.packetizer.set_outbound_cipher(engine, block_size, mac_engine, mac_size, mac_key, sdctr)
compress_out = self._compression_info[self.local_compression][0]
@@ -1862,7 +2169,7 @@ class Transport (threading.Thread):
self.lock.acquire()
try:
chan._set_remote_channel(server_chanid, server_window_size, server_max_packet_size)
- self._log(INFO, 'Secsh channel %d opened.' % chanid)
+ self._log(DEBUG, 'Secsh channel %d opened.' % chanid)
if chanid in self.channel_events:
self.channel_events[chanid].set()
del self.channel_events[chanid]
@@ -1876,7 +2183,7 @@ class Transport (threading.Thread):
reason_str = m.get_text()
lang = m.get_text()
reason_text = CONNECTION_FAILED_CODE.get(reason, '(unknown code)')
- self._log(INFO, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text))
+ self._log(ERROR, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text))
self.lock.acquire()
try:
self.saved_exception = ChannelException(reason, reason_text)
@@ -1961,7 +2268,7 @@ class Transport (threading.Thread):
self._channels.put(my_chanid, chan)
self.channels_seen[my_chanid] = True
chan._set_transport(self)
- chan._set_window(self.window_size, self.max_packet_size)
+ chan._set_window(self.default_window_size, self.default_max_packet_size)
chan._set_remote_channel(chanid, initial_window_size, max_packet_size)
finally:
self.lock.release()
@@ -1969,10 +2276,10 @@ class Transport (threading.Thread):
m.add_byte(cMSG_CHANNEL_OPEN_SUCCESS)
m.add_int(chanid)
m.add_int(my_chanid)
- m.add_int(self.window_size)
- m.add_int(self.max_packet_size)
+ m.add_int(self.default_window_size)
+ m.add_int(self.default_max_packet_size)
self._send_message(m)
- self._log(INFO, 'Secsh channel %d (%s) opened.', my_chanid, kind)
+ self._log(DEBUG, 'Secsh channel %d (%s) opened.', my_chanid, kind)
if kind == 'auth-agent@openssh.com':
self._forward_agent_handler(chan)
elif kind == 'x11':
@@ -2045,21 +2352,6 @@ class SecurityOptions (object):
"""
return '<paramiko.SecurityOptions for %s>' % repr(self._transport)
- def _get_ciphers(self):
- return self._transport._preferred_ciphers
-
- def _get_digests(self):
- return self._transport._preferred_macs
-
- def _get_key_types(self):
- return self._transport._preferred_keys
-
- def _get_kex(self):
- return self._transport._preferred_kex
-
- def _get_compression(self):
- return self._transport._preferred_compression
-
def _set(self, name, orig, x):
if type(x) is list:
x = tuple(x)
@@ -2071,30 +2363,51 @@ class SecurityOptions (object):
raise ValueError('unknown cipher')
setattr(self._transport, name, x)
- def _set_ciphers(self, x):
+ @property
+ def ciphers(self):
+ """Symmetric encryption ciphers"""
+ return self._transport._preferred_ciphers
+
+ @ciphers.setter
+ def ciphers(self, x):
self._set('_preferred_ciphers', '_cipher_info', x)
- def _set_digests(self, x):
+ @property
+ def digests(self):
+ """Digest (one-way hash) algorithms"""
+ return self._transport._preferred_macs
+
+ @digests.setter
+ def digests(self, x):
self._set('_preferred_macs', '_mac_info', x)
- def _set_key_types(self, x):
+ @property
+ def key_types(self):
+ """Public-key algorithms"""
+ return self._transport._preferred_keys
+
+ @key_types.setter
+ def key_types(self, x):
self._set('_preferred_keys', '_key_info', x)
- def _set_kex(self, x):
+
+ @property
+ def kex(self):
+ """Key exchange algorithms"""
+ return self._transport._preferred_kex
+
+ @kex.setter
+ def kex(self, x):
self._set('_preferred_kex', '_kex_info', x)
- def _set_compression(self, x):
- self._set('_preferred_compression', '_compression_info', x)
+ @property
+ def compression(self):
+ """Compression algorithms"""
+ return self._transport._preferred_compression
- ciphers = property(_get_ciphers, _set_ciphers, None,
- "Symmetric encryption ciphers")
- digests = property(_get_digests, _set_digests, None,
- "Digest (one-way hash) algorithms")
- key_types = property(_get_key_types, _set_key_types, None,
- "Public-key algorithms")
- kex = property(_get_kex, _set_kex, None, "Key exchange algorithms")
- compression = property(_get_compression, _set_compression, None,
- "Compression algorithms")
+ @compression.setter
+ def compression(self, x):
+ self._set('_preferred_compression', '_compression_info', x)
class ChannelMap (object):
diff --git a/paramiko/util.py b/paramiko/util.py
index 46278a69..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])
@@ -135,15 +135,14 @@ def tb_strings():
return ''.join(traceback.format_exception(*sys.exc_info())).split('\n')
-def generate_key_bytes(hashclass, salt, key, nbytes):
+def generate_key_bytes(hash_alg, salt, key, nbytes):
"""
Given a password, passphrase, or other human-source key, scramble it
through a secure hash into some keyworthy bytes. This specific algorithm
is used for encrypting/decrypting private key files.
- :param class hashclass:
- class from `Crypto.Hash` that can be used as a secure hashing function
- (like ``MD5`` or ``SHA``).
+ :param function hash_alg: A function which creates a new hash object, such
+ as ``hashlib.sha256``.
:param salt: data to salt the hash with.
:type salt: byte string
:param str key: human-entered password or passphrase.
@@ -155,7 +154,7 @@ def generate_key_bytes(hashclass, salt, key, nbytes):
if len(salt) > 8:
salt = salt[:8]
while nbytes > 0:
- hash_obj = hashclass.new()
+ hash_obj = hash_alg()
if len(digest) > 0:
hash_obj.update(digest)
hash_obj.update(b(key))
@@ -300,9 +299,9 @@ class Counter (object):
self.value = array.array('c', zero_byte * (self.blocksize - len(x)) + x)
return self.value.tostring()
+ @classmethod
def new(cls, nbits, initial_value=long(1), overflow=long(0)):
return cls(nbits, initial_value=initial_value, overflow=overflow)
- new = classmethod(new)
def constant_time_bytes_eq(a, b):
@@ -313,3 +312,15 @@ def constant_time_bytes_eq(a, b):
for i in (xrange if PY2 else range)(len(a)):
res |= byte_ord(a[i]) ^ byte_ord(b[i])
return res == 0
+
+
+class ClosingContextManager(object):
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self.close()
+
+
+def clamp_value(minimum, val, maximum):
+ return max(minimum, min(val, maximum))
diff --git a/setup.py b/setup.py
index 235fd0e3..629c28fd 100644
--- a/setup.py
+++ b/setup.py
@@ -42,7 +42,7 @@ try:
kw = {
'install_requires': [
'pycrypto >= 2.1, != 2.4',
- 'ecdsa',
+ 'ecdsa >= 0.11',
],
}
except ImportError:
@@ -86,6 +86,8 @@ setup(
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
],
**kw
)
diff --git a/setup_helper.py b/setup_helper.py
index ff6b0e16..9e3834b3 100644
--- a/setup_helper.py
+++ b/setup_helper.py
@@ -30,9 +30,42 @@ import distutils.archive_util
from distutils.dir_util import mkpath
from distutils.spawn import spawn
-
-def make_tarball(base_name, base_dir, compress='gzip',
- verbose=False, dry_run=False):
+try:
+ from pwd import getpwnam
+except ImportError:
+ getpwnam = None
+
+try:
+ from grp import getgrnam
+except ImportError:
+ getgrnam = None
+
+def _get_gid(name):
+ """Returns a gid, given a group name."""
+ if getgrnam is None or name is None:
+ return None
+ try:
+ result = getgrnam(name)
+ except KeyError:
+ result = None
+ if result is not None:
+ return result[2]
+ return None
+
+def _get_uid(name):
+ """Returns an uid, given a user name."""
+ if getpwnam is None or name is None:
+ return None
+ try:
+ result = getpwnam(name)
+ except KeyError:
+ result = None
+ if result is not None:
+ return result[2]
+ return None
+
+def make_tarball(base_name, base_dir, compress='gzip', verbose=0, dry_run=0,
+ owner=None, group=None):
"""Create a tar file from all the files under 'base_dir'.
This file may be compressed.
@@ -75,11 +108,30 @@ def make_tarball(base_name, base_dir, compress='gzip',
mkpath(os.path.dirname(archive_name), dry_run=dry_run)
log.info('Creating tar file %s with mode %s' % (archive_name, mode))
+ uid = _get_uid(owner)
+ gid = _get_gid(group)
+
+ def _set_uid_gid(tarinfo):
+ if gid is not None:
+ tarinfo.gid = gid
+ tarinfo.gname = group
+ if uid is not None:
+ tarinfo.uid = uid
+ tarinfo.uname = owner
+ return tarinfo
+
if not dry_run:
tar = tarfile.open(archive_name, mode=mode)
# This recursively adds everything underneath base_dir
- tar.add(base_dir)
- tar.close()
+ try:
+ try:
+ # Support for the `filter' parameter was added in Python 2.7,
+ # earlier versions will raise TypeError.
+ tar.add(base_dir, filter=_set_uid_gid)
+ except TypeError:
+ tar.add(base_dir)
+ finally:
+ tar.close()
if compress and compress not in tarfile_compress_flag:
spawn([compress] + compress_flags[compress] + [archive_name],
diff --git a/sites/docs/api/agent.rst b/sites/docs/api/agent.rst
index 3b614a82..f01ad972 100644
--- a/sites/docs/api/agent.rst
+++ b/sites/docs/api/agent.rst
@@ -1,4 +1,4 @@
-SSH Agents
+SSH agents
==========
.. automodule:: paramiko.agent
diff --git a/sites/docs/api/kex_gss.rst b/sites/docs/api/kex_gss.rst
new file mode 100644
index 00000000..9fd09221
--- /dev/null
+++ b/sites/docs/api/kex_gss.rst
@@ -0,0 +1,5 @@
+GSS-API key exchange
+====================
+
+.. automodule:: paramiko.kex_gss
+ :member-order: bysource
diff --git a/sites/docs/api/ssh_gss.rst b/sites/docs/api/ssh_gss.rst
new file mode 100644
index 00000000..7a687e11
--- /dev/null
+++ b/sites/docs/api/ssh_gss.rst
@@ -0,0 +1,14 @@
+GSS-API authentication
+======================
+
+.. automodule:: paramiko.ssh_gss
+ :member-order: bysource
+
+.. autoclass:: _SSH_GSSAuth
+ :member-order: bysource
+
+.. autoclass:: _SSH_GSSAPI
+ :member-order: bysource
+
+.. autoclass:: _SSH_SSPI
+ :member-order: bysource
diff --git a/sites/docs/index.rst b/sites/docs/index.rst
index f336b393..87265d95 100644
--- a/sites/docs/index.rst
+++ b/sites/docs/index.rst
@@ -50,6 +50,8 @@ Authentication & keys
api/agent
api/hostkeys
api/keys
+ api/ssh_gss
+ api/kex_gss
Other primary functions
diff --git a/sites/shared_conf.py b/sites/shared_conf.py
index 4a6a5c4e..99fab315 100644
--- a/sites/shared_conf.py
+++ b/sites/shared_conf.py
@@ -12,7 +12,6 @@ html_theme_options = {
'description': "A Python implementation of SSHv2.",
'github_user': 'paramiko',
'github_repo': 'paramiko',
- 'gratipay_user': 'bitprophet',
'analytics_id': 'UA-18486793-2',
'travis_button': True,
}
diff --git a/sites/www/blog.py b/sites/www/blog.py
deleted file mode 100644
index 3b129ebf..00000000
--- a/sites/www/blog.py
+++ /dev/null
@@ -1,140 +0,0 @@
-from collections import namedtuple
-from datetime import datetime
-import time
-import email.utils
-
-from sphinx.util.compat import Directive
-from docutils import nodes
-
-
-class BlogDateDirective(Directive):
- """
- Used to parse/attach date info to blog post documents.
-
- No nodes generated, since none are needed.
- """
- has_content = True
-
- def run(self):
- # Tag parent document with parsed date value.
- self.state.document.blog_date = datetime.strptime(
- self.content[0], "%Y-%m-%d"
- )
- # Don't actually insert any nodes, we're already done.
- return []
-
-class blog_post_list(nodes.General, nodes.Element):
- pass
-
-class BlogPostListDirective(Directive):
- """
- Simply spits out a 'blog_post_list' temporary node for replacement.
-
- Gets replaced at doctree-resolved time - only then will all blog post
- documents be written out (& their date directives executed).
- """
- def run(self):
- return [blog_post_list('')]
-
-
-Post = namedtuple('Post', 'name doc title date opener')
-
-def get_posts(app):
- # Obtain blog posts
- post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs)
- posts = map(lambda x: (x, app.env.get_doctree(x)), post_names)
- # Obtain common data used for list page & RSS
- data = []
- for post, doc in sorted(posts, key=lambda x: x[1].blog_date, reverse=True):
- # Welp. No "nice" way to get post title. Thanks Sphinx.
- title = doc[0][0][0]
- # Date. This may or may not end up reflecting the required
- # *input* format, but doing it here gives us flexibility.
- date = doc.blog_date
- # 1st paragraph as opener. TODO: allow a role or something marking
- # where to actually pull from?
- opener = doc.traverse(nodes.paragraph)[0]
- data.append(Post(post, doc, title, date, opener))
- return data
-
-def replace_blog_post_lists(app, doctree, fromdocname):
- """
- Replace blog_post_list nodes with ordered list-o-links to posts.
- """
- # Obtain blog posts
- post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs)
- posts = map(lambda x: (x, app.env.get_doctree(x)), post_names)
- # Build "list" of links/etc
- post_links = []
- for post, doc, title, date, opener in get_posts(app):
- # Link itself
- uri = app.builder.get_relative_uri(fromdocname, post)
- link = nodes.reference('', '', refdocname=post, refuri=uri)
- # Title, bolded. TODO: use 'topic' or something maybe?
- link.append(nodes.strong('', title))
- date = date.strftime("%Y-%m-%d")
- # Meh @ not having great docutils nodes which map to this.
- html = '<div class="timestamp"><span>%s</span></div>' % date
- timestamp = nodes.raw(text=html, format='html')
- # NOTE: may group these within another element later if styling
- # necessitates it
- group = [timestamp, nodes.paragraph('', '', link), opener]
- post_links.extend(group)
-
- # Replace temp node(s) w/ expanded list-o-links
- for node in doctree.traverse(blog_post_list):
- node.replace_self(post_links)
-
-def rss_timestamp(timestamp):
- # Use horribly inappropriate module for its magical daylight-savings-aware
- # timezone madness. Props to Tinkerer for the idea.
- return email.utils.formatdate(
- time.mktime(timestamp.timetuple()),
- localtime=True
- )
-
-def generate_rss(app):
- # Meh at having to run this subroutine like 3x per build. Not worth trying
- # to be clever for now tho.
- posts_ = get_posts(app)
- # LOL URLs
- root = app.config.rss_link
- if not root.endswith('/'):
- root += '/'
- # Oh boy
- posts = [
- (
- root + app.builder.get_target_uri(x.name),
- x.title,
- str(x.opener[0]), # Grab inner text element from paragraph
- rss_timestamp(x.date),
- )
- for x in posts_
- ]
- location = 'blog/rss.xml'
- context = {
- 'title': app.config.project,
- 'link': root,
- 'atom': root + location,
- 'description': app.config.rss_description,
- # 'posts' is sorted by date already
- 'date': rss_timestamp(posts_[0].date),
- 'posts': posts,
- }
- yield (location, context, 'rss.xml')
-
-def setup(app):
- # Link in RSS feed back to main website, e.g. 'http://paramiko.org'
- app.add_config_value('rss_link', None, '')
- # Ditto for RSS description field
- app.add_config_value('rss_description', None, '')
- # Interprets date metadata in blog post documents
- app.add_directive('date', BlogDateDirective)
- # Inserts blog post list node (in e.g. a listing page) for replacement
- # below
- app.add_node(blog_post_list)
- app.add_directive('blog-posts', BlogPostListDirective)
- # Performs abovementioned replacement
- app.connect('doctree-resolved', replace_blog_post_lists)
- # Generates RSS page from whole cloth at page generation step
- app.connect('html-collect-pages', generate_rss)
diff --git a/sites/www/blog.rst b/sites/www/blog.rst
deleted file mode 100644
index af9651e4..00000000
--- a/sites/www/blog.rst
+++ /dev/null
@@ -1,16 +0,0 @@
-====
-Blog
-====
-
-.. blog-posts directive gets replaced with an ordered list of blog posts.
-
-.. blog-posts::
-
-
-.. The following toctree ensures blog posts get processed.
-
-.. toctree::
- :hidden:
- :glob:
-
- blog/*
diff --git a/sites/www/blog/first-post.rst b/sites/www/blog/first-post.rst
deleted file mode 100644
index 7b075073..00000000
--- a/sites/www/blog/first-post.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-===========
-First post!
-===========
-
-A blog post.
-
-.. date:: 2013-12-04
diff --git a/sites/www/blog/second-post.rst b/sites/www/blog/second-post.rst
deleted file mode 100644
index c4463f33..00000000
--- a/sites/www/blog/second-post.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-===========
-Another one
-===========
-
-.. date:: 2013-12-05
-
-Indeed!
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 50447c04..bd890b4e 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -4,18 +4,110 @@ Changelog
* :bug:`502` Fix an issue in server mode, when processing an exec request.
A command that is not a valid UTF-8 string, caused an UnicodeDecodeError.
+* :bug:`401` Fix line number reporting in log output regarding invalid
+ ``known_hosts`` line entries. Thanks to Dylan Thacker-Smith for catch &
+ patch.
+* :support:`525 backported` Update the vendored Windows API addon to a more
+ recent edition. Also fixes :issue:`193`, :issue:`488`, :issue:`498`. Thanks
+ to Jason Coombs.
+* :release:`1.15.4 <2015-11-02>`
+* :release:`1.14.3 <2015-11-02>`
+* :release:`1.13.4 <2015-11-02>`
+* :bug:`366` Fix `~paramiko.sftp_attributes.SFTPAttributes` so its string
+ representation doesn't raise exceptions on empty/initialized instances. Patch
+ by Ulrich Petri.
+* :bug:`359` Use correct attribute name when trying to use Python 3's
+ ``int.bit_length`` method; prior to fix, the Python 2 custom fallback
+ implementation was always used, even on Python 3. Thanks to Alex Gaynor.
+* :support:`594 backported` Correct some post-Python3-port docstrings to
+ specify ``bytes`` type instead of ``str``. Credit to ``@redixin``.
+* :bug:`565` Don't explode with ``IndexError`` when reading private key files
+ lacking an ``-----END <type> PRIVATE KEY-----`` footer. Patch courtesy of
+ Prasanna Santhanam.
+* :feature:`604` Add support for the ``aes192-ctr`` and ``aes192-cbc`` ciphers.
+ Thanks to Michiel Tiller for noticing it was as easy as tweaking some key
+ sizes :D
+* :feature:`356` (also :issue:`596`, :issue:`365`, :issue:`341`, :issue:`164`,
+ :issue:`581`, and a bunch of other duplicates besides) Add support for SHA-2
+ based key exchange (kex) algorithm ``diffie-hellman-group-exchange-sha256``
+ and (H)MAC algorithms ``hmac-sha2-256`` and ``hmac-sha2-512``.
+
+ This change includes tweaks to debug-level logging regarding
+ algorithm-selection handshakes; the old all-in-one log line is now multiple
+ easier-to-read, printed-at-handshake-time log lines.
+
+ Thanks to the many people who submitted patches for this functionality and/or
+ assisted in testing those patches. That list includes but is not limited to,
+ and in no particular order: Matthias Witte, Dag Wieers, Ash Berlin, Etienne
+ Perot, Gert van Dijk, ``@GuyShaanan``, Aaron Bieber, ``@cyphase``, and Eric
+ Brown.
+* :release:`1.15.3 <2015-10-02>`
+* :support:`554 backported` Fix inaccuracies in the docstring for the ECDSA key
+ class. Thanks to Jared Hance for the patch.
+* :support:`516 backported` Document `~paramiko.agent.AgentRequestHandler`.
+ Thanks to ``@toejough`` for report & suggestions.
+* :bug:`496` Fix a handful of small but critical bugs in Paramiko's GSSAPI
+ support (note: this includes switching from PyCrypo's Random to
+ `os.urandom`). Thanks to Anselm Kruis for catch & patch.
+* :bug:`491` (combines :issue:`62` and :issue:`439`) Implement timeout
+ functionality to address hangs from dropped network connections and/or failed
+ handshakes. Credit to ``@vazir`` and ``@dacut`` for the original patches and
+ to Olle Lundberg for reimplementation.
+* :bug:`490` Skip invalid/unparseable lines in ``known_hosts`` files, instead
+ of raising `~paramiko.ssh_exception.SSHException`. This brings Paramiko's
+ behavior more in line with OpenSSH, which silently ignores such input. Catch
+ & patch courtesy of Martin Topholm.
+* :bug:`404` Print details when displaying
+ `~paramiko.ssh_exception.BadHostKeyException` objects (expected vs received
+ data) instead of just "hey shit broke". Patch credit: Loic Dachary.
+* :bug:`469` (also :issue:`488`, :issue:`461` and like a dozen others) Fix a
+ typo introduced in the 1.15 release which broke WinPageant support. Thanks to
+ everyone who submitted patches, and to Steve Cohen who was the lucky winner
+ of the cherry-pick lottery.
+* :bug:`353` (via :issue:`482`) Fix a bug introduced in the Python 3 port
+ which caused ``OverFlowError`` (and other symptoms) in SFTP functionality.
+ Thanks to ``@dboreham`` for leading the troubleshooting charge, and to
+ Scott Maxwell for the final patch.
+* :support:`582` Fix some old ``setup.py`` related helper code which was
+ breaking ``bdist_dumb`` on Mac OS X. Thanks to Peter Odding for the patch.
+* :bug:`22 major` Try harder to connect to multiple network families (e.g. IPv4
+ vs IPv6) in case of connection issues; this helps with problems such as hosts
+ which resolve both IPv4 and IPv6 addresses but are only listening on IPv4.
+ Thanks to Dries Desmet for original report and Torsten Landschoff for the
+ foundational patchset.
+* :bug:`402` Check to see if an SSH agent is actually present before trying to
+ forward it to the remote end. This replaces what was usually a useless
+ ``TypeError`` with a human-readable
+ `~paramiko.ssh_exception.AuthenticationException`. Credit to Ken Jordan for
+ the fix and Yvan Marques for original report.
+* :release:`1.15.2 <2014-12-19>`
+* :release:`1.14.2 <2014-12-19>`
* :release:`1.13.3 <2014-12-19>`
* :bug:`413` (also :issue:`414`, :issue:`420`, :issue:`454`) Be significantly
smarter about polling & timing behavior when running proxy commands, to avoid
unnecessary (often 100%!) CPU usage. Major thanks to Jason Dunsmore for
report & initial patchset and to Chris Adams & John Morrissey for followup
improvements.
+* :bug:`455` Tweak packet size handling to conform better to the OpenSSH RFCs;
+ this helps address issues with interactive program cursors. Courtesy of Jeff
+ Quast.
* :bug:`428` Fix an issue in `~paramiko.file.BufferedFile` (primarily used in
the SFTP modules) concerning incorrect behavior by
`~paramiko.file.BufferedFile.readlines` on files whose size exceeds the
buffer size. Thanks to ``@achapp`` for catch & patch.
+* :bug:`415` Fix ``ssh_config`` parsing to correctly interpret ``ProxyCommand
+ none`` as the lack of a proxy command, instead of as a literal command string
+ of ``"none"``. Thanks to Richard Spiers for the catch & Sean Johnson for the
+ fix.
+* :support:`431 backported` Replace handrolled ``ssh_config`` parsing code with
+ use of the ``shlex`` module. Thanks to Yan Kalchevskiy.
* :support:`422 backported` Clean up some unused imports. Courtesy of Olle
Lundberg.
+* :support:`421 backported` Modernize threading calls to user newer API. Thanks
+ to Olle Lundberg.
+* :support:`419 backported` Modernize a bunch of the codebase internals to
+ leverage decorators. Props to ``@beckjake`` for realizing we're no longer on
+ Python 2.2 :D
* :bug:`266` Change numbering of `~paramiko.transport.Transport` channels to
start at 0 instead of 1 for better compatibility with OpenSSH & certain
server implementations which break on 1-indexed channels. Thanks to
@@ -29,10 +121,69 @@ Changelog
for the catch.
* :bug:`320` Update our win_pageant module to be Python 3 compatible. Thanks to
``@sherbang`` and ``@adamkerz`` for the patches.
+* :release:`1.15.1 <2014-09-22>`
+* :bug:`399` SSH agent forwarding (potentially other functionality as
+ well) would hang due to incorrect values passed into the new window size
+ arguments for `.Transport` (thanks to a botched merge). This has been
+ corrected. Thanks to Dylan Thacker-Smith for the report & patch.
+* :feature:`167` Add `.SSHConfig.get_hostnames` for easier introspection of a
+ loaded SSH config file or object. Courtesy of Søren Løvborg.
+* :release:`1.15.0 <2014-09-18>`
+* :support:`393` Replace internal use of PyCrypto's ``SHA.new`` with the
+ stdlib's ``hashlib.sha1``. Thanks to Alex Gaynor.
+* :feature:`267` (also :issue:`250`, :issue:`241`, :issue:`228`) Add GSS-API /
+ SSPI (e.g. Kerberos) key exchange and authentication support
+ (:ref:`installation docs here <gssapi>`). Mega thanks to Sebastian Deiß, with
+ assist by Torsten Landschoff.
+
+ .. note::
+ Unix users should be aware that the ``python-gssapi`` library (a
+ requirement for using this functionality) only appears to support
+ Python 2.7 and up at this time.
+
+* :bug:`346 major` Fix an issue in private key files' encryption salts that
+ could cause tracebacks and file corruption if keys were re-encrypted. Credit
+ to Xavier Nunn.
+* :feature:`362` Allow users to control the SSH banner timeout. Thanks to Cory
+ Benfield.
+* :feature:`372` Update default window & packet sizes to more closely adhere to
+ the pertinent RFC; also expose these settings in the public API so they may
+ be overridden by client code. This should address some general speed issues
+ such as :issue:`175`. Big thanks to Olle Lundberg for the update.
+* :bug:`373 major` Attempt to fix a handful of issues (such as :issue:`354`)
+ related to infinite loops and threading deadlocks. Thanks to Olle Lundberg as
+ well as a handful of community members who provided advice & feedback via
+ IRC.
+* :support:`374` (also :issue:`375`) Old code cleanup courtesy of Olle
+ Lundberg.
+* :support:`377` Factor `~paramiko.channel.Channel` openness sanity check into
+ a decorator. Thanks to Olle Lundberg for original patch.
+* :bug:`298 major` Don't perform point validation on ECDSA keys in
+ ``known_hosts`` files, since a) this can cause significant slowdown when such
+ keys exist, and b) ``known_hosts`` files are implicitly trustworthy. Thanks
+ to Kieran Spear for catch & patch.
+
+ .. note::
+ This change bumps up the version requirement for the ``ecdsa`` library to
+ ``0.11``.
+
+* :bug:`234 major` Lower logging levels for a few overly-noisy log messages
+ about secure channels. Thanks to David Pursehouse for noticing & contributing
+ the fix.
+* :feature:`218` Add support for ECDSA private keys on the client side. Thanks
+ to ``@aszlig`` for the patch.
+* :bug:`335 major` Fix ECDSA key generation (generation of brand new ECDSA keys
+ was broken previously). Thanks to ``@solarw`` for catch & patch.
+* :feature:`184` Support quoted values in SSH config file parsing. Credit to
+ Yan Kalchevskiy.
+* :feature:`131` Add a `~paramiko.sftp_client.SFTPClient.listdir_iter` method
+ to `~paramiko.sftp_client.SFTPClient` allowing for more efficient,
+ async/generator based file listings. Thanks to John Begeman.
* :support:`378 backported` Minor code cleanup in the SSH config module
courtesy of Olle Lundberg.
* :support:`249 backported` Consolidate version information into one spot.
Thanks to Gabi Davar for the reminder.
+* :release:`1.14.1 <2014-08-25>`
* :release:`1.13.2 <2014-08-25>`
* :bug:`376` Be less aggressive about expanding variables in ``ssh_config``
files, which results in a speedup of SSH config parsing. Credit to Olle
@@ -59,6 +210,9 @@ Changelog
Thanks to ``@basictheprogram`` for the initial report, Jelmer Vernooij for
the fix and Andrew Starr-Bochicchio & Jeremy T. Bouse (among others) for
discussion & feedback.
+* :support:`371` Add Travis support & docs update for Python 3.4. Thanks to
+ Olle Lundberg.
+* :release:`1.14.0 <2014-05-07>`
* :release:`1.13.1 <2014-05-07>`
* :release:`1.12.4 <2014-05-07>`
* :release:`1.11.6 <2014-05-07>`
@@ -83,6 +237,12 @@ Changelog
character. Thanks to Antoine Brenner.
* :bug:`308` Fix regression in dsskey.py that caused sporadic signature
verification failures. Thanks to Chris Rose.
+* :support:`299` Use deterministic signatures for ECDSA keys for improved
+ security. Thanks to Alex Gaynor.
+* :support:`297` Replace PyCrypto's ``Random`` with `os.urandom` for improved
+ speed and security. Thanks again to Alex.
+* :support:`295` Swap out a bunch of PyCrypto hash functions with use of
+ `hashlib`. Thanks to Alex Gaynor.
* :support:`290` (also :issue:`292`) Add support for building universal
(Python 2+3 compatible) wheel files during the release process. Courtesy of
Alex Gaynor.
@@ -96,7 +256,8 @@ Changelog
* :release:`1.11.5 <2014-03-13>`
* :release:`1.10.7 <2014-03-13>`
* :feature:`16` **Python 3 support!** Our test suite passes under Python 3, and
- it (& Fabric's test suite) continues to pass under Python 2.
+ it (& Fabric's test suite) continues to pass under Python 2. **Python 2.5 is
+ no longer supported with this change!**
The merged code was built on many contributors' efforts, both code &
feedback. In no particular order, we thank Daniel Goertzen, Ivan Kolodyazhny,
diff --git a/sites/www/conf.py b/sites/www/conf.py
index c7828203..0b0fb85c 100644
--- a/sites/www/conf.py
+++ b/sites/www/conf.py
@@ -6,24 +6,16 @@ from os.path import abspath, join, dirname
sys.path.append(abspath(join(dirname(__file__), '..')))
from shared_conf import *
-# Local blog extension
-sys.path.append(abspath('.'))
-extensions.append('blog')
-rss_link = 'http://paramiko.org'
-rss_description = 'Paramiko project news'
-
# Releases changelog extension
extensions.append('releases')
-releases_release_uri = "https://github.com/paramiko/paramiko/tree/%s"
+# Paramiko 1.x tags start with 'v'. Meh.
+releases_release_uri = "https://github.com/paramiko/paramiko/tree/v%s"
releases_issue_uri = "https://github.com/paramiko/paramiko/issues/%s"
-# Intersphinx for referencing API/usage docs
-extensions.append('sphinx.ext.intersphinx')
# Default is 'local' building, but reference the public docs site when building
# under RTD.
target = join(dirname(__file__), '..', 'docs', '_build')
if os.environ.get('READTHEDOCS') == 'True':
- # TODO: switch to docs.paramiko.org post go-live of sphinx API docs
target = 'http://docs.paramiko.org/en/latest/'
intersphinx_mapping['docs'] = (target, None)
diff --git a/sites/www/contact.rst b/sites/www/contact.rst
index 2b6583f5..7e6c947e 100644
--- a/sites/www/contact.rst
+++ b/sites/www/contact.rst
@@ -9,3 +9,4 @@ following ways:
* Mailing list: ``paramiko@librelist.com`` (see `the LibreList homepage
<http://librelist.com>`_ for usage details).
* This website - a blog section is forthcoming.
+* Submit contributions on Github - see the :doc:`contributing` page.
diff --git a/sites/www/contributing.rst b/sites/www/contributing.rst
index 2b752cc5..a44414e8 100644
--- a/sites/www/contributing.rst
+++ b/sites/www/contributing.rst
@@ -5,15 +5,22 @@ Contributing
How to get the code
===================
-Our primary Git repository is on Github at `paramiko/paramiko
-<https://github.com/paramiko/paramiko>`_; please follow their instructions for
-cloning to your local system. (If you intend to submit patches/pull requests,
-we recommend forking first, then cloning your fork. Github has excellent
-documentation for all this.)
+Our primary Git repository is on Github at `paramiko/paramiko`_;
+please follow their instructions for cloning to your local system. (If you
+intend to submit patches/pull requests, we recommend forking first, then
+cloning your fork. Github has excellent documentation for all this.)
How to submit bug reports or new code
=====================================
Please see `this project-agnostic contribution guide
-<http://contribution-guide.org>`_ - we follow it explicitly.
+<http://contribution-guide.org>`_ - we follow it explicitly. Again, our code
+repository and bug tracker is `on Github`_.
+
+Our current changelog is located in ``sites/www/changelog.rst`` - the top
+level files like ``ChangeLog.*`` and ``NEWS`` are historical only.
+
+
+.. _paramiko/paramiko:
+.. _on Github: https://github.com/paramiko/paramiko
diff --git a/sites/www/faq.rst b/sites/www/faq.rst
new file mode 100644
index 00000000..a5d9b383
--- /dev/null
+++ b/sites/www/faq.rst
@@ -0,0 +1,26 @@
+===================================
+Frequently Asked/Answered Questions
+===================================
+
+Which version should I use? I see multiple active releases.
+===========================================================
+
+Please see :ref:`the installation docs <release-lines>` which have an explicit
+section about this topic.
+
+Paramiko doesn't work with my Cisco, Windows or other non-Unix system!
+======================================================================
+
+In an ideal world, the developers would love to support every possible target
+system. Unfortunately, volunteer development time and access to non-mainstream
+platforms are limited, meaning that we can only fully support standard OpenSSH
+implementations such as those found on the average Linux distribution (as well
+as on Mac OS X and \*BSD.)
+
+Because of this, **we typically close bug reports for nonstandard SSH
+implementations or host systems**.
+
+However, **closed does not imply locked** - affected users can still post
+comments on such tickets - and **we will always consider actual patch
+submissions for these issues**, provided they can get +1s from similarly
+affected users and are proven to not break existing functionality.
diff --git a/sites/www/index.rst b/sites/www/index.rst
index 0f07d7e9..8e7562af 100644
--- a/sites/www/index.rst
+++ b/sites/www/index.rst
@@ -1,7 +1,7 @@
Welcome to Paramiko!
====================
-Paramiko is a Python (2.5+) implementation of the SSHv2 protocol [#]_,
+Paramiko is a Python (2.6+, 3.3+) implementation of the SSHv2 protocol [#]_,
providing both client and server functionality. While it leverages a Python C
extension for low level cryptography (`PyCrypto <http://pycrypto.org>`_),
Paramiko itself is a pure Python interface around SSH networking concepts.
@@ -11,30 +11,22 @@ contribution guidelines, development roadmap, news/blog, and so forth. Detailed
usage and API documentation can be found at our code documentation site,
`docs.paramiko.org <http://docs.paramiko.org>`_.
+Please see the sidebar to the left to begin.
+
.. toctree::
:hidden:
changelog
+ FAQs <faq>
installing
contributing
contact
-.. Hide blog in hidden toctree for now (to avoid warnings.)
-
-.. toctree::
- :hidden:
-
- blog
-
.. rubric:: Footnotes
.. [#]
- SSH is defined in RFCs
- `4251 <http://www.rfc-editor.org/rfc/rfc4251.txt>`_,
- `4252 <http://www.rfc-editor.org/rfc/rfc4252.txt>`_,
- `4253 <http://www.rfc-editor.org/rfc/rfc4253.txt>`_, and
- `4254 <http://www.rfc-editor.org/rfc/rfc4254.txt>`_;
- the primary working implementation of the protocol is the `OpenSSH project
+ SSH is defined in :rfc:`4251`, :rfc:`4252`, :rfc:`4253` and :rfc:`4254`. The
+ primary working implementation of the protocol is the `OpenSSH project
<http://openssh.org>`_. Paramiko implements a large portion of the SSH
feature set, but there are occasional gaps.
diff --git a/sites/www/installing.rst b/sites/www/installing.rst
index 0ca9b156..a657c3fc 100644
--- a/sites/www/installing.rst
+++ b/sites/www/installing.rst
@@ -2,6 +2,8 @@
Installing
==========
+.. _paramiko-itself:
+
Paramiko itself
===============
@@ -14,50 +16,47 @@ via `pip <http://pip-installer.org>`_::
Users who want the bleeding edge can install the development version via
``pip install paramiko==dev``.
-We currently support **Python 2.5/2.6/2.7**, with support for Python 3 coming
-soon. Users on Python 2.4 or older are urged to upgrade. Paramiko *may* work on
-Python 2.4 still, but there is no longer any support guarantee.
+We currently support **Python 2.6, 2.7 and 3.3+** (Python **3.2** should also
+work but has a less-strong compatibility guarantee from us.) Users on Python
+2.5 or older are urged to upgrade.
-Paramiko has two dependencies: the pure-Python ECDSA module ``ecdsa``, and the
+Paramiko has two hard dependencies: the pure-Python ECDSA module ``ecdsa``, and the
PyCrypto C extension. ``ecdsa`` is easily installable from wherever you
obtained Paramiko's package; PyCrypto may require more work. Read on for
details.
-PyCrypto
-========
-
-`PyCrypto <https://www.dlitz.net/software/pycrypto/>`_ provides the low-level
-(C-based) encryption algorithms we need to implement the SSH protocol. There
-are a couple gotchas associated with installing PyCrypto: its compatibility
-with Python's package tools, and the fact that it is a C-based extension.
+If you need GSS-API / SSPI support, see :ref:`the below subsection on it
+<gssapi>` for details on additional dependencies.
-.. _pycrypto-and-pip:
+.. _release-lines:
-Possible gotcha on older Python and/or pip versions
----------------------------------------------------
+Release lines
+-------------
-We strongly recommend using ``pip`` to as it is newer and generally better than
-``easy_install``. However, a combination of bugs in specific (now rather old)
-versions of Python, ``pip`` and PyCrypto can prevent installation of PyCrypto.
-Specifically:
+Users desiring stability may wish to pin themselves to a specific release line
+once they first start using Paramiko; to assist in this, we guarantee bugfixes
+for the last 2-3 releases including the latest stable one.
-* Python = 2.5.x
-* PyCrypto >= 2.1 (required for most modern versions of Paramiko)
-* ``pip`` < 0.8.1
+If you're unsure which version to install, we have suggestions:
-When all three criteria are met, you may encounter ``No such file or
-directory`` IOErrors when trying to ``pip install paramiko`` or ``pip install
-PyCrypto``.
+* **Completely new users** should always default to the **latest stable
+ release** (as above, whatever is newest / whatever shows up with ``pip
+ install paramiko``.)
+* **Users upgrading from a much older version** (e.g. the 1.7.x line) should
+ probably get the **oldest actively supported line** (see the paragraph above
+ this list for what that currently is.)
+* **Everybody else** is hopefully already "on" a given version and can
+ carefully upgrade to whichever version they care to, when their release line
+ stops being supported.
-The fix is to make sure at least one of the above criteria is not met, by doing
-the following (in order of preference):
-* Upgrade to ``pip`` 0.8.1 or above, e.g. by running ``pip install -U pip``.
-* Upgrade to Python 2.6 or above.
-* Downgrade to Paramiko 1.7.6 or 1.7.7, which do not require PyCrypto >= 2.1,
- and install PyCrypto 2.0.1 (the oldest version on PyPI which works with
- Paramiko 1.7.6/1.7.7)
+PyCrypto
+========
+`PyCrypto <https://www.dlitz.net/software/pycrypto/>`_ provides the low-level
+(C-based) encryption algorithms we need to implement the SSH protocol. There
+are a couple gotchas associated with installing PyCrypto: its compatibility
+with Python's package tools, and the fact that it is a C-based extension.
C extension
-----------
@@ -103,3 +102,31 @@ installation of Paramiko via ``pypm``::
Installing paramiko-1.7.8
Installing pycrypto-2.4
C:\>
+
+
+.. _gssapi:
+
+Optional dependencies for GSS-API / SSPI / Kerberos
+===================================================
+
+In order to use GSS-API/Kerberos & related functionality, a couple of
+additional dependencies are required (these are not listed in our ``setup.py``
+due to their infrequent utility & non-platform-agnostic requirements):
+
+* It hopefully goes without saying but **all platforms** need **a working
+ installation of GSS-API itself**, e.g. Heimdal.
+* **All platforms** need `pyasn1 <https://pypi.python.org/pypi/pyasn1>`_
+ ``0.1.7`` or better.
+* **Unix** needs `python-gssapi <https://pypi.python.org/pypi/python-gssapi/>`_
+ ``0.6.1`` or better.
+
+ .. note:: This library appears to only function on Python 2.7 and up.
+
+* **Windows** needs `pywin32 <https://pypi.python.org/pypi/pywin32>`_ ``2.1.8``
+ or better.
+
+.. note::
+ If you use Microsoft SSPI for kerberos authentication and credential
+ delegation, make sure that the target host is trusted for delegation in the
+ active directory configuration. For details see:
+ http://technet.microsoft.com/en-us/library/cc738491%28v=ws.10%29.aspx
diff --git a/tasks.py b/tasks.py
index 1afce514..3d55a778 100644
--- a/tasks.py
+++ b/tasks.py
@@ -3,32 +3,20 @@ from os.path import join
from shutil import rmtree, copytree
from invoke import Collection, ctask as task
-from invocations import docs as _docs
+from invocations.docs import docs, www, sites
from invocations.packaging import publish
-d = 'sites'
-
-# Usage doc/API site (published as docs.paramiko.org)
-docs_path = join(d, 'docs')
-docs_build = join(docs_path, '_build')
-docs = Collection.from_module(_docs, name='docs', config={
- 'sphinx.source': docs_path,
- 'sphinx.target': docs_build,
-})
-
-# Main/about/changelog site ((www.)?paramiko.org)
-www_path = join(d, 'www')
-www = Collection.from_module(_docs, name='www', config={
- 'sphinx.source': www_path,
- 'sphinx.target': join(www_path, '_build'),
-})
-
-
# Until we move to spec-based testing
@task
-def test(ctx):
- ctx.run("python test.py --verbose", pty=True)
+def test(ctx, coverage=False, flags=""):
+ if "--verbose" not in flags.split():
+ flags += " --verbose"
+ runner = "python"
+ if coverage:
+ runner = "coverage run --source=paramiko"
+ ctx.run("{0} test.py {1}".format(runner, flags), pty=True)
+
@task
def coverage(ctx):
@@ -43,11 +31,12 @@ def release(ctx):
# Move the built docs into where Epydocs used to live
target = 'docs'
rmtree(target, ignore_errors=True)
- copytree(docs_build, target)
+ # TODO: make it easier to yank out this config val from the docs coll
+ copytree('sites/docs/_build', target)
# Publish
- publish(ctx, wheel=True)
+ publish(ctx)
# Remind
print("\n\nDon't forget to update RTD's versions page for new minor releases!")
-ns = Collection(test, coverage, release, docs=docs, www=www)
+ns = Collection(test, coverage, release, docs, www, sites)
diff --git a/test.py b/test.py
index bd966d1e..37fc5a6f 100755
--- a/test.py
+++ b/test.py
@@ -44,6 +44,10 @@ from tests.test_packetizer import PacketizerTest
from tests.test_auth import AuthTest
from tests.test_transport import TransportTest
from tests.test_client import SSHClientTest
+from test_client import SSHClientTest
+from test_gssapi import GSSAPITest
+from test_ssh_gss import GSSAuthTest
+from test_kex_gss import GSSKexTest
default_host = 'localhost'
default_user = os.environ.get('USER', 'nobody')
@@ -85,6 +89,17 @@ def main():
help='skip SFTP client/server tests, which can be slow')
parser.add_option('--no-big-file', action='store_false', dest='use_big_file', default=True,
help='skip big file SFTP tests, which are slow as molasses')
+ parser.add_option('--gssapi-test', action='store_true', dest='gssapi_test', default=False,
+ help='Test the used APIs for GSS-API / SSPI authentication')
+ parser.add_option('--test-gssauth', action='store_true', dest='test_gssauth', default=False,
+ help='Test GSS-API / SSPI authentication for SSHv2. To test this, you need kerberos a infrastructure.\
+ Note: Paramiko needs access to your krb5.keytab file. Make it readable for Paramiko or\
+ copy the used key to another file and set the environment variable KRB5_KTNAME to this file.')
+ parser.add_option('--test-gssapi-keyex', action='store_true', dest='test_gsskex', default=False,
+ help='Test GSS-API / SSPI authenticated iffie-Hellman Key Exchange and user\
+ authentication. To test this, you need kerberos a infrastructure.\
+ Note: Paramiko needs access to your krb5.keytab file. Make it readable for Paramiko or\
+ copy the used key to another file and set the environment variable KRB5_KTNAME to this file.')
parser.add_option('-R', action='store_false', dest='use_loopback_sftp', default=True,
help='perform SFTP tests against a remote server (by default, SFTP tests ' +
'are done through a loopback socket)')
@@ -101,12 +116,22 @@ def main():
parser.add_option('-P', '--sftp-passwd', dest='password', type='string', default=default_passwd,
metavar='<password>',
help='[with -R] (optional) password to unlock the private key for remote sftp tests')
-
+ parser.add_option('--krb5_principal', dest='krb5_principal', type='string',
+ metavar='<krb5_principal>',
+ help='The krb5 principal (your username) for GSS-API / SSPI authentication')
+ parser.add_option('--targ_name', dest='targ_name', type='string',
+ metavar='<targ_name>',
+ help='Target name for GSS-API / SSPI authentication.\
+ This is the hosts name you are running the test on in the kerberos database.')
+ parser.add_option('--server_mode', action='store_true', dest='server_mode', default=False,
+ help='Usage with --gssapi-test. Test the available GSS-API / SSPI server mode to.\
+ Note: you need to have access to the kerberos keytab file.')
+
options, args = parser.parse_args()
-
+
# setup logging
paramiko.util.log_to_file('test.log')
-
+
if options.use_sftp:
from tests.test_sftp import SFTPTest
if options.use_loopback_sftp:
@@ -136,6 +161,15 @@ def main():
suite.addTest(unittest.makeSuite(SFTPTest))
if options.use_big_file:
suite.addTest(unittest.makeSuite(BigSFTPTest))
+ if options.gssapi_test:
+ GSSAPITest.init(options.targ_name, options.server_mode)
+ suite.addTest(unittest.makeSuite(GSSAPITest))
+ if options.test_gssauth:
+ GSSAuthTest.init(options.krb5_principal, options.targ_name)
+ suite.addTest(unittest.makeSuite(GSSAuthTest))
+ if options.test_gsskex:
+ GSSKexTest.init(options.krb5_principal, options.targ_name)
+ suite.addTest(unittest.makeSuite(GSSKexTest))
verbosity = 1
if options.verbose:
verbosity = 2
@@ -149,10 +183,7 @@ def main():
# TODO: make that not a problem, jeez
for thread in threading.enumerate():
if thread is not threading.currentThread():
- if PY2:
- thread._Thread__stop()
- else:
- thread._stop()
+ thread.join(timeout=1)
# Exit correctly
if not result.wasSuccessful():
sys.exit(1)
diff --git a/tests/stub_sftp.py b/tests/stub_sftp.py
index 47644433..24380ba1 100644
--- a/tests/stub_sftp.py
+++ b/tests/stub_sftp.py
@@ -21,6 +21,7 @@ A stub SFTP server for loopback SFTP testing.
"""
import os
+import sys
from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes, \
SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED
from paramiko.common import o666
diff --git a/tests/test_auth.py b/tests/test_auth.py
index 1d972d53..ec78e3ce 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -118,12 +118,12 @@ class AuthTest (unittest.TestCase):
self.ts.add_server_key(host_key)
self.event = threading.Event()
self.server = NullServer()
- self.assertTrue(not self.event.isSet())
+ self.assertTrue(not self.event.is_set())
self.ts.start_server(self.event, self.server)
def verify_finished(self):
self.event.wait(1.0)
- self.assertTrue(self.event.isSet())
+ self.assertTrue(self.event.is_set())
self.assertTrue(self.ts.is_active())
def test_1_bad_auth_type(self):
diff --git a/tests/test_buffered_pipe.py b/tests/test_buffered_pipe.py
index a53081a9..eeb4d0ad 100644
--- a/tests/test_buffered_pipe.py
+++ b/tests/test_buffered_pipe.py
@@ -22,10 +22,10 @@ Some unit tests for BufferedPipe.
import threading
import time
+import unittest
from paramiko.buffered_pipe import BufferedPipe, PipeTimeout
from paramiko import pipe
-
-from tests.util import ParamikoTest
+from paramiko.py3compat import b
def delay_thread(p):
@@ -40,7 +40,7 @@ def close_thread(p):
p.close()
-class BufferedPipeTest(ParamikoTest):
+class BufferedPipeTest(unittest.TestCase):
def test_1_buffered_pipe(self):
p = BufferedPipe()
self.assertTrue(not p.read_ready())
@@ -48,12 +48,12 @@ class BufferedPipeTest(ParamikoTest):
self.assertTrue(p.read_ready())
data = p.read(6)
self.assertEqual(b'hello.', data)
-
+
p.feed('plus/minus')
self.assertEqual(b'plu', p.read(3))
self.assertEqual(b's/m', p.read(3))
self.assertEqual(b'inus', p.read(4))
-
+
p.close()
self.assertTrue(not p.read_ready())
self.assertEqual(b'', p.read(1))
diff --git a/tests/test_client.py b/tests/test_client.py
index 1791bed6..04cab439 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -20,6 +20,8 @@
Some unit tests for SSHClient.
"""
+from __future__ import with_statement
+
import socket
from tempfile import mkstemp
import threading
@@ -27,12 +29,25 @@ import unittest
import weakref
import warnings
import os
+import time
from tests.util import test_path
import paramiko
-from paramiko.common import PY2
+from paramiko.common import PY2, b
+from paramiko.ssh_exception import SSHException
+
+
+FINGERPRINTS = {
+ 'ssh-dss': b'\x44\x78\xf0\xb9\xa2\x3c\xc5\x18\x20\x09\xff\x75\x5b\xc1\xd2\x6c',
+ 'ssh-rsa': b'\x60\x73\x38\x44\xcb\x51\x86\x65\x7f\xde\xda\xa2\x2b\x5a\x57\xd5',
+ 'ecdsa-sha2-nistp256': b'\x25\x19\xeb\x55\xe6\xa1\x47\xff\x4f\x38\xd2\x75\x6f\xa5\xd5\x60',
+}
class NullServer (paramiko.ServerInterface):
+ def __init__(self, *args, **kwargs):
+ # Allow tests to enable/disable specific key types
+ self.__allowed_keys = kwargs.pop('allowed_keys', [])
+ super(NullServer, self).__init__(*args, **kwargs)
def get_allowed_auths(self, username):
if username == 'slowdive':
@@ -45,7 +60,14 @@ class NullServer (paramiko.ServerInterface):
return paramiko.AUTH_FAILED
def check_auth_publickey(self, username, key):
- if (key.get_name() == 'ssh-dss') and key.get_fingerprint() == b'\x44\x78\xf0\xb9\xa2\x3c\xc5\x18\x20\x09\xff\x75\x5b\xc1\xd2\x6c':
+ try:
+ expected = FINGERPRINTS[key.get_name()]
+ except KeyError:
+ return paramiko.AUTH_FAILED
+ if (
+ key.get_name() in self.__allowed_keys and
+ key.get_fingerprint() == expected
+ ):
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
@@ -72,32 +94,46 @@ class SSHClientTest (unittest.TestCase):
if hasattr(self, attr):
getattr(self, attr).close()
- def _run(self):
+ def _run(self, allowed_keys=None, delay=0):
+ if allowed_keys is None:
+ allowed_keys = FINGERPRINTS.keys()
self.socks, addr = self.sockl.accept()
self.ts = paramiko.Transport(self.socks)
host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key'))
self.ts.add_server_key(host_key)
- server = NullServer()
+ server = NullServer(allowed_keys=allowed_keys)
+ if delay:
+ time.sleep(delay)
self.ts.start_server(self.event, server)
- def test_1_client(self):
+ def _test_connection(self, **kwargs):
"""
- verify that the SSHClient stuff works too.
+ (Most) kwargs get passed directly into SSHClient.connect().
+
+ The exception is ``allowed_keys`` which is stripped and handed to the
+ ``NullServer`` used for testing.
"""
- threading.Thread(target=self._run).start()
+ run_kwargs = {'allowed_keys': kwargs.pop('allowed_keys', None)}
+ # Server setup
+ threading.Thread(target=self._run, kwargs=run_kwargs).start()
host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key'))
public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+ # Client setup
self.tc = paramiko.SSHClient()
self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key)
- self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
+ # Actual connection
+ self.tc.connect(self.addr, self.port, username='slowdive', **kwargs)
+
+ # Authentication successful?
self.event.wait(1.0)
- self.assertTrue(self.event.isSet())
+ self.assertTrue(self.event.is_set())
self.assertTrue(self.ts.is_active())
self.assertEqual('slowdive', self.ts.get_username())
self.assertEqual(True, self.ts.is_authenticated())
+ # Command execution functions?
stdin, stdout, stderr = self.tc.exec_command('yes')
schan = self.ts.accept(1.0)
@@ -110,61 +146,71 @@ class SSHClientTest (unittest.TestCase):
self.assertEqual('This is on stderr.\n', stderr.readline())
self.assertEqual('', stderr.readline())
+ # Cleanup
stdin.close()
stdout.close()
stderr.close()
+ def test_1_client(self):
+ """
+ verify that the SSHClient stuff works too.
+ """
+ self._test_connection(password='pygmalion')
+
def test_2_client_dsa(self):
"""
verify that SSHClient works with a DSA key.
"""
- threading.Thread(target=self._run).start()
- host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key'))
- public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+ self._test_connection(key_filename=test_path('test_dss.key'))
- self.tc = paramiko.SSHClient()
- self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key)
- self.tc.connect(self.addr, self.port, username='slowdive', key_filename=test_path('test_dss.key'))
-
- self.event.wait(1.0)
- self.assertTrue(self.event.isSet())
- self.assertTrue(self.ts.is_active())
- self.assertEqual('slowdive', self.ts.get_username())
- self.assertEqual(True, self.ts.is_authenticated())
-
- stdin, stdout, stderr = self.tc.exec_command('yes')
- schan = self.ts.accept(1.0)
-
- schan.send('Hello there.\n')
- schan.send_stderr('This is on stderr.\n')
- schan.close()
-
- self.assertEqual('Hello there.\n', stdout.readline())
- self.assertEqual('', stdout.readline())
- self.assertEqual('This is on stderr.\n', stderr.readline())
- self.assertEqual('', stderr.readline())
+ def test_client_rsa(self):
+ """
+ verify that SSHClient works with an RSA key.
+ """
+ self._test_connection(key_filename=test_path('test_rsa.key'))
- stdin.close()
- stdout.close()
- stderr.close()
+ def test_2_5_client_ecdsa(self):
+ """
+ verify that SSHClient works with an ECDSA key.
+ """
+ self._test_connection(key_filename=test_path('test_ecdsa.key'))
def test_3_multiple_key_files(self):
"""
verify that SSHClient accepts and tries multiple key files.
"""
- threading.Thread(target=self._run).start()
- host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key'))
- public_host_key = paramiko.RSAKey(data=host_key.asbytes())
-
- self.tc = paramiko.SSHClient()
- self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key)
- self.tc.connect(self.addr, self.port, username='slowdive', key_filename=[test_path('test_rsa.key'), test_path('test_dss.key')])
-
- self.event.wait(1.0)
- self.assertTrue(self.event.isSet())
- self.assertTrue(self.ts.is_active())
- self.assertEqual('slowdive', self.ts.get_username())
- self.assertEqual(True, self.ts.is_authenticated())
+ # This is dumb :(
+ types_ = {
+ 'rsa': 'ssh-rsa',
+ 'dss': 'ssh-dss',
+ 'ecdsa': 'ecdsa-sha2-nistp256',
+ }
+ # Various combos of attempted & valid keys
+ # TODO: try every possible combo using itertools functions
+ for attempt, accept in (
+ (['rsa', 'dss'], ['dss']), # Original test #3
+ (['dss', 'rsa'], ['dss']), # Ordering matters sometimes, sadly
+ (['dss', 'rsa', 'ecdsa'], ['dss']), # Try ECDSA but fail
+ (['rsa', 'ecdsa'], ['ecdsa']), # ECDSA success
+ ):
+ self._test_connection(
+ key_filename=[
+ test_path('test_{0}.key'.format(x)) for x in attempt
+ ],
+ allowed_keys=[types_[x] for x in accept],
+ )
+
+ def test_multiple_key_files_failure(self):
+ """
+ Expect failure when multiple keys in play and none are accepted
+ """
+ # Until #387 is fixed we have to catch a high-up exception since
+ # various platforms trigger different errors here >_<
+ self.assertRaises(SSHException,
+ self._test_connection,
+ key_filename=[test_path('test_rsa.key')],
+ allowed_keys=['ecdsa-sha2-nistp256'],
+ )
def test_4_auto_add_policy(self):
"""
@@ -180,7 +226,7 @@ class SSHClientTest (unittest.TestCase):
self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
self.event.wait(1.0)
- self.assertTrue(self.event.isSet())
+ self.assertTrue(self.event.is_set())
self.assertTrue(self.ts.is_active())
self.assertEqual('slowdive', self.ts.get_username())
self.assertEqual(True, self.ts.is_authenticated())
@@ -221,6 +267,8 @@ class SSHClientTest (unittest.TestCase):
"""
# Unclear why this is borked on Py3, but it is, and does not seem worth
# pursuing at the moment.
+ # XXX: It's the release of the references to e.g packetizer that fails
+ # in py3...
if not PY2:
return
threading.Thread(target=self._run).start()
@@ -233,7 +281,7 @@ class SSHClientTest (unittest.TestCase):
self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
self.event.wait(1.0)
- self.assertTrue(self.event.isSet())
+ self.assertTrue(self.event.is_set())
self.assertTrue(self.ts.is_active())
p = weakref.ref(self.tc._transport.packetizer)
@@ -252,3 +300,47 @@ class SSHClientTest (unittest.TestCase):
gc.collect()
self.assertTrue(p() is None)
+
+ def test_client_can_be_used_as_context_manager(self):
+ """
+ verify that an SSHClient can be used a context manager
+ """
+ threading.Thread(target=self._run).start()
+ host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key'))
+ public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+
+ with paramiko.SSHClient() as tc:
+ self.tc = tc
+ self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ self.assertEquals(0, len(self.tc.get_host_keys()))
+ self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
+
+ self.event.wait(1.0)
+ self.assertTrue(self.event.is_set())
+ self.assertTrue(self.ts.is_active())
+
+ self.assertTrue(self.tc._transport is not None)
+
+ self.assertTrue(self.tc._transport is None)
+
+ def test_7_banner_timeout(self):
+ """
+ verify that the SSHClient has a configurable banner timeout.
+ """
+ # Start the thread with a 1 second wait.
+ threading.Thread(target=self._run, kwargs={'delay': 1}).start()
+ host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key'))
+ public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+
+ self.tc = paramiko.SSHClient()
+ self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key)
+ # Connect with a half second banner timeout.
+ self.assertRaises(
+ paramiko.SSHException,
+ self.tc.connect,
+ self.addr,
+ self.port,
+ username='slowdive',
+ password='pygmalion',
+ banner_timeout=0.5
+ )
diff --git a/tests/test_gssapi.py b/tests/test_gssapi.py
new file mode 100644
index 00000000..96c268d9
--- /dev/null
+++ b/tests/test_gssapi.py
@@ -0,0 +1,135 @@
+# Copyright (C) 2013-2014 science + computing ag
+# Author: Sebastian Deiss <sebastian.deiss@t-online.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.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+"""
+Test the used APIs for GSS-API / SSPI authentication
+"""
+
+import unittest
+import socket
+
+
+class GSSAPITest(unittest.TestCase):
+ @staticmethod
+ def init(hostname=None, srv_mode=False):
+ global krb5_mech, targ_name, server_mode
+ krb5_mech = "1.2.840.113554.1.2.2"
+ targ_name = hostname
+ server_mode = srv_mode
+
+ def test_1_pyasn1(self):
+ """
+ Test the used methods of pyasn1.
+ """
+ from pyasn1.type.univ import ObjectIdentifier
+ from pyasn1.codec.der import encoder, decoder
+ oid = encoder.encode(ObjectIdentifier(krb5_mech))
+ mech, __ = decoder.decode(oid)
+ self.assertEquals(krb5_mech, mech.__str__())
+
+ def test_2_gssapi_sspi(self):
+ """
+ Test the used methods of python-gssapi or sspi, sspicon from pywin32.
+ """
+ _API = "MIT"
+ try:
+ import gssapi
+ except ImportError:
+ import sspicon
+ import sspi
+ _API = "SSPI"
+
+ c_token = None
+ gss_ctxt_status = False
+ mic_msg = b"G'day Mate!"
+
+ if _API == "MIT":
+ if server_mode:
+ gss_flags = (gssapi.C_PROT_READY_FLAG,
+ gssapi.C_INTEG_FLAG,
+ gssapi.C_MUTUAL_FLAG,
+ gssapi.C_DELEG_FLAG)
+ else:
+ gss_flags = (gssapi.C_PROT_READY_FLAG,
+ gssapi.C_INTEG_FLAG,
+ gssapi.C_DELEG_FLAG)
+ # Initialize a GSS-API context.
+ ctx = gssapi.Context()
+ ctx.flags = gss_flags
+ krb5_oid = gssapi.OID.mech_from_string(krb5_mech)
+ target_name = gssapi.Name("host@" + targ_name,
+ gssapi.C_NT_HOSTBASED_SERVICE)
+ gss_ctxt = gssapi.InitContext(peer_name=target_name,
+ mech_type=krb5_oid,
+ req_flags=ctx.flags)
+ if server_mode:
+ c_token = gss_ctxt.step(c_token)
+ gss_ctxt_status = gss_ctxt.established
+ self.assertEquals(False, gss_ctxt_status)
+ # Accept a GSS-API context.
+ gss_srv_ctxt = gssapi.AcceptContext()
+ s_token = gss_srv_ctxt.step(c_token)
+ gss_ctxt_status = gss_srv_ctxt.established
+ self.assertNotEquals(None, s_token)
+ self.assertEquals(True, gss_ctxt_status)
+ # Establish the client context
+ c_token = gss_ctxt.step(s_token)
+ self.assertEquals(None, c_token)
+ else:
+ while not gss_ctxt.established:
+ c_token = gss_ctxt.step(c_token)
+ self.assertNotEquals(None, c_token)
+ # Build MIC
+ mic_token = gss_ctxt.get_mic(mic_msg)
+
+ if server_mode:
+ # Check MIC
+ status = gss_srv_ctxt.verify_mic(mic_msg, mic_token)
+ self.assertEquals(0, status)
+ else:
+ gss_flags = sspicon.ISC_REQ_INTEGRITY |\
+ sspicon.ISC_REQ_MUTUAL_AUTH |\
+ sspicon.ISC_REQ_DELEGATE
+ # Initialize a GSS-API context.
+ target_name = "host/" + socket.getfqdn(targ_name)
+ gss_ctxt = sspi.ClientAuth("Kerberos",
+ scflags=gss_flags,
+ targetspn=target_name)
+ if server_mode:
+ error, token = gss_ctxt.authorize(c_token)
+ c_token = token[0].Buffer
+ self.assertEquals(0, error)
+ # Accept a GSS-API context.
+ gss_srv_ctxt = sspi.ServerAuth("Kerberos", spn=target_name)
+ error, token = gss_srv_ctxt.authorize(c_token)
+ s_token = token[0].Buffer
+ # Establish the context.
+ error, token = gss_ctxt.authorize(s_token)
+ c_token = token[0].Buffer
+ self.assertEquals(None, c_token)
+ self.assertEquals(0, error)
+ # Build MIC
+ mic_token = gss_ctxt.sign(mic_msg)
+ # Check MIC
+ gss_srv_ctxt.verify(mic_msg, mic_token)
+ else:
+ error, token = gss_ctxt.authorize(c_token)
+ c_token = token[0].Buffer
+ self.assertNotEquals(0, error)
diff --git a/tests/test_hostkeys.py b/tests/test_hostkeys.py
index 0ee1bbf0..2bdcad9c 100644
--- a/tests/test_hostkeys.py
+++ b/tests/test_hostkeys.py
@@ -31,6 +31,7 @@ test_hosts_file = """\
secure.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA1PD6U2/TVxET6lkpKhOk5r\
9q/kAYG6sP9f5zuUYP8i7FOFp/6ncCEbbtg/lB+A3iidyxoSWl+9jtoyyDOOVX4UIDV9G11Ml8om3\
D+jrpI9cycZHqilK0HmxDeCuxbwyMuaCygU9gS2qoRvNLWZk70OpIKSSpBo0Wl3/XUmz9uhc=
+broken.example.com ssh-rsa AAAA
happy.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31M\
BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\
5ymME3bQ4J/k1IKxCtz/bAlAqFgKoc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M=
diff --git a/tests/test_kex.py b/tests/test_kex.py
index c522be46..19804fbf 100644
--- a/tests/test_kex.py
+++ b/tests/test_kex.py
@@ -21,17 +21,18 @@ Some unit tests for the key exchange protocols.
"""
from binascii import hexlify
+import os
import unittest
+
import paramiko.util
from paramiko.kex_group1 import KexGroup1
-from paramiko.kex_gex import KexGex
+from paramiko.kex_gex import KexGex, KexGexSHA256
from paramiko import Message
from paramiko.common import byte_chr
-class FakeRng (object):
- def read(self, n):
- return byte_chr(0xcc) * n
+def dummy_urandom(n):
+ return byte_chr(0xcc) * n
class FakeKey (object):
@@ -41,7 +42,7 @@ class FakeKey (object):
def asbytes(self):
return b'fake-key'
- def sign_ssh_data(self, rng, H):
+ def sign_ssh_data(self, H):
return b'fake-sig'
@@ -53,8 +54,7 @@ class FakeModulusPack (object):
return self.G, self.P
-class FakeTransport (object):
- rng = FakeRng()
+class FakeTransport(object):
local_version = 'SSH-2.0-paramiko_1.0'
remote_version = 'SSH-2.0-lame'
local_kex_init = 'local-kex-init'
@@ -91,10 +91,11 @@ class KexTest (unittest.TestCase):
K = 14730343317708716439807310032871972459448364195094179797249681733965528989482751523943515690110179031004049109375612685505881911274101441415545039654102474376472240501616988799699744135291070488314748284283496055223852115360852283821334858541043710301057312858051901453919067023103730011648890038847384890504
def setUp(self):
- pass
+ self._original_urandom = os.urandom
+ os.urandom = dummy_urandom
def tearDown(self):
- pass
+ os.urandom = self._original_urandom
def test_1_group1_client(self):
transport = FakeTransport()
@@ -251,3 +252,121 @@ class KexTest (unittest.TestCase):
self.assertEqual(H, hexlify(transport._H).upper())
self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
self.assertTrue(transport._activated)
+
+ def test_7_gex_sha256_client(self):
+ transport = FakeTransport()
+ transport.server_mode = False
+ kex = KexGexSHA256(transport)
+ kex.start_kex()
+ x = b'22000004000000080000002000'
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect)
+
+ msg = Message()
+ msg.add_mpint(FakeModulusPack.P)
+ msg.add_mpint(FakeModulusPack.G)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg)
+ x = b'20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4'
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect)
+
+ msg = Message()
+ msg.add_string('fake-host-key')
+ msg.add_mpint(69)
+ msg.add_string('fake-sig')
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg)
+ H = b'AD1A9365A67B4496F05594AD1BF656E3CDA0851289A4C1AFF549FEAE50896DF4'
+ self.assertEqual(self.K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual((b'fake-host-key', b'fake-sig'), transport._verify)
+ self.assertTrue(transport._activated)
+
+ def test_8_gex_sha256_old_client(self):
+ transport = FakeTransport()
+ transport.server_mode = False
+ kex = KexGexSHA256(transport)
+ kex.start_kex(_test_old_style=True)
+ x = b'1E00000800'
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect)
+
+ msg = Message()
+ msg.add_mpint(FakeModulusPack.P)
+ msg.add_mpint(FakeModulusPack.G)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg)
+ x = b'20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4'
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect)
+
+ msg = Message()
+ msg.add_string('fake-host-key')
+ msg.add_mpint(69)
+ msg.add_string('fake-sig')
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg)
+ H = b'518386608B15891AE5237DEE08DCADDE76A0BCEFCE7F6DB3AD66BC41D256DFE5'
+ self.assertEqual(self.K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual((b'fake-host-key', b'fake-sig'), transport._verify)
+ self.assertTrue(transport._activated)
+
+ def test_9_gex_sha256_server(self):
+ transport = FakeTransport()
+ transport.server_mode = True
+ kex = KexGexSHA256(transport)
+ kex.start_kex()
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect)
+
+ msg = Message()
+ msg.add_int(1024)
+ msg.add_int(2048)
+ msg.add_int(4096)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, msg)
+ x = b'1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102'
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect)
+
+ msg = Message()
+ msg.add_mpint(12345)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg)
+ K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581
+ H = b'CCAC0497CF0ABA1DBF55E1A3995D17F4CC31824B0E8D95CDF8A06F169D050D80'
+ x = b'210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967'
+ self.assertEqual(K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertTrue(transport._activated)
+
+ def test_10_gex_sha256_server_with_old_client(self):
+ transport = FakeTransport()
+ transport.server_mode = True
+ kex = KexGexSHA256(transport)
+ kex.start_kex()
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect)
+
+ msg = Message()
+ msg.add_int(2048)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD, msg)
+ x = b'1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102'
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect)
+
+ msg = Message()
+ msg.add_mpint(12345)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg)
+ K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581
+ H = b'3DDD2AD840AD095E397BA4D0573972DC60F6461FD38A187CACA6615A5BC8ADBB'
+ x = b'210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967'
+ self.assertEqual(K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertTrue(transport._activated)
+
+
diff --git a/tests/test_kex_gss.py b/tests/test_kex_gss.py
new file mode 100644
index 00000000..3bf788da
--- /dev/null
+++ b/tests/test_kex_gss.py
@@ -0,0 +1,131 @@
+# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
+# Copyright (C) 2013-2014 science + computing ag
+# Author: Sebastian Deiss <sebastian.deiss@t-online.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.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+"""
+Unit Tests for the GSS-API / SSPI SSHv2 Diffie-Hellman Key Exchange and user
+authentication
+"""
+
+
+import socket
+import threading
+import unittest
+
+import paramiko
+
+
+class NullServer (paramiko.ServerInterface):
+
+ def get_allowed_auths(self, username):
+ return 'gssapi-keyex'
+
+ def check_auth_gssapi_keyex(self, username,
+ gss_authenticated=paramiko.AUTH_FAILED,
+ cc_file=None):
+ if gss_authenticated == paramiko.AUTH_SUCCESSFUL:
+ return paramiko.AUTH_SUCCESSFUL
+ return paramiko.AUTH_FAILED
+
+ def enable_auth_gssapi(self):
+ UseGSSAPI = True
+ return UseGSSAPI
+
+ def check_channel_request(self, kind, chanid):
+ return paramiko.OPEN_SUCCEEDED
+
+ def check_channel_exec_request(self, channel, command):
+ if command != 'yes':
+ return False
+ return True
+
+
+class GSSKexTest(unittest.TestCase):
+ @staticmethod
+ def init(username, hostname):
+ global krb5_principal, targ_name
+ krb5_principal = username
+ targ_name = hostname
+
+ def setUp(self):
+ self.username = krb5_principal
+ self.hostname = socket.getfqdn(targ_name)
+ self.sockl = socket.socket()
+ self.sockl.bind((targ_name, 0))
+ self.sockl.listen(1)
+ self.addr, self.port = self.sockl.getsockname()
+ self.event = threading.Event()
+ thread = threading.Thread(target=self._run)
+ thread.start()
+
+ def tearDown(self):
+ for attr in "tc ts socks sockl".split():
+ if hasattr(self, attr):
+ getattr(self, attr).close()
+
+ def _run(self):
+ self.socks, addr = self.sockl.accept()
+ self.ts = paramiko.Transport(self.socks, gss_kex=True)
+ host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key')
+ self.ts.add_server_key(host_key)
+ self.ts.set_gss_host(targ_name)
+ try:
+ self.ts.load_server_moduli()
+ except:
+ print ('(Failed to load moduli -- gex will be unsupported.)')
+ server = NullServer()
+ self.ts.start_server(self.event, server)
+
+ def test_1_gsskex_and_auth(self):
+ """
+ Verify that Paramiko can handle SSHv2 GSS-API / SSPI authenticated
+ Diffie-Hellman Key Exchange and user authentication with the GSS-API
+ context created during key exchange.
+ """
+ host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key')
+ public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+
+ self.tc = paramiko.SSHClient()
+ self.tc.get_host_keys().add('[%s]:%d' % (self.hostname, self.port),
+ 'ssh-rsa', public_host_key)
+ self.tc.connect(self.hostname, self.port, username=self.username,
+ gss_auth=True, gss_kex=True)
+
+ self.event.wait(1.0)
+ self.assert_(self.event.is_set())
+ self.assert_(self.ts.is_active())
+ self.assertEquals(self.username, self.ts.get_username())
+ self.assertEquals(True, self.ts.is_authenticated())
+
+ stdin, stdout, stderr = self.tc.exec_command('yes')
+ schan = self.ts.accept(1.0)
+
+ schan.send('Hello there.\n')
+ schan.send_stderr('This is on stderr.\n')
+ schan.close()
+
+ self.assertEquals('Hello there.\n', stdout.readline())
+ self.assertEquals('', stdout.readline())
+ self.assertEquals('This is on stderr.\n', stderr.readline())
+ self.assertEquals('', stderr.readline())
+
+ stdin.close()
+ stdout.close()
+ stderr.close()
diff --git a/tests/test_message.py b/tests/test_message.py
index f308c037..f18cae90 100644
--- a/tests/test_message.py
+++ b/tests/test_message.py
@@ -92,12 +92,12 @@ class MessageTest (unittest.TestCase):
def test_4_misc(self):
msg = Message(self.__d)
- self.assertEqual(msg.get_int(), 5)
- self.assertEqual(msg.get_int(), 0x1122334455)
- self.assertEqual(msg.get_int(), 0xf00000000000000000)
+ self.assertEqual(msg.get_adaptive_int(), 5)
+ self.assertEqual(msg.get_adaptive_int(), 0x1122334455)
+ self.assertEqual(msg.get_adaptive_int(), 0xf00000000000000000)
self.assertEqual(msg.get_so_far(), self.__d[:29])
self.assertEqual(msg.get_remainder(), self.__d[29:])
msg.rewind()
- self.assertEqual(msg.get_int(), 5)
+ self.assertEqual(msg.get_adaptive_int(), 5)
self.assertEqual(msg.get_so_far(), self.__d[:4])
self.assertEqual(msg.get_remainder(), self.__d[4:])
diff --git a/tests/test_packetizer.py b/tests/test_packetizer.py
index d4d5544e..8faec03c 100644
--- a/tests/test_packetizer.py
+++ b/tests/test_packetizer.py
@@ -21,9 +21,12 @@ Some unit tests for the ssh2 protocol in Transport.
"""
import unittest
+from hashlib import sha1
+
from tests.loop import LoopSocket
+
from Crypto.Cipher import AES
-from Crypto.Hash import SHA
+
from paramiko import Message, Packetizer, util
from paramiko.common import byte_chr, zero_byte
@@ -41,7 +44,7 @@ class PacketizerTest (unittest.TestCase):
p.set_log(util.get_logger('paramiko.transport'))
p.set_hexdump(True)
cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16)
- p.set_outbound_cipher(cipher, 16, SHA, 12, x1f * 20)
+ p.set_outbound_cipher(cipher, 16, sha1, 12, x1f * 20)
# message has to be at least 16 bytes long, so we'll have at least one
# block of data encrypted that contains zero random padding bytes
@@ -64,10 +67,56 @@ class PacketizerTest (unittest.TestCase):
p.set_log(util.get_logger('paramiko.transport'))
p.set_hexdump(True)
cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16)
- p.set_inbound_cipher(cipher, 16, SHA, 12, x1f * 20)
+ p.set_inbound_cipher(cipher, 16, sha1, 12, x1f * 20)
wsock.send(b'\x43\x91\x97\xbd\x5b\x50\xac\x25\x87\xc2\xc4\x6b\xc7\xe9\x38\xc0\x90\xd2\x16\x56\x0d\x71\x73\x61\x38\x7c\x4c\x3d\xfb\x97\x7d\xe2\x6e\x03\xb1\xa0\xc2\x1c\xd6\x41\x41\x4c\xb4\x59')
cmd, m = p.read_message()
self.assertEqual(100, cmd)
self.assertEqual(100, m.get_int())
self.assertEqual(1, m.get_int())
self.assertEqual(900, m.get_int())
+
+ def test_3_closed(self):
+ rsock = LoopSocket()
+ wsock = LoopSocket()
+ rsock.link(wsock)
+ p = Packetizer(wsock)
+ p.set_log(util.get_logger('paramiko.transport'))
+ p.set_hexdump(True)
+ cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16)
+ p.set_outbound_cipher(cipher, 16, sha1, 12, x1f * 20)
+
+ # message has to be at least 16 bytes long, so we'll have at least one
+ # block of data encrypted that contains zero random padding bytes
+ m = Message()
+ m.add_byte(byte_chr(100))
+ m.add_int(100)
+ m.add_int(1)
+ m.add_int(900)
+ wsock.send = lambda x: 0
+ from functools import wraps
+ import errno
+ import os
+ import signal
+
+ class TimeoutError(Exception):
+ pass
+
+ def timeout(seconds=1, error_message=os.strerror(errno.ETIME)):
+ def decorator(func):
+ def _handle_timeout(signum, frame):
+ raise TimeoutError(error_message)
+
+ def wrapper(*args, **kwargs):
+ signal.signal(signal.SIGALRM, _handle_timeout)
+ signal.alarm(seconds)
+ try:
+ result = func(*args, **kwargs)
+ finally:
+ signal.alarm(0)
+ return result
+
+ return wraps(func)(wrapper)
+
+ return decorator
+ send = timeout()(p.send_message)
+ self.assertRaises(EOFError, send, m)
diff --git a/tests/test_pkey.py b/tests/test_pkey.py
index 6ff68fc2..f673254f 100644
--- a/tests/test_pkey.py
+++ b/tests/test_pkey.py
@@ -20,11 +20,14 @@
Some unit tests for public/private key objects.
"""
-from binascii import hexlify
import unittest
+import os
+from binascii import hexlify
+from hashlib import md5
+
from paramiko import RSAKey, DSSKey, ECDSAKey, Message, util
from paramiko.py3compat import StringIO, byte_chr, b, bytes
-from paramiko.common import rng
+
from tests.util import test_path
# from openssh's ssh-keygen
@@ -90,8 +93,7 @@ class KeyTest (unittest.TestCase):
pass
def test_1_generate_key_bytes(self):
- from Crypto.Hash import MD5
- key = util.generate_key_bytes(MD5, x1234, 'happy birthday', 30)
+ key = util.generate_key_bytes(md5, x1234, 'happy birthday', 30)
exp = b'\x61\xE1\xF2\x72\xF4\xC1\xC4\x56\x15\x86\xBD\x32\x24\x98\xC0\xE9\x24\x67\x27\x80\xF4\x7B\xB3\x7D\xDA\x7D\x54\x01\x9E\x64'
self.assertEqual(exp, key)
@@ -166,7 +168,7 @@ class KeyTest (unittest.TestCase):
def test_8_sign_rsa(self):
# verify that the rsa private key can sign and verify
key = RSAKey.from_private_key_file(test_path('test_rsa.key'))
- msg = key.sign_ssh_data(rng, b'ice weasels')
+ msg = key.sign_ssh_data(b'ice weasels')
self.assertTrue(type(msg) is Message)
msg.rewind()
self.assertEqual('ssh-rsa', msg.get_text())
@@ -179,7 +181,7 @@ class KeyTest (unittest.TestCase):
def test_9_sign_dss(self):
# verify that the dss private key can sign and verify
key = DSSKey.from_private_key_file(test_path('test_dss.key'))
- msg = key.sign_ssh_data(rng, b'ice weasels')
+ msg = key.sign_ssh_data(b'ice weasels')
self.assertTrue(type(msg) is Message)
msg.rewind()
self.assertEqual('ssh-dss', msg.get_text())
@@ -193,13 +195,13 @@ class KeyTest (unittest.TestCase):
def test_A_generate_rsa(self):
key = RSAKey.generate(1024)
- msg = key.sign_ssh_data(rng, b'jerri blank')
+ msg = key.sign_ssh_data(b'jerri blank')
msg.rewind()
self.assertTrue(key.verify_ssh_sig(b'jerri blank', msg))
def test_B_generate_dss(self):
key = DSSKey.generate(1024)
- msg = key.sign_ssh_data(rng, b'jerri blank')
+ msg = key.sign_ssh_data(b'jerri blank')
msg.rewind()
self.assertTrue(key.verify_ssh_sig(b'jerri blank', msg))
@@ -240,7 +242,7 @@ class KeyTest (unittest.TestCase):
def test_13_sign_ecdsa(self):
# verify that the rsa private key can sign and verify
key = ECDSAKey.from_private_key_file(test_path('test_ecdsa.key'))
- msg = key.sign_ssh_data(rng, b'ice weasels')
+ msg = key.sign_ssh_data(b'ice weasels')
self.assertTrue(type(msg) is Message)
msg.rewind()
self.assertEqual('ecdsa-sha2-nistp256', msg.get_text())
@@ -252,3 +254,20 @@ class KeyTest (unittest.TestCase):
msg.rewind()
pub = ECDSAKey(data=key.asbytes())
self.assertTrue(pub.verify_ssh_sig(b'ice weasels', msg))
+
+ def test_salt_size(self):
+ # Read an existing encrypted private key
+ file_ = test_path('test_rsa_password.key')
+ password = 'television'
+ newfile = file_ + '.new'
+ newpassword = 'radio'
+ key = RSAKey(filename=file_, password=password)
+ # Write out a newly re-encrypted copy with a new password.
+ # When the bug under test exists, this will ValueError.
+ try:
+ key.write_private_key_file(newfile, password=newpassword)
+ # Verify the inner key data still matches (when no ValueError)
+ key2 = RSAKey(filename=newfile, password=newpassword)
+ self.assertEqual(key, key2)
+ finally:
+ os.remove(newfile)
diff --git a/tests/test_sftp.py b/tests/test_sftp.py
index 2b6aa3b6..aa450f59 100755
--- a/tests/test_sftp.py
+++ b/tests/test_sftp.py
@@ -23,12 +23,13 @@ a real actual sftp server is contacted, and a new folder is created there to
do test file operations in (so no existing files will be harmed).
"""
-from binascii import hexlify
import os
+import socket
import sys
-import warnings
import threading
import unittest
+import warnings
+from binascii import hexlify
from tempfile import mkstemp
import paramiko
@@ -96,7 +97,7 @@ def get_sftp():
class SFTPTest (unittest.TestCase):
-
+ @staticmethod
def init(hostname, username, keyfile, passwd):
global sftp, tc
@@ -128,8 +129,8 @@ class SFTPTest (unittest.TestCase):
sys.stderr.write('\n')
sys.exit(1)
sftp = paramiko.SFTP.from_transport(t)
- init = staticmethod(init)
+ @staticmethod
def init_loopback():
global sftp, tc
@@ -149,12 +150,11 @@ class SFTPTest (unittest.TestCase):
event.wait(1.0)
sftp = paramiko.SFTP.from_transport(tc)
- init_loopback = staticmethod(init_loopback)
+ @staticmethod
def set_big_file_test(onoff):
global g_big_file_test
g_big_file_test = onoff
- set_big_file_test = staticmethod(set_big_file_test)
def setUp(self):
global FOLDER
@@ -195,6 +195,21 @@ class SFTPTest (unittest.TestCase):
pass
sftp = paramiko.SFTP.from_transport(tc)
+ def test_2_sftp_can_be_used_as_context_manager(self):
+ """
+ verify that the sftp session is closed when exiting the context manager
+ """
+ global sftp
+ with sftp:
+ pass
+ try:
+ sftp.open(FOLDER + '/test2', 'w')
+ self.fail('expected exception')
+ except (EOFError, socket.error):
+ pass
+ finally:
+ sftp = paramiko.SFTP.from_transport(tc)
+
def test_3_write(self):
"""
verify that a file can be created and written, and the size is correct.
@@ -279,8 +294,8 @@ class SFTPTest (unittest.TestCase):
def test_7_listdir(self):
"""
- verify that a folder can be created, a bunch of files can be placed in it,
- and those files show up in sftp.listdir.
+ verify that a folder can be created, a bunch of files can be placed in
+ it, and those files show up in sftp.listdir.
"""
try:
sftp.open(FOLDER + '/duck.txt', 'w').close()
@@ -298,6 +313,26 @@ class SFTPTest (unittest.TestCase):
sftp.remove(FOLDER + '/fish.txt')
sftp.remove(FOLDER + '/tertiary.py')
+ def test_7_5_listdir_iter(self):
+ """
+ listdir_iter version of above test
+ """
+ try:
+ sftp.open(FOLDER + '/duck.txt', 'w').close()
+ sftp.open(FOLDER + '/fish.txt', 'w').close()
+ sftp.open(FOLDER + '/tertiary.py', 'w').close()
+
+ x = [x.filename for x in sftp.listdir_iter(FOLDER)]
+ self.assertEqual(len(x), 3)
+ self.assertTrue('duck.txt' in x)
+ self.assertTrue('fish.txt' in x)
+ self.assertTrue('tertiary.py' in x)
+ self.assertTrue('random' not in x)
+ finally:
+ sftp.remove(FOLDER + '/duck.txt')
+ sftp.remove(FOLDER + '/fish.txt')
+ sftp.remove(FOLDER + '/tertiary.py')
+
def test_8_setstat(self):
"""
verify that the setstat functions (chown, chmod, utime, truncate) work.
@@ -776,6 +811,11 @@ class SFTPTest (unittest.TestCase):
sftp.remove('%s/nonutf8data' % FOLDER)
+ def test_sftp_attributes_empty_str(self):
+ sftp_attributes = SFTPAttributes()
+ self.assertEqual(str(sftp_attributes), "?--------- 1 0 0 0 (unknown date) ?")
+
+
if __name__ == '__main__':
SFTPTest.init_loopback()
# logging is required by test_N_file_with_percent
diff --git a/tests/test_ssh_gss.py b/tests/test_ssh_gss.py
new file mode 100644
index 00000000..e20d348f
--- /dev/null
+++ b/tests/test_ssh_gss.py
@@ -0,0 +1,124 @@
+# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
+# Copyright (C) 2013-2014 science + computing ag
+# Author: Sebastian Deiss <sebastian.deiss@t-online.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.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+"""
+Unit Tests for the GSS-API / SSPI SSHv2 Authentication (gssapi-with-mic)
+"""
+
+import socket
+import threading
+import unittest
+
+import paramiko
+
+
+class NullServer (paramiko.ServerInterface):
+
+ def get_allowed_auths(self, username):
+ return 'gssapi-with-mic'
+
+ def check_auth_gssapi_with_mic(self, username,
+ gss_authenticated=paramiko.AUTH_FAILED,
+ cc_file=None):
+ if gss_authenticated == paramiko.AUTH_SUCCESSFUL:
+ return paramiko.AUTH_SUCCESSFUL
+ return paramiko.AUTH_FAILED
+
+ def enable_auth_gssapi(self):
+ UseGSSAPI = True
+ GSSAPICleanupCredentials = True
+ return UseGSSAPI
+
+ def check_channel_request(self, kind, chanid):
+ return paramiko.OPEN_SUCCEEDED
+
+ def check_channel_exec_request(self, channel, command):
+ if command != 'yes':
+ return False
+ return True
+
+
+class GSSAuthTest(unittest.TestCase):
+ @staticmethod
+ def init(username, hostname):
+ global krb5_principal, targ_name
+ krb5_principal = username
+ targ_name = hostname
+
+ def setUp(self):
+ self.username = krb5_principal
+ self.hostname = socket.getfqdn(targ_name)
+ self.sockl = socket.socket()
+ self.sockl.bind((targ_name, 0))
+ self.sockl.listen(1)
+ self.addr, self.port = self.sockl.getsockname()
+ self.event = threading.Event()
+ thread = threading.Thread(target=self._run)
+ thread.start()
+
+ def tearDown(self):
+ for attr in "tc ts socks sockl".split():
+ if hasattr(self, attr):
+ getattr(self, attr).close()
+
+ def _run(self):
+ self.socks, addr = self.sockl.accept()
+ self.ts = paramiko.Transport(self.socks)
+ host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key')
+ self.ts.add_server_key(host_key)
+ server = NullServer()
+ self.ts.start_server(self.event, server)
+
+ def test_1_gss_auth(self):
+ """
+ Verify that Paramiko can handle SSHv2 GSS-API / SSPI authentication
+ (gssapi-with-mic) in client and server mode.
+ """
+ host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key')
+ public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+
+ self.tc = paramiko.SSHClient()
+ self.tc.get_host_keys().add('[%s]:%d' % (self.hostname, self.port),
+ 'ssh-rsa', public_host_key)
+ self.tc.connect(self.hostname, self.port, username=self.username,
+ gss_auth=True)
+
+ self.event.wait(1.0)
+ self.assert_(self.event.is_set())
+ self.assert_(self.ts.is_active())
+ self.assertEquals(self.username, self.ts.get_username())
+ self.assertEquals(True, self.ts.is_authenticated())
+
+ stdin, stdout, stderr = self.tc.exec_command('yes')
+ schan = self.ts.accept(1.0)
+
+ schan.send('Hello there.\n')
+ schan.send_stderr('This is on stderr.\n')
+ schan.close()
+
+ self.assertEquals('Hello there.\n', stdout.readline())
+ self.assertEquals('', stdout.readline())
+ self.assertEquals('This is on stderr.\n', stderr.readline())
+ self.assertEquals('', stderr.readline())
+
+ stdin.close()
+ stdout.close()
+ stderr.close()
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 93d33099..a93d8b63 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -20,22 +20,28 @@
Some unit tests for the ssh2 protocol in Transport.
"""
+from __future__ import with_statement
+
from binascii import hexlify
import select
import socket
import time
import threading
import random
+from hashlib import sha1
+import unittest
from paramiko import Transport, SecurityOptions, ServerInterface, RSAKey, DSSKey, \
SSHException, ChannelException
from paramiko import AUTH_FAILED, AUTH_SUCCESSFUL
from paramiko import OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
-from paramiko.common import MSG_KEXINIT, cMSG_CHANNEL_WINDOW_ADJUST
+from paramiko.common import MSG_KEXINIT, cMSG_CHANNEL_WINDOW_ADJUST, \
+ MIN_PACKET_SIZE, MIN_WINDOW_SIZE, MAX_WINDOW_SIZE, \
+ DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE
from paramiko.py3compat import bytes
from paramiko.message import Message
from tests.loop import LoopSocket
-from tests.util import ParamikoTest, test_path
+from tests.util import test_path
LONG_BANNER = """\
@@ -55,7 +61,7 @@ class NullServer (ServerInterface):
paranoid_did_password = False
paranoid_did_public_key = False
paranoid_key = DSSKey.from_private_key_file(test_path('test_dss.key'))
-
+
def get_allowed_auths(self, username):
if username == 'slowdive':
return 'publickey,password'
@@ -78,24 +84,24 @@ class NullServer (ServerInterface):
def check_channel_shell_request(self, channel):
return True
-
+
def check_global_request(self, kind, msg):
self._global_request = kind
return False
-
+
def check_channel_x11_request(self, channel, single_connection, auth_protocol, auth_cookie, screen_number):
self._x11_single_connection = single_connection
self._x11_auth_protocol = auth_protocol
self._x11_auth_cookie = auth_cookie
self._x11_screen_number = screen_number
return True
-
+
def check_port_forward_request(self, addr, port):
self._listen = socket.socket()
self._listen.bind(('127.0.0.1', 0))
self._listen.listen(1)
return self._listen.getsockname()[1]
-
+
def cancel_port_forward_request(self, addr, port):
self._listen.close()
self._listen = None
@@ -105,7 +111,7 @@ class NullServer (ServerInterface):
return OPEN_SUCCEEDED
-class TransportTest(ParamikoTest):
+class TransportTest(unittest.TestCase):
def setUp(self):
self.socks = LoopSocket()
self.sockc = LoopSocket()
@@ -123,20 +129,20 @@ class TransportTest(ParamikoTest):
host_key = RSAKey.from_private_key_file(test_path('test_rsa.key'))
public_host_key = RSAKey(data=host_key.asbytes())
self.ts.add_server_key(host_key)
-
+
if client_options is not None:
client_options(self.tc.get_security_options())
if server_options is not None:
server_options(self.ts.get_security_options())
-
+
event = threading.Event()
self.server = NullServer()
- self.assertTrue(not event.isSet())
+ self.assertTrue(not event.is_set())
self.ts.start_server(event, self.server)
self.tc.connect(hostkey=public_host_key,
username='slowdive', password='pygmalion')
event.wait(1.0)
- self.assertTrue(event.isSet())
+ self.assertTrue(event.is_set())
self.assertTrue(self.ts.is_active())
def test_1_security_options(self):
@@ -155,7 +161,7 @@ class TransportTest(ParamikoTest):
self.assertTrue(False)
except TypeError:
pass
-
+
def test_2_compute_key(self):
self.tc.K = 123281095979686581523377256114209720774539068973101330872763622971399429481072519713536292772709507296759612401802191955568143056534122385270077606457721553469730659233569339356140085284052436697480759510519672848743794433460113118986816826624865291116513647975790797391795651716378444844877749505443714557929
self.tc.H = b'\x0C\x83\x07\xCD\xE6\x85\x6F\xF3\x0B\xA9\x36\x84\xEB\x0F\x04\xC2\x52\x0E\x9E\xD3'
@@ -175,7 +181,7 @@ class TransportTest(ParamikoTest):
self.ts.add_server_key(host_key)
event = threading.Event()
server = NullServer()
- self.assertTrue(not event.isSet())
+ self.assertTrue(not event.is_set())
self.assertEqual(None, self.tc.get_username())
self.assertEqual(None, self.ts.get_username())
self.assertEqual(False, self.tc.is_authenticated())
@@ -184,7 +190,7 @@ class TransportTest(ParamikoTest):
self.tc.connect(hostkey=public_host_key,
username='slowdive', password='pygmalion')
event.wait(1.0)
- self.assertTrue(event.isSet())
+ self.assertTrue(event.is_set())
self.assertTrue(self.ts.is_active())
self.assertEqual('slowdive', self.tc.get_username())
self.assertEqual('slowdive', self.ts.get_username())
@@ -200,15 +206,15 @@ class TransportTest(ParamikoTest):
self.ts.add_server_key(host_key)
event = threading.Event()
server = NullServer()
- self.assertTrue(not event.isSet())
+ self.assertTrue(not event.is_set())
self.socks.send(LONG_BANNER)
self.ts.start_server(event, server)
self.tc.connect(hostkey=public_host_key,
username='slowdive', password='pygmalion')
event.wait(1.0)
- self.assertTrue(event.isSet())
+ self.assertTrue(event.is_set())
self.assertTrue(self.ts.is_active())
-
+
def test_4_special(self):
"""
verify that the client can demand odd handshake settings, and can
@@ -222,7 +228,7 @@ class TransportTest(ParamikoTest):
self.assertEqual('aes256-cbc', self.tc.remote_cipher)
self.assertEqual(12, self.tc.packetizer.get_mac_size_out())
self.assertEqual(12, self.tc.packetizer.get_mac_size_in())
-
+
self.tc.send_ignore(1024)
self.tc.renegotiate_keys()
self.ts.send_ignore(1024)
@@ -236,7 +242,7 @@ class TransportTest(ParamikoTest):
self.tc.set_keepalive(1)
time.sleep(2)
self.assertEqual('keepalive@lag.net', self.server._global_request)
-
+
def test_6_exec_command(self):
"""
verify that exec_command() does something reasonable.
@@ -250,7 +256,7 @@ class TransportTest(ParamikoTest):
self.assertTrue(False)
except SSHException:
pass
-
+
chan = self.tc.open_session()
chan.exec_command('yes')
schan = self.ts.accept(1.0)
@@ -264,7 +270,7 @@ class TransportTest(ParamikoTest):
f = chan.makefile_stderr()
self.assertEqual('This is on stderr.\n', f.readline())
self.assertEqual('', f.readline())
-
+
# now try it with combined stdout/stderr
chan = self.tc.open_session()
chan.exec_command('yes')
@@ -273,11 +279,27 @@ class TransportTest(ParamikoTest):
schan.send_stderr('This is on stderr.\n')
schan.close()
- chan.set_combine_stderr(True)
+ chan.set_combine_stderr(True)
f = chan.makefile()
self.assertEqual('Hello there.\n', f.readline())
self.assertEqual('This is on stderr.\n', f.readline())
self.assertEqual('', f.readline())
+
+ def test_6a_channel_can_be_used_as_context_manager(self):
+ """
+ verify that exec_command() does something reasonable.
+ """
+ self.setup_test_server()
+
+ with self.tc.open_session() as chan:
+ with self.ts.accept(1.0) as schan:
+ chan.exec_command('yes')
+ schan.send('Hello there.\n')
+ schan.close()
+
+ f = chan.makefile()
+ self.assertEqual('Hello there.\n', f.readline())
+ self.assertEqual('', f.readline())
def test_7_invoke_shell(self):
"""
@@ -320,7 +342,7 @@ class TransportTest(ParamikoTest):
schan.shutdown_write()
schan.send_exit_status(23)
schan.close()
-
+
f = chan.makefile()
self.assertEqual('Hello there.\n', f.readline())
self.assertEqual('', f.readline())
@@ -342,14 +364,14 @@ class TransportTest(ParamikoTest):
chan.invoke_shell()
schan = self.ts.accept(1.0)
- # nothing should be ready
+ # nothing should be ready
r, w, e = select.select([chan], [], [], 0.1)
self.assertEqual([], r)
self.assertEqual([], w)
self.assertEqual([], e)
-
+
schan.send('hello\n')
-
+
# something should be ready now (give it 1 second to appear)
for i in range(10):
r, w, e = select.select([chan], [], [], 0.1)
@@ -361,7 +383,7 @@ class TransportTest(ParamikoTest):
self.assertEqual([], e)
self.assertEqual(b'hello\n', chan.recv(6))
-
+
# and, should be dead again now
r, w, e = select.select([chan], [], [], 0.1)
self.assertEqual([], r)
@@ -369,7 +391,7 @@ class TransportTest(ParamikoTest):
self.assertEqual([], e)
schan.close()
-
+
# detect eof?
for i in range(10):
r, w, e = select.select([chan], [], [], 0.1)
@@ -380,14 +402,14 @@ class TransportTest(ParamikoTest):
self.assertEqual([], w)
self.assertEqual([], e)
self.assertEqual(bytes(), chan.recv(16))
-
+
# make sure the pipe is still open for now...
p = chan._pipe
self.assertEqual(False, p._closed)
chan.close()
# ...and now is closed.
self.assertEqual(True, p._closed)
-
+
def test_B_renegotiate(self):
"""
verify that a transport can correctly renegotiate mid-stream.
@@ -402,7 +424,7 @@ class TransportTest(ParamikoTest):
for i in range(20):
chan.send('x' * 1024)
chan.close()
-
+
# allow a few seconds for the rekeying to complete
for i in range(50):
if self.tc.H != self.tc.session_id:
@@ -426,9 +448,11 @@ class TransportTest(ParamikoTest):
bytes = self.tc.packetizer._Packetizer__sent_bytes
chan.send('x' * 1024)
bytes2 = self.tc.packetizer._Packetizer__sent_bytes
+ block_size = self.tc._cipher_info[self.tc.local_cipher]['block-size']
+ mac_size = self.tc._mac_info[self.tc.local_mac]['size']
# tests show this is actually compressed to *52 bytes*! including packet overhead! nice!! :)
self.assertTrue(bytes2 - bytes < 1024)
- self.assertEqual(52, bytes2 - bytes)
+ self.assertEqual(16 + block_size + mac_size, bytes2 - bytes)
chan.close()
schan.close()
@@ -441,28 +465,28 @@ class TransportTest(ParamikoTest):
chan = self.tc.open_session()
chan.exec_command('yes')
schan = self.ts.accept(1.0)
-
+
requested = []
def handler(c, addr_port):
addr, port = addr_port
requested.append((addr, port))
self.tc._queue_incoming_channel(c)
-
+
self.assertEqual(None, getattr(self.server, '_x11_screen_number', None))
cookie = chan.request_x11(0, single_connection=True, handler=handler)
self.assertEqual(0, self.server._x11_screen_number)
self.assertEqual('MIT-MAGIC-COOKIE-1', self.server._x11_auth_protocol)
self.assertEqual(cookie, self.server._x11_auth_cookie)
self.assertEqual(True, self.server._x11_single_connection)
-
+
x11_server = self.ts.open_x11_channel(('localhost', 6093))
x11_client = self.tc.accept()
self.assertEqual('localhost', requested[0][0])
self.assertEqual(6093, requested[0][1])
-
+
x11_server.send('hello')
self.assertEqual(b'hello', x11_client.recv(5))
-
+
x11_server.close()
x11_client.close()
chan.close()
@@ -477,13 +501,13 @@ class TransportTest(ParamikoTest):
chan = self.tc.open_session()
chan.exec_command('yes')
schan = self.ts.accept(1.0)
-
+
requested = []
def handler(c, origin_addr_port, server_addr_port):
requested.append(origin_addr_port)
requested.append(server_addr_port)
self.tc._queue_incoming_channel(c)
-
+
port = self.tc.request_port_forward('127.0.0.1', 0, handler)
self.assertEqual(port, self.server._listen.getsockname()[1])
@@ -492,14 +516,14 @@ class TransportTest(ParamikoTest):
ss, _ = self.server._listen.accept()
sch = self.ts.open_forwarded_tcpip_channel(ss.getsockname(), ss.getpeername())
cch = self.tc.accept()
-
+
sch.send('hello')
self.assertEqual(b'hello', cch.recv(5))
sch.close()
cch.close()
ss.close()
cs.close()
-
+
# now cancel it.
self.tc.cancel_port_forward('127.0.0.1', port)
self.assertTrue(self.server._listen is None)
@@ -513,7 +537,7 @@ class TransportTest(ParamikoTest):
chan = self.tc.open_session()
chan.exec_command('yes')
schan = self.ts.accept(1.0)
-
+
# open a port on the "server" that the client will ask to forward to.
greeting_server = socket.socket()
greeting_server.bind(('127.0.0.1', 0))
@@ -524,13 +548,13 @@ class TransportTest(ParamikoTest):
sch = self.ts.accept(1.0)
cch = socket.socket()
cch.connect(self.server._tcpip_dest)
-
+
ss, _ = greeting_server.accept()
ss.send(b'Hello!\n')
ss.close()
sch.send(cch.recv(8192))
sch.close()
-
+
self.assertEqual(b'Hello!\n', cs.recv(7))
cs.close()
@@ -544,14 +568,14 @@ class TransportTest(ParamikoTest):
chan.invoke_shell()
schan = self.ts.accept(1.0)
- # nothing should be ready
+ # nothing should be ready
r, w, e = select.select([chan], [], [], 0.1)
self.assertEqual([], r)
self.assertEqual([], w)
self.assertEqual([], e)
-
+
schan.send_stderr('hello\n')
-
+
# something should be ready now (give it 1 second to appear)
for i in range(10):
r, w, e = select.select([chan], [], [], 0.1)
@@ -563,7 +587,7 @@ class TransportTest(ParamikoTest):
self.assertEqual([], e)
self.assertEqual(b'hello\n', chan.recv_stderr(6))
-
+
# and, should be dead again now
r, w, e = select.select([chan], [], [], 0.1)
self.assertEqual([], r)
@@ -585,12 +609,13 @@ class TransportTest(ParamikoTest):
self.assertEqual(chan.send_ready(), True)
total = 0
K = '*' * 1024
- while total < 1024 * 1024:
+ limit = 1+(64 * 2 ** 15)
+ while total < limit:
chan.send(K)
total += len(K)
if not chan.send_ready():
break
- self.assertTrue(total < 1024 * 1024)
+ self.assertTrue(total < limit)
schan.close()
chan.close()
@@ -599,10 +624,10 @@ class TransportTest(ParamikoTest):
def test_I_rekey_deadlock(self):
"""
Regression test for deadlock when in-transit messages are received after MSG_KEXINIT is sent
-
+
Note: When this test fails, it may leak threads.
"""
-
+
# Test for an obscure deadlocking bug that can occur if we receive
# certain messages while initiating a key exchange.
#
@@ -619,7 +644,7 @@ class TransportTest(ParamikoTest):
# NeedRekeyException.
# 4. In response to NeedRekeyException, the transport thread sends
# MSG_KEXINIT to the remote host.
- #
+ #
# On the remote host (using any SSH implementation):
# 5. The MSG_CHANNEL_DATA is received, and MSG_CHANNEL_WINDOW_ADJUST is sent.
# 6. The MSG_KEXINIT is received, and a corresponding MSG_KEXINIT is sent.
@@ -654,11 +679,11 @@ class TransportTest(ParamikoTest):
self.done_event = done_event
self.watchdog_event = threading.Event()
self.last = None
-
+
def run(self):
try:
for i in range(1, 1+self.iterations):
- if self.done_event.isSet():
+ if self.done_event.is_set():
break
self.watchdog_event.set()
#print i, "SEND"
@@ -666,7 +691,7 @@ class TransportTest(ParamikoTest):
finally:
self.done_event.set()
self.watchdog_event.set()
-
+
class ReceiveThread(threading.Thread):
def __init__(self, chan, done_event):
threading.Thread.__init__(self, None, None, self.__class__.__name__)
@@ -674,10 +699,10 @@ class TransportTest(ParamikoTest):
self.chan = chan
self.done_event = done_event
self.watchdog_event = threading.Event()
-
+
def run(self):
try:
- while not self.done_event.isSet():
+ while not self.done_event.is_set():
if self.chan.recv_ready():
chan.recv(65536)
self.watchdog_event.set()
@@ -687,10 +712,10 @@ class TransportTest(ParamikoTest):
finally:
self.done_event.set()
self.watchdog_event.set()
-
+
self.setup_test_server()
self.ts.packetizer.REKEY_BYTES = 2048
-
+
chan = self.tc.open_session()
chan.exec_command('yes')
schan = self.ts.accept(1.0)
@@ -712,7 +737,7 @@ class TransportTest(ParamikoTest):
self._send_message(m2)
return _negotiate_keys(self, m)
self.tc._handler_table[MSG_KEXINIT] = _negotiate_keys_wrapper
-
+
# Parameters for the test
iterations = 500 # The deadlock does not happen every time, but it
# should after many iterations.
@@ -724,23 +749,23 @@ class TransportTest(ParamikoTest):
# Start the sending thread
st = SendThread(schan, iterations, done_event)
st.start()
-
+
# Start the receiving thread
rt = ReceiveThread(chan, done_event)
rt.start()
- # Act as a watchdog timer, checking
+ # Act as a watchdog timer, checking
deadlocked = False
- while not deadlocked and not done_event.isSet():
+ while not deadlocked and not done_event.is_set():
for event in (st.watchdog_event, rt.watchdog_event):
event.wait(timeout)
- if done_event.isSet():
+ if done_event.is_set():
break
- if not event.isSet():
+ if not event.is_set():
deadlocked = True
break
event.clear()
-
+
# Tell the threads to stop (if they haven't already stopped). Note
# that if one or more threads are deadlocked, they might hang around
# forever (until the process exits).
@@ -752,3 +777,38 @@ class TransportTest(ParamikoTest):
# Close the channels
schan.close()
chan.close()
+
+ def test_J_sanitze_packet_size(self):
+ """
+ verify that we conform to the rfc of packet and window sizes.
+ """
+ for val, correct in [(4095, MIN_PACKET_SIZE),
+ (None, DEFAULT_MAX_PACKET_SIZE),
+ (2**32, MAX_WINDOW_SIZE)]:
+ self.assertEqual(self.tc._sanitize_packet_size(val), correct)
+
+ def test_K_sanitze_window_size(self):
+ """
+ verify that we conform to the rfc of packet and window sizes.
+ """
+ for val, correct in [(32767, MIN_WINDOW_SIZE),
+ (None, DEFAULT_WINDOW_SIZE),
+ (2**32, MAX_WINDOW_SIZE)]:
+ self.assertEqual(self.tc._sanitize_window_size(val), correct)
+
+ def test_L_handshake_timeout(self):
+ """
+ verify that we can get a hanshake timeout.
+ """
+ host_key = RSAKey.from_private_key_file(test_path('test_rsa.key'))
+ public_host_key = RSAKey(data=host_key.asbytes())
+ self.ts.add_server_key(host_key)
+ event = threading.Event()
+ server = NullServer()
+ self.assertTrue(not event.is_set())
+ self.tc.handshake_timeout = 0.000000000001
+ self.ts.start_server(event, server)
+ self.assertRaises(EOFError, self.tc.connect,
+ hostkey=public_host_key,
+ username='slowdive',
+ password='pygmalion')
diff --git a/tests/test_util.py b/tests/test_util.py
index 0e7d0b2b..bfdc525e 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -23,13 +23,13 @@ Some unit tests for utility functions.
from binascii import hexlify
import errno
import os
-from Crypto.Hash import SHA
+from hashlib import sha1
+import unittest
+
import paramiko.util
from paramiko.util import lookup_ssh_host_config as host_config, safe_string
from paramiko.py3compat import StringIO, byte_ord, b
-from tests.util import ParamikoTest
-
test_config_file = """\
Host *
User robey
@@ -40,7 +40,12 @@ Host *.example.com
\tUser bjork
Port=3333
Host *
- \t \t Crazy something dumb
+"""
+
+dont_strip_whitespace_please = "\t \t Crazy something dumb "
+
+test_config_file += dont_strip_whitespace_please
+test_config_file += """
Host spoo.example.com
Crazy something else
"""
@@ -59,7 +64,7 @@ BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\
from paramiko import *
-class UtilTest(ParamikoTest):
+class UtilTest(unittest.TestCase):
def test_1_import(self):
"""
verify that all the classes can be imported from paramiko.
@@ -136,7 +141,7 @@ class UtilTest(ParamikoTest):
)
def test_4_generate_key_bytes(self):
- x = paramiko.util.generate_key_bytes(SHA, b'ABCDEFGH', 'This is my secret passphrase.', 64)
+ x = paramiko.util.generate_key_bytes(sha1, b'ABCDEFGH', 'This is my secret passphrase.', 64)
hex = ''.join(['%02x' % byte_ord(c) for c in x])
self.assertEqual(hex, '9110e2f6793b69363e58173e9436b13a5a4b339005741d5c680e505f57d871347b4239f14fb5c46e857d5e100424873ba849ac699cea98d729e57b3e84378e8b')
@@ -153,12 +158,6 @@ class UtilTest(ParamikoTest):
finally:
os.unlink('hostfile.temp')
- def test_6_random(self):
- from paramiko.common import rng
- # just verify that we can pull out 32 bytes and not get an exception.
- x = rng.read(32)
- self.assertEqual(len(x), 32)
-
def test_7_host_config_expose_issue_33(self):
test_config_file = """
Host www13.*
@@ -339,12 +338,122 @@ IdentityFile something_%l_using_fqdn
config = paramiko.util.parse_ssh_config(StringIO(test_config))
assert config.lookup('meh') # will die during lookup() if bug regresses
+ def test_clamp_value(self):
+ self.assertEqual(32768, paramiko.util.clamp_value(32767, 32768, 32769))
+ self.assertEqual(32767, paramiko.util.clamp_value(32767, 32765, 32769))
+ self.assertEqual(32769, paramiko.util.clamp_value(32767, 32770, 32769))
+
def test_13_config_dos_crlf_succeeds(self):
config_file = StringIO("host abcqwerty\r\nHostName 127.0.0.1\r\n")
config = paramiko.SSHConfig()
config.parse(config_file)
self.assertEqual(config.lookup("abcqwerty")["hostname"], "127.0.0.1")
+ def test_14_get_hostnames(self):
+ f = StringIO(test_config_file)
+ config = paramiko.util.parse_ssh_config(f)
+ self.assertEqual(config.get_hostnames(), set(['*', '*.example.com', 'spoo.example.com']))
+
+ def test_quoted_host_names(self):
+ test_config_file = """\
+Host "param pam" param "pam"
+ Port 1111
+
+Host "param2"
+ Port 2222
+
+Host param3 parara
+ Port 3333
+
+Host param4 "p a r" "p" "par" para
+ Port 4444
+"""
+ res = {
+ 'param pam': {'hostname': 'param pam', 'port': '1111'},
+ 'param': {'hostname': 'param', 'port': '1111'},
+ 'pam': {'hostname': 'pam', 'port': '1111'},
+
+ 'param2': {'hostname': 'param2', 'port': '2222'},
+
+ 'param3': {'hostname': 'param3', 'port': '3333'},
+ 'parara': {'hostname': 'parara', 'port': '3333'},
+
+ 'param4': {'hostname': 'param4', 'port': '4444'},
+ 'p a r': {'hostname': 'p a r', 'port': '4444'},
+ 'p': {'hostname': 'p', 'port': '4444'},
+ 'par': {'hostname': 'par', 'port': '4444'},
+ 'para': {'hostname': 'para', 'port': '4444'},
+ }
+ f = StringIO(test_config_file)
+ config = paramiko.util.parse_ssh_config(f)
+ for host, values in res.items():
+ self.assertEquals(
+ paramiko.util.lookup_ssh_host_config(host, config),
+ values
+ )
+
+ def test_quoted_params_in_config(self):
+ test_config_file = """\
+Host "param pam" param "pam"
+ IdentityFile id_rsa
+
+Host "param2"
+ IdentityFile "test rsa key"
+
+Host param3 parara
+ IdentityFile id_rsa
+ IdentityFile "test rsa key"
+"""
+ res = {
+ 'param pam': {'hostname': 'param pam', 'identityfile': ['id_rsa']},
+ 'param': {'hostname': 'param', 'identityfile': ['id_rsa']},
+ 'pam': {'hostname': 'pam', 'identityfile': ['id_rsa']},
+
+ 'param2': {'hostname': 'param2', 'identityfile': ['test rsa key']},
+
+ 'param3': {'hostname': 'param3', 'identityfile': ['id_rsa', 'test rsa key']},
+ 'parara': {'hostname': 'parara', 'identityfile': ['id_rsa', 'test rsa key']},
+ }
+ f = StringIO(test_config_file)
+ config = paramiko.util.parse_ssh_config(f)
+ for host, values in res.items():
+ self.assertEquals(
+ paramiko.util.lookup_ssh_host_config(host, config),
+ values
+ )
+
+ def test_quoted_host_in_config(self):
+ conf = SSHConfig()
+ correct_data = {
+ 'param': ['param'],
+ '"param"': ['param'],
+
+ 'param pam': ['param', 'pam'],
+ '"param" "pam"': ['param', 'pam'],
+ '"param" pam': ['param', 'pam'],
+ 'param "pam"': ['param', 'pam'],
+
+ 'param "pam" p': ['param', 'pam', 'p'],
+ '"param" pam "p"': ['param', 'pam', 'p'],
+
+ '"pa ram"': ['pa ram'],
+ '"pa ram" pam': ['pa ram', 'pam'],
+ 'param "p a m"': ['param', 'p a m'],
+ }
+ incorrect_data = [
+ 'param"',
+ '"param',
+ 'param "pam',
+ 'param "pam" "p a',
+ ]
+ for host, values in correct_data.items():
+ self.assertEquals(
+ conf._get_hosts(host),
+ values
+ )
+ for host in incorrect_data:
+ self.assertRaises(Exception, conf._get_hosts, host)
+
def test_safe_string(self):
vanilla = b("vanilla")
has_bytes = b("has \7\3 bytes")
@@ -355,3 +464,23 @@ IdentityFile something_%l_using_fqdn
assert safe_vanilla == vanilla, err.format(safe_vanilla, vanilla)
assert safe_has_bytes == expected_bytes, \
err.format(safe_has_bytes, expected_bytes)
+
+ def test_proxycommand_none_issue_418(self):
+ test_config_file = """
+Host proxycommand-standard-none
+ ProxyCommand None
+
+Host proxycommand-with-equals-none
+ ProxyCommand=None
+ """
+ for host, values in {
+ 'proxycommand-standard-none': {'hostname': 'proxycommand-standard-none'},
+ 'proxycommand-with-equals-none': {'hostname': 'proxycommand-with-equals-none'}
+ }.items():
+
+ f = StringIO(test_config_file)
+ config = paramiko.util.parse_ssh_config(f)
+ self.assertEqual(
+ paramiko.util.lookup_ssh_host_config(host, config),
+ values
+ )
diff --git a/tests/util.py b/tests/util.py
index 66d2696c..b546a7e1 100644
--- a/tests/util.py
+++ b/tests/util.py
@@ -1,17 +1,7 @@
import os
-import unittest
root_path = os.path.dirname(os.path.realpath(__file__))
-
-class ParamikoTest(unittest.TestCase):
- # for Python 2.3 and below
- if not hasattr(unittest.TestCase, 'assertTrue'):
- assertTrue = unittest.TestCase.failUnless
- if not hasattr(unittest.TestCase, 'assertFalse'):
- assertFalse = unittest.TestCase.failIf
-
-
def test_path(filename):
return os.path.join(root_path, filename)
diff --git a/tox.ini b/tox.ini
index 55e3fe64..7d4fcf8a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
-envlist = py25,py26,py27,py32,py33
+envlist = py26,py27,py32,py33,py34
[testenv]
-commands = pip install --use-mirrors -q -r tox-requirements.txt
+commands = pip install -q -r tox-requirements.txt
python test.py