diff options
author | Anselm Kruis <a.kruis@science-computing.de> | 2017-08-01 21:50:24 +0200 |
---|---|---|
committer | Anselm Kruis <a.kruis@science-computing.de> | 2017-08-04 18:39:14 +0200 |
commit | c214e5043fdaf72e355bc014239ebeddf269059d (patch) | |
tree | f85cb461efb13f40a7cdb50f635346fa4b65583b | |
parent | 0595ee447ded0ab5bd178e1c3299a77706328a09 (diff) |
AuthHandler: fix the server-mode "gssapi-with-mic" logic
A paramiko server is now able to handle a restart of the user
authentication during the GSS-API token exchange. This may occur, if
the client detects a local GSSAPI problem (e.g. a missing kerberos
ticket) and continues with another authentication method.
The added test case test_2_auth_trickledown still fails, because the
paramiko client contains a bug too.
-rw-r--r-- | paramiko/auth_handler.py | 151 | ||||
-rw-r--r-- | paramiko/transport.py | 2 | ||||
-rw-r--r-- | tests/test_ssh_gss.py | 45 |
3 files changed, 145 insertions, 53 deletions
diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 33f01da6..13c41c9b 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -503,52 +503,13 @@ 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 @@ -658,3 +619,101 @@ 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/transport.py b/paramiko/transport.py index 19d8ee70..c68397dd 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1838,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/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) |