diff options
-rw-r--r-- | .travis.yml | 2 | ||||
-rw-r--r--[-rwxr-xr-x] | demos/demo_simple.py | 11 | ||||
-rw-r--r-- | paramiko/__init__.py | 2 | ||||
-rw-r--r-- | paramiko/_version.py | 2 | ||||
-rw-r--r-- | paramiko/auth_handler.py | 195 | ||||
-rw-r--r-- | paramiko/client.py | 2 | ||||
-rw-r--r-- | paramiko/common.py | 22 | ||||
-rw-r--r-- | paramiko/file.py | 6 | ||||
-rw-r--r-- | paramiko/hostkeys.py | 7 | ||||
-rw-r--r-- | paramiko/kex_gss.py | 2 | ||||
-rw-r--r-- | paramiko/primes.py | 1 | ||||
-rw-r--r-- | paramiko/py3compat.py | 11 | ||||
-rw-r--r-- | paramiko/sftp_client.py | 11 | ||||
-rw-r--r-- | paramiko/ssh_gss.py | 36 | ||||
-rw-r--r-- | paramiko/transport.py | 27 | ||||
-rw-r--r-- | paramiko/win_pageant.py | 2 | ||||
-rw-r--r-- | setup.py | 1 | ||||
-rw-r--r-- | sites/www/changelog.rst | 24 | ||||
-rw-r--r-- | tests/__init__.py | 36 | ||||
-rw-r--r-- | tests/test_client.py | 63 | ||||
-rwxr-xr-x | tests/test_file.py | 57 | ||||
-rwxr-xr-x | tests/test_sftp.py | 30 | ||||
-rw-r--r-- | tests/test_ssh_gss.py | 45 | ||||
-rw-r--r-- | tests/test_transport.py | 69 |
24 files changed, 513 insertions, 151 deletions
diff --git a/.travis.yml b/.travis.yml index 7ca8db09..0e46ec84 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ python: - "3.4" - "3.5" - "3.6" - - "pypy-5.4.1" + - "pypy-5.6.0" install: # Self-install for setup.py-driven deps - pip install -e . diff --git a/demos/demo_simple.py b/demos/demo_simple.py index 3a17988c..7ae3d8c8 100755..100644 --- a/demos/demo_simple.py +++ b/demos/demo_simple.py @@ -37,8 +37,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 +UseGSSAPI = paramiko.GSS_AUTH_AVAILABLE # enable "gssapi-with-mic" authentication, if supported by your python installation +DoGSSAPIKeyExchange = paramiko.GSS_AUTH_AVAILABLE # enable "gssapi-kex" key exchange, if supported by your python installation +# UseGSSAPI = False +# DoGSSAPIKeyExchange = False port = 22 # get hostname @@ -64,7 +66,7 @@ if username == '': username = input('Username [%s]: ' % default_username) if len(username) == 0: username = default_username -if not UseGSSAPI or (not UseGSSAPI and not DoGSSAPIKeyExchange): +if not UseGSSAPI and not DoGSSAPIKeyExchange: password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) @@ -74,7 +76,7 @@ try: client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.WarningPolicy()) print('*** Connecting...') - if not UseGSSAPI or (not UseGSSAPI and not DoGSSAPIKeyExchange): + if not UseGSSAPI and not DoGSSAPIKeyExchange: client.connect(hostname, port, username, password) else: # SSPI works only with the FQDN of the target host @@ -83,6 +85,7 @@ try: client.connect(hostname, port, username, gss_auth=UseGSSAPI, gss_kex=DoGSSAPIKeyExchange) except Exception: + # traceback.print_exc() password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) client.connect(hostname, port, username, password) diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 197f519a..01dc973c 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -34,7 +34,7 @@ from paramiko.client import ( WarningPolicy, ) from paramiko.auth_handler import AuthHandler -from paramiko.ssh_gss import GSSAuth, GSS_AUTH_AVAILABLE +from paramiko.ssh_gss import GSSAuth, GSS_AUTH_AVAILABLE, GSS_EXCEPTIONS from paramiko.channel import Channel, ChannelFile from paramiko.ssh_exception import ( SSHException, PasswordRequiredException, BadAuthenticationType, diff --git a/paramiko/_version.py b/paramiko/_version.py index 4e7cf19d..98c3cc2d 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (2, 0, 5) +__version_info__ = (2, 0, 6) __version__ = '.'.join(map(str, __version_info__)) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 33f01da6..24ada232 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -43,7 +43,7 @@ from paramiko.ssh_exception import ( PartialAuthentication, ) from paramiko.server import InteractiveQuery -from paramiko.ssh_gss import GSSAuth +from paramiko.ssh_gss import GSSAuth, GSS_EXCEPTIONS class AuthHandler (object): @@ -262,19 +262,26 @@ class AuthHandler (object): 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,)) + try: + m.add_string(sshgss.ssh_init_sec_context( + self.gss_host, + mech, + self.username,)) + except GSS_EXCEPTIONS as e: + return self._handle_local_gss_failure(e) 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) + try: + next_token = sshgss.ssh_init_sec_context( + self.gss_host, + mech, + self.username, + srv_token) + except GSS_EXCEPTIONS as e: + return self._handle_local_gss_failure(e) # 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. @@ -302,7 +309,7 @@ class AuthHandler (object): maj_status = m.get_int() min_status = m.get_int() err_msg = m.get_string() - m.get_string() # Lang tag - discarded + m.get_string() # Lang tag - discarded raise SSHException("GSS-API Error:\nMajor Status: %s\n\ Minor Status: %s\ \nError Message:\ %s\n") % (str(maj_status), @@ -395,7 +402,7 @@ class AuthHandler (object): (self.auth_username != username)): self.transport._log( WARNING, - 'Auth rejected because the client attempted to change username in mid-flight' # noqa + 'Auth rejected because the client attempted to change username in mid-flight' # noqa ) self._disconnect_no_more_auth() return @@ -503,52 +510,16 @@ class AuthHandler (object): 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: - result = AUTH_FAILED - self._send_auth_result(username, method, result) - return - # 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) + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_RESPONSE) + m.add_bytes(supported_mech) + self.transport.auth_handler = GssapiWithMicAuthHandler(self, + sshgss) + self.transport._expected_packet = (MSG_USERAUTH_GSSAPI_TOKEN, + MSG_USERAUTH_REQUEST, + MSG_SERVICE_REQUEST) + self.transport._send_message(m) + return elif method == "gssapi-keyex" and gss_auth: mic_token = m.get_string() sshgss = self.transport.kexgss_ctxt @@ -648,6 +619,17 @@ class AuthHandler (object): self._send_auth_result( self.auth_username, 'keyboard-interactive', result) + def _handle_local_gss_failure(self, e): + self.transport.saved_exception = e + self.transport._log(DEBUG, "GSSAPI failure: %s" % str(e)) + self.transport._log(INFO, 'Authentication (%s) failed.' % + self.auth_method) + self.authenticated = False + self.username = None + if self.auth_event is not None: + self.auth_event.set() + return + _handler_table = { MSG_SERVICE_REQUEST: _parse_service_request, MSG_SERVICE_ACCEPT: _parse_service_accept, @@ -658,3 +640,102 @@ class AuthHandler (object): MSG_USERAUTH_INFO_REQUEST: _parse_userauth_info_request, MSG_USERAUTH_INFO_RESPONSE: _parse_userauth_info_response, } + + +class GssapiWithMicAuthHandler(object): + """A specialized Auth handler for gssapi-with-mic + + During the GSSAPI token exchange we need a modified dispatch table, + because the packet type numbers are not unique. + """ + + method = "gssapi-with-mic" + + def __init__(self, delegate, sshgss): + self._delegate = delegate + self.sshgss = sshgss + + def abort(self): + self._restore_delegate_auth_handler() + return self._delegate.abort() + + @property + def transport(self): + return self._delegate.transport + + @property + def _send_auth_result(self): + return self._delegate._send_auth_result + + @property + def auth_username(self): + return self._delegate.auth_username + + @property + def gss_host(self): + return self._delegate.gss_host + + def _restore_delegate_auth_handler(self): + self.transport.auth_handler = self._delegate + + def _parse_userauth_gssapi_token(self, m): + client_token = m.get_string() + # use the client token as input to establish a secure + # context. + sshgss = self.sshgss + try: + token = sshgss.ssh_accept_sec_context(self.gss_host, + client_token, + self.auth_username) + except Exception as e: + self.transport.saved_exception = e + result = AUTH_FAILED + self._restore_delegate_auth_handler() + self._send_auth_result(self.auth_username, self.method, result) + raise + if token is not None: + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN) + m.add_string(token) + self.transport._expected_packet = (MSG_USERAUTH_GSSAPI_TOKEN, + MSG_USERAUTH_GSSAPI_MIC, + MSG_USERAUTH_REQUEST) + self.transport._send_message(m) + + def _parse_userauth_gssapi_mic(self, m): + mic_token = m.get_string() + sshgss = self.sshgss + username = self.auth_username + self._restore_delegate_auth_handler() + try: + sshgss.ssh_check_mic(mic_token, + self.transport.session_id, + username) + except Exception as e: + self.transport.saved_exception = e + result = AUTH_FAILED + self._send_auth_result(username, self.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) + # okay, send result + self._send_auth_result(username, self.method, result) + + def _parse_service_request(self, m): + self._restore_delegate_auth_handler() + return self._delegate._parse_service_request(m) + + def _parse_userauth_request(self, m): + self._restore_delegate_auth_handler() + return self._delegate._parse_userauth_request(m) + + _handler_table = { + MSG_SERVICE_REQUEST: _parse_service_request, + MSG_USERAUTH_REQUEST: _parse_userauth_request, + MSG_USERAUTH_GSSAPI_TOKEN: _parse_userauth_gssapi_token, + MSG_USERAUTH_GSSAPI_MIC: _parse_userauth_gssapi_mic, + } diff --git a/paramiko/client.py b/paramiko/client.py index 224109bf..39617d5b 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -353,7 +353,7 @@ class SSHClient (ClosingContextManager): # 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: + if not self._transport.gss_kex_used: our_server_key = self._system_host_keys.get( server_hostkey_name, {}).get(keytype) if our_server_key is None: diff --git a/paramiko/common.py b/paramiko/common.py index 556f046a..0012372a 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -20,9 +20,7 @@ Common constants and global variables. """ import logging -from paramiko.py3compat import ( - byte_chr, PY2, bytes_types, string_types, b, long, -) +from paramiko.py3compat import byte_chr, PY2, bytes_types, text_type, long MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED, MSG_DEBUG, \ MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT = range(1, 7) @@ -163,14 +161,16 @@ else: def asbytes(s): - if not isinstance(s, bytes_types): - if isinstance(s, string_types): - s = b(s) - else: - try: - s = s.asbytes() - except Exception: - raise Exception('Unknown type') + """Coerce to bytes if possible or return unchanged.""" + if isinstance(s, bytes_types): + return s + if isinstance(s, text_type): + # Accept text and encode as utf-8 for compatibility only. + return s.encode("utf-8") + asbytes = getattr(s, "asbytes", None) + if asbytes is not None: + return asbytes() + # May be an object that implements the buffer api, let callers handle. return s diff --git a/paramiko/file.py b/paramiko/file.py index 5212091a..a1bdafbe 100644 --- a/paramiko/file.py +++ b/paramiko/file.py @@ -18,7 +18,7 @@ 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.py3compat import BytesIO, PY2, u, bytes_types, text_type from paramiko.util import ClosingContextManager @@ -391,7 +391,9 @@ class BufferedFile (ClosingContextManager): :param data: ``str``/``bytes`` data to write """ - data = b(data) + if isinstance(data, text_type): + # Accept text and encode as utf-8 for compatibility only. + data = data.encode('utf-8') if self._closed: raise IOError('File is closed') if not (self._flags & self.FLAG_WRITE): diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index 008ba592..c873f58b 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -20,17 +20,12 @@ import binascii import os +from collections import MutableMapping from hashlib import sha1 from hmac import HMAC from paramiko.py3compat import b, u, encodebytes, decodebytes -try: - from collections import MutableMapping -except ImportError: - # noinspection PyUnresolvedReferences - from UserDict import DictMixin as MutableMapping - from paramiko.dsskey import DSSKey from paramiko.rsakey import RSAKey from paramiko.util import get_logger, constant_time_bytes_eq diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index ba24c0a0..3406babb 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -40,7 +40,7 @@ This module provides GSS-API / SSPI Key Exchange as defined in :rfc:`4462`. import os from hashlib import sha1 -from paramiko.common import * # noqa +from paramiko.common import DEBUG, max_byte, zero_byte from paramiko import util from paramiko.message import Message from paramiko.py3compat import byte_chr, byte_mask, byte_ord diff --git a/paramiko/primes.py b/paramiko/primes.py index 48a34e53..65617914 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -25,7 +25,6 @@ import os from paramiko import util from paramiko.py3compat import byte_mask, long from paramiko.ssh_exception import SSHException -from paramiko.common import * # noqa def _roll_random(n): diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py index 095b0d09..6703ace8 100644 --- a/paramiko/py3compat.py +++ b/paramiko/py3compat.py @@ -65,15 +65,8 @@ if PY2: return s - try: - import cStringIO - - StringIO = cStringIO.StringIO # NOQA - except ImportError: - import StringIO - - StringIO = StringIO.StringIO # NOQA - + import cStringIO + StringIO = cStringIO.StringIO BytesIO = StringIO diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 12fccb2f..ee5ab073 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -28,9 +28,7 @@ from paramiko import util from paramiko.channel import Channel from paramiko.message import Message from paramiko.common import INFO, DEBUG, o777 -from paramiko.py3compat import ( - bytestring, b, u, long, string_types, bytes_types, -) +from paramiko.py3compat import bytestring, b, u, long from paramiko.sftp import ( BaseSFTP, CMD_OPENDIR, CMD_HANDLE, SFTPError, CMD_READDIR, CMD_NAME, CMD_CLOSE, SFTP_FLAG_READ, SFTP_FLAG_WRITE, SFTP_FLAG_CREATE, @@ -758,13 +756,12 @@ class SFTPClient(BaseSFTP, ClosingContextManager): msg.add_int64(item) elif isinstance(item, int): msg.add_int(item) - elif isinstance(item, (string_types, bytes_types)): - msg.add_string(item) elif isinstance(item, SFTPAttributes): item._pack(msg) else: - raise Exception( - 'unknown type for %r type %r' % (item, type(item))) + # For all other types, rely on as_string() to either coerce + # to bytes before writing or raise a suitable exception. + msg.add_string(item) num = self.request_number self._expecting[num] = fileobj self.request_number += 1 diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index 414485f9..b3c3f72b 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -33,34 +33,39 @@ import struct import os import sys -""" -:var bool GSS_AUTH_AVAILABLE: - Constraint that indicates if GSS-API / SSPI is available. -""" + +#: A boolean constraint that indicates if GSS-API / SSPI is available. GSS_AUTH_AVAILABLE = True + +#: A tuple of the exception types used by the underlying GSSAPI implementation. +GSS_EXCEPTIONS = () + + from pyasn1.type.univ import ObjectIdentifier from pyasn1.codec.der import encoder, decoder -from paramiko.common import MSG_USERAUTH_REQUEST -from paramiko.ssh_exception import SSHException -""" -:var str _API: Constraint for the used API -""" +#: :var str _API: Constraint for the used API _API = "MIT" try: import gssapi + GSS_EXCEPTIONS = (gssapi.GSSException,) except (ImportError, OSError): try: + import pywintypes import sspicon import sspi _API = "SSPI" + GSS_EXCEPTIONS = (pywintypes.error,) except ImportError: GSS_AUTH_AVAILABLE = False _API = None +from paramiko.common import MSG_USERAUTH_REQUEST +from paramiko.ssh_exception import SSHException + def GSSAuth(auth_method, gss_deleg_creds=True): """ @@ -345,9 +350,9 @@ class _SSH_GSSAPI(_SSH_GSSAuth): 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._username, + self._service, + self._auth_method) self._gss_srv_ctxt.verify_mic(mic_field, mic_token) else: # for key exchange with gssapi-keyex @@ -438,9 +443,10 @@ class _SSH_SSPI(_SSH_GSSAuth): 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)) + except pywintypes.error as e: + e.strerror += ", Target: {1}".format(e, self._gss_host) + raise + if error == 0: """ if the status is GSS_COMPLETE (error = 0) the context is fully diff --git a/paramiko/transport.py b/paramiko/transport.py index d219550d..1ab60841 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -106,9 +106,9 @@ class Transport(threading.Thread, ClosingContextManager): 'aes192-ctr', 'aes256-ctr', 'aes128-cbc', - 'blowfish-cbc', 'aes192-cbc', 'aes256-cbc', + 'blowfish-cbc', '3des-cbc', ) _preferred_macs = ( @@ -132,6 +132,11 @@ class Transport(threading.Thread, ClosingContextManager): 'diffie-hellman-group-exchange-sha1', 'diffie-hellman-group-exchange-sha256', ) + _preferred_gsskex = ( + 'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==', + 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==', + 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==', + ) _preferred_compression = ('none',) _cipher_info = { @@ -309,14 +314,9 @@ class Transport(threading.Thread, ClosingContextManager): threading.Thread.__init__(self) self.setDaemon(True) self.sock = sock - # Python < 2.3 doesn't have the settimeout method - RogerB - try: - # we set the timeout so we can check self.active periodically to - # see if we should bail. socket.timeout exception is never - # propagated. - self.sock.settimeout(self._active_check_timeout) - except AttributeError: - pass + # we set the timeout so we can check self.active periodically to + # see if we should bail. socket.timeout exception is never propagated. + self.sock.settimeout(self._active_check_timeout) # negotiated crypto parameters self.packetizer = Packetizer(sock) @@ -338,12 +338,7 @@ class Transport(threading.Thread, ClosingContextManager): 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') + self._preferred_kex = self._preferred_gsskex + self._preferred_kex # state used during negotiation self.kex_engine = None @@ -1843,6 +1838,8 @@ class Transport(threading.Thread, ClosingContextManager): ): handler = self.auth_handler._handler_table[ptype] handler(self.auth_handler, m) + if len(self._expected_packet) > 0: + continue else: self._log(WARNING, 'Oops, unhandled type %d' % ptype) msg = Message() diff --git a/paramiko/win_pageant.py b/paramiko/win_pageant.py index c8c2c7bc..fda3b9c1 100644 --- a/paramiko/win_pageant.py +++ b/paramiko/win_pageant.py @@ -25,7 +25,7 @@ import array import ctypes.wintypes import platform import struct -from paramiko.util import * # noqa +from paramiko.common import zero_byte from paramiko.py3compat import b try: @@ -72,6 +72,7 @@ setup( 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], install_requires=[ 'cryptography>=1.1', diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 49854695..ac1ba9ae 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -3,6 +3,30 @@ Changelog ========= * :release:`1.18.4 <2017-09-18>` +* :bug:`1061` Clean up GSSAPI authentication procedures so they do not prevent + normal fallback to other authentication methods on failure. (In other words, + presence of GSSAPI functionality on a target server precluded use of _any_ + other auth type if the user was unable to pass GSSAPI auth.) Patch via Anselm + Kruis. +* :bug:`1060` Fix key exchange (kex) algorithm list for GSSAPI authentication; + previously, the list used solely out-of-date algorithms, and now contains + newer ones listed preferentially before the old. Credit: Anselm Kruis. +* :bug:`1055` (also :issue:`1056`, :issue:`1057`, :issue:`1058`, :issue:`1059`) + Fix up host-key checking in our GSSAPI support, which was previously using an + incorrect API call. Thanks to Anselm Kruis for the patches. +* :release:`2.0.6 <2017-06-09>` +* :release:`1.18.3 <2017-06-09>` +* :release:`1.17.5 <2017-06-09>` +* :support:`906 (1.18+)` Clean up a handful of outdated imports and related + tweaks. Thanks to Pierce Lopez. +* :bug:`984` Enhance default cipher preference order such that + ``aes(192|256)-cbc`` are preferred over ``blowfish-cbc``. Thanks to Alex + Gaynor. +* :bug:`971 (1.17+)` Allow any type implementing the buffer API to be used with + `BufferedFile <paramiko.file.BufferedFile>`, `Channel + <paramiko.channel.Channel>`, and `SFTPFile <paramiko.sftp_file.SFTPFile>`. + This resolves a regression introduced in 1.13 with the Python 3 porting + changes, when using types such as ``memoryview``. Credit: Martin Packman. * :bug:`741` (also :issue:`809`, :issue:`772`; all via :issue:`912`) Writing encrypted/password-protected private key files was silently broken since 2.0 due to an incorrect API call; this has been fixed. diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..8878f14d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,36 @@ +# Copyright (C) 2017 Martin Packman <gzlist@googlemail.com> +# +# 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. + +"""Base classes and helpers for testing paramiko.""" + +import unittest + +from paramiko.py3compat import ( + builtins, + ) + + +def skipUnlessBuiltin(name): + """Skip decorated test if builtin name does not exist.""" + if getattr(builtins, name, None) is None: + skip = getattr(unittest, "skip", None) + if skip is None: + # Python 2.6 pseudo-skip + return lambda func: None + return skip("No builtin " + repr(name)) + return lambda func: func diff --git a/tests/test_client.py b/tests/test_client.py index f2f2ea45..9da6eaca 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -35,7 +35,7 @@ import time from tests.util import test_path import paramiko -from paramiko.common import PY2 +from paramiko.py3compat import PY2, b from paramiko.ssh_exception import SSHException @@ -141,6 +141,7 @@ class SSHClientTest (unittest.TestCase): self.assertTrue(self.ts.is_active()) self.assertEqual('slowdive', self.ts.get_username()) self.assertEqual(True, self.ts.is_authenticated()) + self.assertEqual(False, self.tc.get_transport().gss_kex_used) # Command execution functions? stdin, stdout, stderr = self.tc.exec_command('yes') @@ -366,3 +367,63 @@ class SSHClientTest (unittest.TestCase): password='pygmalion', ) self._test_connection(**kwargs) + + def test_9_auth_trickledown_gsskex(self): + """ + Failed gssapi-keyex auth doesn't prevent subsequent key auth from succeeding + """ + if not paramiko.GSS_AUTH_AVAILABLE: + return # for python 2.6 lacks skipTest + kwargs = dict( + gss_kex=True, + key_filename=[test_path('test_rsa.key')], + ) + self._test_connection(**kwargs) + + def test_10_auth_trickledown_gssauth(self): + """ + Failed gssapi-with-mic auth doesn't prevent subsequent key auth from succeeding + """ + if not paramiko.GSS_AUTH_AVAILABLE: + return # for python 2.6 lacks skipTest + kwargs = dict( + gss_auth=True, + key_filename=[test_path('test_rsa.key')], + ) + self._test_connection(**kwargs) + + def test_11_reject_policy(self): + """ + verify that SSHClient's RejectPolicy works. + """ + threading.Thread(target=self._run).start() + + self.tc = paramiko.SSHClient() + self.tc.set_missing_host_key_policy(paramiko.RejectPolicy()) + self.assertEqual(0, len(self.tc.get_host_keys())) + self.assertRaises( + paramiko.SSHException, + self.tc.connect, + password='pygmalion', **self.connect_kwargs + ) + + def test_12_reject_policy_gsskex(self): + """ + verify that SSHClient's RejectPolicy works, + even if gssapi-keyex was enabled but not used. + """ + # Test for a bug present in paramiko versions released before 2017-08-01 + if not paramiko.GSS_AUTH_AVAILABLE: + return # for python 2.6 lacks skipTest + threading.Thread(target=self._run).start() + + self.tc = paramiko.SSHClient() + self.tc.set_missing_host_key_policy(paramiko.RejectPolicy()) + self.assertEqual(0, len(self.tc.get_host_keys())) + self.assertRaises( + paramiko.SSHException, + self.tc.connect, + password='pygmalion', + gss_kex=True, + **self.connect_kwargs + ) diff --git a/tests/test_file.py b/tests/test_file.py index 7fab6985..b33ecd51 100755 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -21,10 +21,14 @@ Some unit tests for the BufferedFile abstraction. """ import unittest -from paramiko.file import BufferedFile -from paramiko.common import linefeed_byte, crlf, cr_byte import sys +from paramiko.common import linefeed_byte, crlf, cr_byte +from paramiko.file import BufferedFile +from paramiko.py3compat import BytesIO + +from tests import skipUnlessBuiltin + class LoopbackFile (BufferedFile): """ @@ -33,19 +37,16 @@ class LoopbackFile (BufferedFile): def __init__(self, mode='r', bufsize=-1): BufferedFile.__init__(self) self._set_mode(mode, bufsize) - self.buffer = bytes() + self.buffer = BytesIO() + self.offset = 0 def _read(self, size): - if len(self.buffer) == 0: - return None - if size > len(self.buffer): - size = len(self.buffer) - data = self.buffer[:size] - self.buffer = self.buffer[size:] + data = self.buffer.getvalue()[self.offset:self.offset+size] + self.offset += len(data) return data def _write(self, data): - self.buffer += data + self.buffer.write(data) return len(data) @@ -187,6 +188,42 @@ class BufferedFileTest (unittest.TestCase): self.assertEqual(data, b'hello') f.close() + def test_write_bad_type(self): + with LoopbackFile('wb') as f: + self.assertRaises(TypeError, f.write, object()) + + def test_write_unicode_as_binary(self): + text = u"\xa7 why is writing text to a binary file allowed?\n" + with LoopbackFile('rb+') as f: + f.write(text) + self.assertEqual(f.read(), text.encode("utf-8")) + + @skipUnlessBuiltin('memoryview') + def test_write_bytearray(self): + with LoopbackFile('rb+') as f: + f.write(bytearray(12)) + self.assertEqual(f.read(), 12 * b"\0") + + @skipUnlessBuiltin('buffer') + def test_write_buffer(self): + data = 3 * b"pretend giant block of data\n" + offsets = range(0, len(data), 8) + with LoopbackFile('rb+') as f: + for offset in offsets: + f.write(buffer(data, offset, 8)) + self.assertEqual(f.read(), data) + + @skipUnlessBuiltin('memoryview') + def test_write_memoryview(self): + data = 3 * b"pretend giant block of data\n" + offsets = range(0, len(data), 8) + with LoopbackFile('rb+') as f: + view = memoryview(data) + for offset in offsets: + f.write(view[offset:offset+8]) + self.assertEqual(f.read(), data) + + if __name__ == '__main__': from unittest import main main() diff --git a/tests/test_sftp.py b/tests/test_sftp.py index d3064fff..98a9cebb 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -35,6 +35,7 @@ from tempfile import mkstemp import paramiko from paramiko.py3compat import PY2, b, u, StringIO from paramiko.common import o777, o600, o666, o644 +from tests import skipUnlessBuiltin from tests.stub_sftp import StubServer, StubSFTPServer from tests.loop import LoopSocket from tests.util import test_path @@ -817,6 +818,35 @@ class SFTPTest (unittest.TestCase): sftp_attributes = SFTPAttributes() self.assertEqual(str(sftp_attributes), "?--------- 1 0 0 0 (unknown date) ?") + @skipUnlessBuiltin('buffer') + def test_write_buffer(self): + """Test write() using a buffer instance.""" + data = 3 * b'A potentially large block of data to chunk up.\n' + try: + with sftp.open('%s/write_buffer' % FOLDER, 'wb') as f: + for offset in range(0, len(data), 8): + f.write(buffer(data, offset, 8)) + + with sftp.open('%s/write_buffer' % FOLDER, 'rb') as f: + self.assertEqual(f.read(), data) + finally: + sftp.remove('%s/write_buffer' % FOLDER) + + @skipUnlessBuiltin('memoryview') + def test_write_memoryview(self): + """Test write() using a memoryview instance.""" + data = 3 * b'A potentially large block of data to chunk up.\n' + try: + with sftp.open('%s/write_memoryview' % FOLDER, 'wb') as f: + view = memoryview(data) + for offset in range(0, len(data), 8): + f.write(view[offset:offset+8]) + + with sftp.open('%s/write_memoryview' % FOLDER, 'rb') as f: + self.assertEqual(f.read(), data) + finally: + sftp.remove('%s/write_memoryview' % FOLDER) + if __name__ == '__main__': SFTPTest.init_loopback() diff --git a/tests/test_ssh_gss.py b/tests/test_ssh_gss.py index 967b3b81..d8d05d2b 100644 --- a/tests/test_ssh_gss.py +++ b/tests/test_ssh_gss.py @@ -29,11 +29,13 @@ import unittest import paramiko +from tests.util import test_path +from tests.test_client import FINGERPRINTS class NullServer (paramiko.ServerInterface): def get_allowed_auths(self, username): - return 'gssapi-with-mic' + return 'gssapi-with-mic,publickey' def check_auth_gssapi_with_mic(self, username, gss_authenticated=paramiko.AUTH_FAILED, @@ -45,6 +47,16 @@ class NullServer (paramiko.ServerInterface): def enable_auth_gssapi(self): return True + def check_auth_publickey(self, username, key): + try: + expected = FINGERPRINTS[key.get_name()] + except KeyError: + return paramiko.AUTH_FAILED + else: + if key.get_fingerprint() == expected: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + def check_channel_request(self, kind, chanid): return paramiko.OPEN_SUCCEEDED @@ -85,19 +97,21 @@ class GSSAuthTest(unittest.TestCase): server = NullServer() self.ts.start_server(self.event, server) - def test_1_gss_auth(self): + def _test_connection(self, **kwargs): """ - Verify that Paramiko can handle SSHv2 GSS-API / SSPI authentication - (gssapi-with-mic) in client and server mode. + (Most) kwargs get passed directly into SSHClient.connect(). + + The exception is ... no exception yet """ 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), + self.tc.set_missing_host_key_policy(paramiko.WarningPolicy()) + self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) - self.tc.connect(self.hostname, self.port, username=self.username, - gss_auth=True) + self.tc.connect(hostname=self.addr, port=self.port, username=self.username, gss_host=self.hostname, + gss_auth=True, **kwargs) self.event.wait(1.0) self.assert_(self.event.is_set()) @@ -120,3 +134,20 @@ class GSSAuthTest(unittest.TestCase): stdin.close() stdout.close() stderr.close() + + def test_1_gss_auth(self): + """ + Verify that Paramiko can handle SSHv2 GSS-API / SSPI authentication + (gssapi-with-mic) in client and server mode. + """ + self._test_connection(allow_agent=False, + look_for_keys=False) + + def test_2_auth_trickledown(self): + """ + Failed gssapi-with-mic auth doesn't prevent subsequent key auth from succeeding + """ + self.hostname = "this_host_does_not_exists_and_causes_a_GSSAPI-exception" + self._test_connection(key_filename=[test_path('test_rsa.key')], + allow_agent=False, + look_for_keys=False) diff --git a/tests/test_transport.py b/tests/test_transport.py index c426cef1..3e352919 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -43,6 +43,7 @@ from paramiko.common import ( ) from paramiko.py3compat import bytes from paramiko.message import Message +from tests import skipUnlessBuiltin from tests.loop import LoopSocket from tests.util import test_path @@ -858,3 +859,71 @@ class TransportTest(unittest.TestCase): self.assertEqual([chan], r) self.assertEqual([], w) self.assertEqual([], e) + + def test_channel_send_misc(self): + """ + verify behaviours sending various instances to a channel + """ + self.setup_test_server() + text = u"\xa7 slice me nicely" + with self.tc.open_session() as chan: + schan = self.ts.accept(1.0) + if schan is None: + self.fail("Test server transport failed to accept") + sfile = schan.makefile() + + # TypeError raised on non string or buffer type + self.assertRaises(TypeError, chan.send, object()) + self.assertRaises(TypeError, chan.sendall, object()) + + # sendall() accepts a unicode instance + chan.sendall(text) + expected = text.encode("utf-8") + self.assertEqual(sfile.read(len(expected)), expected) + + @skipUnlessBuiltin('buffer') + def test_channel_send_buffer(self): + """ + verify sending buffer instances to a channel + """ + self.setup_test_server() + data = 3 * b'some test data\n whole' + with self.tc.open_session() as chan: + schan = self.ts.accept(1.0) + if schan is None: + self.fail("Test server transport failed to accept") + sfile = schan.makefile() + + # send() accepts buffer instances + sent = 0 + while sent < len(data): + sent += chan.send(buffer(data, sent, 8)) + self.assertEqual(sfile.read(len(data)), data) + + # sendall() accepts a buffer instance + chan.sendall(buffer(data)) + self.assertEqual(sfile.read(len(data)), data) + + @skipUnlessBuiltin('memoryview') + def test_channel_send_memoryview(self): + """ + verify sending memoryview instances to a channel + """ + self.setup_test_server() + data = 3 * b'some test data\n whole' + with self.tc.open_session() as chan: + schan = self.ts.accept(1.0) + if schan is None: + self.fail("Test server transport failed to accept") + sfile = schan.makefile() + + # send() accepts memoryview slices + sent = 0 + view = memoryview(data) + while sent < len(view): + sent += chan.send(view[sent:sent+8]) + self.assertEqual(sfile.read(len(data)), data) + + # sendall() accepts a memoryview instance + chan.sendall(memoryview(data)) + self.assertEqual(sfile.read(len(data)), data) |