summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJeff Forcier <jeff@bitprophet.org>2017-11-29 12:11:31 -0800
committerJeff Forcier <jeff@bitprophet.org>2017-11-29 12:11:31 -0800
commitc72b688286a4d897b9370acb355c17b6553e7302 (patch)
treebd9ad6b4a2fb74377ebd323eec2cbe2e485f1836
parenteec27bd4a0809b5096c5465033815d2414f309c2 (diff)
parentb999f40bea04c4bade6c9f52627b6b6b93df6cda (diff)
Merge branch '2.2' into 1051-int
-rw-r--r--.travis.yml3
-rw-r--r--[-rwxr-xr-x]demos/demo_simple.py11
-rw-r--r--dev-requirements.txt8
-rw-r--r--paramiko/__init__.py2
-rw-r--r--paramiko/_version.py2
-rw-r--r--paramiko/auth_handler.py195
-rw-r--r--paramiko/client.py25
-rw-r--r--paramiko/kex_gss.py21
-rw-r--r--paramiko/sftp_client.py2
-rw-r--r--paramiko/sftp_file.py12
-rw-r--r--paramiko/ssh_gss.py36
-rw-r--r--paramiko/transport.py17
-rw-r--r--setup.cfg3
-rw-r--r--sites/www/changelog.rst34
-rw-r--r--tests/test_client.py61
-rw-r--r--tests/test_kex_gss.py21
-rw-r--r--tests/test_ssh_gss.py45
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'
diff --git a/setup.cfg b/setup.cfg
index f2c1499d..8cc271fe 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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)