summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJeff Forcier <jeff@bitprophet.org>2017-09-12 13:12:37 -0700
committerJeff Forcier <jeff@bitprophet.org>2017-09-12 13:12:37 -0700
commit20e0285628cd5f075b6f81c897a440e2eb5da95e (patch)
tree6e467c707fef679e17a7eab5e86151ea28293257
parent93f38814d054fbc1fde2f2e7b59b175ed9f9fb5f (diff)
parent89a9b583e46f634792d814c5cff8e0cecdb5fa50 (diff)
Merge branch '2.0' into 2.1
-rw-r--r--[-rwxr-xr-x]demos/demo_simple.py11
-rw-r--r--paramiko/__init__.py2
-rw-r--r--paramiko/auth_handler.py195
-rw-r--r--paramiko/ssh_gss.py36
-rw-r--r--paramiko/transport.py14
-rw-r--r--sites/www/changelog.rst8
-rw-r--r--tests/test_ssh_gss.py45
7 files changed, 221 insertions, 90 deletions
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/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/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 d26af833..7d5cde56 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -133,6 +133,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 = {
@@ -334,12 +339,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
@@ -1846,6 +1846,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/sites/www/changelog.rst b/sites/www/changelog.rst
index 8665ee94..cac5a715 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,6 +2,14 @@
Changelog
=========
+* :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.
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)