diff options
author | Jeff Forcier <jeff@bitprophet.org> | 2017-11-29 12:11:31 -0800 |
---|---|---|
committer | Jeff Forcier <jeff@bitprophet.org> | 2017-11-29 12:11:31 -0800 |
commit | c72b688286a4d897b9370acb355c17b6553e7302 (patch) | |
tree | bd9ad6b4a2fb74377ebd323eec2cbe2e485f1836 | |
parent | eec27bd4a0809b5096c5465033815d2414f309c2 (diff) | |
parent | b999f40bea04c4bade6c9f52627b6b6b93df6cda (diff) |
Merge branch '2.2' into 1051-int
-rw-r--r-- | .travis.yml | 3 | ||||
-rw-r--r--[-rwxr-xr-x] | demos/demo_simple.py | 11 | ||||
-rw-r--r-- | dev-requirements.txt | 8 | ||||
-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 | 25 | ||||
-rw-r--r-- | paramiko/kex_gss.py | 21 | ||||
-rw-r--r-- | paramiko/sftp_client.py | 2 | ||||
-rw-r--r-- | paramiko/sftp_file.py | 12 | ||||
-rw-r--r-- | paramiko/ssh_gss.py | 36 | ||||
-rw-r--r-- | paramiko/transport.py | 17 | ||||
-rw-r--r-- | setup.cfg | 3 | ||||
-rw-r--r-- | sites/www/changelog.rst | 34 | ||||
-rw-r--r-- | tests/test_client.py | 61 | ||||
-rw-r--r-- | tests/test_kex_gss.py | 21 | ||||
-rw-r--r-- | tests/test_ssh_gss.py | 45 |
17 files changed, 367 insertions, 131 deletions
diff --git a/.travis.yml b/.travis.yml index 0e46ec84..bb0ed5ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,11 @@ python: - "3.4" - "3.5" - "3.6" + - "3.7-dev" - "pypy-5.6.0" install: + # Ensure modern pip/etc on Python 3.3 workers (not sure WTF, but, eh) + - pip install pip==9.0.1 setuptools==36.6.0 # Self-install for setup.py-driven deps - pip install -e . # Dev (doc/test running) requirements 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/dev-requirements.txt b/dev-requirements.txt index 716f432d..2cb0d768 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,12 +1,12 @@ # Older junk tox>=1.4,<1.5 # For newer tasks like building Sphinx docs. -invoke>=0.13,<2.0 -invocations>=0.13,<2.0 +invoke>=0.13,<=0.21.0 +invocations>=0.13,<=0.20.0 sphinx>=1.1.3,<1.5 alabaster>=0.7.5,<2.0 -releases>=1.1.0,<2.0 +releases>=1.1.0,<1.4.0 semantic_version<3.0 wheel==0.24 -twine==1.5 +twine==1.9.1 flake8==2.6.2 diff --git a/paramiko/__init__.py b/paramiko/__init__.py index d67ad62f..4b690834 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 c8ca86d1..cbe430ff 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (2, 2, 1) +__version_info__ = (2, 2, 2) __version__ = '.'.join(map(str, __version_info__)) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index ae88179e..ac6e23da 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -44,7 +44,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): @@ -269,19 +269,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. @@ -309,7 +316,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), @@ -402,7 +409,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 @@ -510,52 +517,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 @@ -655,6 +626,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, @@ -665,3 +647,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 936693fc..34491230 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -350,22 +350,21 @@ class SSHClient (ClosingContextManager): server_hostkey_name = "[%s]:%d" % (hostname, port) our_server_keys = None - # 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_keys = self._system_host_keys.get(server_hostkey_name) - if our_server_keys is None: - our_server_keys = self._host_keys.get(server_hostkey_name) - if our_server_keys is not None: - keytype = our_server_keys.keys()[0] - sec_opts = t.get_security_options() - other_types = [x for x in sec_opts.key_types if x != keytype] - sec_opts.key_types = [keytype] + other_types + our_server_keys = self._system_host_keys.get(server_hostkey_name) + if our_server_keys is None: + our_server_keys = self._host_keys.get(server_hostkey_name) + if our_server_keys is not None: + keytype = our_server_keys.keys()[0] + sec_opts = t.get_security_options() + other_types = [x for x in sec_opts.key_types if x != keytype] + sec_opts.key_types = [keytype] + other_types t.start_client(timeout=timeout) - if not self._transport.use_gss_kex: + # 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.gss_kex_used: server_key = t.get_remote_server_key() if our_server_keys is None: # will raise exception if the key is rejected diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index 3406babb..a2ea9fca 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -83,7 +83,6 @@ class KexGSSGroup1(object): """ 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 @@ -207,15 +206,15 @@ class KexGSSGroup1(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, sha1(str(hm)).digest()) + H = sha1(str(hm)).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) + self.kexgss.ssh_check_mic(mic_token, H) else: - self.kexgss.ssh_check_mic(mic_token, - self.transport.session_id) + self.kexgss.ssh_check_mic(mic_token, H) + self.transport.gss_kex_used = True self.transport._activate_outbound() def _parse_kexgss_init(self, m): @@ -258,6 +257,7 @@ class KexGSSGroup1(object): else: m.add_boolean(False) self.transport._send_message(m) + self.transport.gss_kex_used = True self.transport._activate_outbound() else: m.add_byte(c_MSG_KEXGSS_CONTINUE) @@ -325,7 +325,6 @@ class KexGSSGex(object): """ 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 @@ -501,6 +500,7 @@ class KexGSSGex(object): else: m.add_boolean(False) self.transport._send_message(m) + self.transport.gss_kex_used = True self.transport._activate_outbound() else: m.add_byte(c_MSG_KEXGSS_CONTINUE) @@ -582,11 +582,10 @@ class KexGSSGex(object): 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) + self.kexgss.ssh_check_mic(mic_token, H) else: - self.kexgss.ssh_check_mic(mic_token, - self.transport.session_id) + self.kexgss.ssh_check_mic(mic_token, H) + self.transport.gss_kex_used = True self.transport._activate_outbound() def _parse_kexgss_error(self, m): diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 51c02365..14b8b58a 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -341,7 +341,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager): handle = msg.get_binary() self._log( DEBUG, - 'open(%r, %r) -> %s' % (filename, mode, hexlify(handle))) + 'open(%r, %r) -> %s' % (filename, mode, u(hexlify(handle)))) return SFTPFile(self, handle, mode, bufsize) # Python continues to vacillate about "open" vs "file"... diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py index 337cdbeb..bc34db94 100644 --- a/paramiko/sftp_file.py +++ b/paramiko/sftp_file.py @@ -30,7 +30,7 @@ import time from paramiko.common import DEBUG from paramiko.file import BufferedFile -from paramiko.py3compat import long +from paramiko.py3compat import u, long from paramiko.sftp import ( CMD_CLOSE, CMD_READ, CMD_DATA, SFTPError, CMD_WRITE, CMD_STATUS, CMD_FSTAT, CMD_ATTRS, CMD_FSETSTAT, CMD_EXTENDED, @@ -65,15 +65,15 @@ class SFTPFile (BufferedFile): self._reqs = deque() def __del__(self): - self._close(async=True) + self._close(async_=True) def close(self): """ Close the file. """ - self._close(async=False) + self._close(async_=False) - def _close(self, async=False): + def _close(self, async_=False): # We allow double-close without signaling an error, because real # Python file objects do. However, we must protect against actually # sending multiple CMD_CLOSE packets, because after we close our @@ -83,12 +83,12 @@ class SFTPFile (BufferedFile): # __del__.) if self._closed: return - self.sftp._log(DEBUG, 'close(%s)' % hexlify(self.handle)) + self.sftp._log(DEBUG, 'close(%s)' % u(hexlify(self.handle))) if self.pipelined: self.sftp._finish_responses(self) BufferedFile.close(self) try: - if async: + if async_: # GC'd file handle could be called from an arbitrary thread # -- don't wait for a response self.sftp._async_request(type(None), CMD_CLOSE, self.handle) 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 bab23fa1..21d2d800 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -139,6 +139,11 @@ class Transport(threading.Thread, ClosingContextManager): 'diffie-hellman-group14-sha1', 'diffie-hellman-group1-sha1', ) + _preferred_gsskex = ( + 'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==', + 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==', + 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==', + ) _preferred_compression = ('none',) _cipher_info = { @@ -344,12 +349,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 @@ -857,7 +857,7 @@ class Transport(threading.Thread, ClosingContextManager): if event.is_set(): break elif start_ts + timeout < time.time(): - raise SSHException('Timeout openning channel.') + raise SSHException('Timeout opening channel.') chan = self._channels.get(chanid) if chan is not None: return chan @@ -1858,6 +1858,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() @@ -1992,6 +1994,7 @@ class Transport(threading.Thread, ClosingContextManager): self.clear_to_send.clear() finally: self.clear_to_send_lock.release() + self.gss_kex_used = False self.in_kex = True if self.server_mode: mp_required_prefix = 'diffie-hellman-group-exchange-sha' @@ -1,6 +1,9 @@ [wheel] universal = 1 +[metadata] +license_file = LICENSE + [coverage:run] omit = paramiko/_winapi.py diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 3d75aee2..be13bfeb 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -3,8 +3,38 @@ Changelog ========= * :bug:`1039` Ed25519 auth key decryption raised an unexpected exception when - given a unicode password string (typical in python 3). - Report by Theodor van Nahl and fix by Pierce Lopez. + given a unicode password string (typical in python 3). Report by Theodor van + Nahl and fix by Pierce Lopez. +* :bug:`1108 (1.17+)` Rename a private method keyword argument (which was named + ``async``) so that we're compatible with the upcoming Python 3.7 release + (where ``async`` is a new keyword.) Thanks to ``@vEpiphyte`` for the report. +* :support:`- backported` Include LICENSE file in wheel archives. +* :release:`2.2.2 <2017-09-18>` +* :release:`2.1.4 <2017-09-18>` +* :release:`2.0.7 <2017-09-18>` +* :release:`1.18.4 <2017-09-18>` +* :bug:`1065` Add rekeying support to GSSAPI connections, which was erroneously + missing. Without this fix, any attempt to renegotiate the transport keys for + a ``gss-kex``-authed `~paramiko.transport.Transport` would cause a MIC + failure and terminate the connection. Thanks to Sebastian Deiß and Anselm + Kruis for the patch. +* :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 (1.17+)` (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. +* :bug:`945 (1.18+)` (backport of :issue:`910` and re: :issue:`865`) SSHClient + now requests the type of host key it has (e.g. from known_hosts) and does not + consider a different type to be a "Missing" host key. This fixes a common + case where an ECDSA key is in known_hosts and the server also has an RSA host + key. Thanks to Pierce Lopez. * :support:`1012` (via :issue:`1016`) Enhance documentation around the new `SFTP.posix_rename <paramiko.sftp_client.SFTPClient.posix_rename>` method so it's referenced in the 'standard' ``rename`` method for increased visibility. diff --git a/tests/test_client.py b/tests/test_client.py index e912d5b2..7710055b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -159,6 +159,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') @@ -402,6 +403,66 @@ class SSHClientTest (unittest.TestCase): auth_timeout=0.5, ) + def test_10_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_11_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_12_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_13_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 + ) + def _client_host_key_bad(self, host_key): threading.Thread(target=self._run).start() hostname = '[%s]:%d' % (self.addr, self.port) diff --git a/tests/test_kex_gss.py b/tests/test_kex_gss.py index 3bf788da..af342a7c 100644 --- a/tests/test_kex_gss.py +++ b/tests/test_kex_gss.py @@ -93,7 +93,7 @@ class GSSKexTest(unittest.TestCase): server = NullServer() self.ts.start_server(self.event, server) - def test_1_gsskex_and_auth(self): + def _test_gsskex_and_auth(self, gss_host, rekey=False): """ Verify that Paramiko can handle SSHv2 GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and user authentication with the GSS-API @@ -106,16 +106,19 @@ class GSSKexTest(unittest.TestCase): 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) + gss_auth=True, gss_kex=True, gss_host=gss_host) 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()) + self.assertEquals(True, self.tc.get_transport().gss_kex_used) stdin, stdout, stderr = self.tc.exec_command('yes') schan = self.ts.accept(1.0) + if rekey: + self.tc.get_transport().renegotiate_keys() schan.send('Hello there.\n') schan.send_stderr('This is on stderr.\n') @@ -129,3 +132,17 @@ class GSSKexTest(unittest.TestCase): stdin.close() stdout.close() stderr.close() + + 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. + """ + self._test_gsskex_and_auth(gss_host=None) + + def test_2_gsskex_and_auth_rekey(self): + """ + Verify that Paramiko can rekey. + """ + self._test_gsskex_and_auth(gss_host=None, rekey=True) 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) |