diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README | 18 | ||||
-rw-r--r-- | demos/demo_server.py | 37 | ||||
-rw-r--r--[-rwxr-xr-x] | demos/demo_sftp.py | 19 | ||||
-rwxr-xr-x | demos/demo_simple.py | 24 | ||||
-rw-r--r-- | dev-requirements.txt | 2 | ||||
-rw-r--r-- | paramiko/__init__.py | 1 | ||||
-rw-r--r-- | paramiko/agent.py | 1 | ||||
-rw-r--r-- | paramiko/auth_handler.py | 221 | ||||
-rw-r--r-- | paramiko/client.py | 80 | ||||
-rw-r--r-- | paramiko/common.py | 19 | ||||
-rw-r--r-- | paramiko/kex_group1.py | 20 | ||||
-rw-r--r-- | paramiko/kex_group14.py | 33 | ||||
-rw-r--r-- | paramiko/kex_gss.py | 634 | ||||
-rw-r--r-- | paramiko/message.py | 27 | ||||
-rw-r--r-- | paramiko/pipe.py | 1 | ||||
-rw-r--r-- | paramiko/primes.py | 1 | ||||
-rw-r--r-- | paramiko/server.py | 74 | ||||
-rw-r--r-- | paramiko/ssh_gss.py | 598 | ||||
-rw-r--r-- | paramiko/transport.py | 116 | ||||
-rw-r--r-- | sites/docs/api/kex_gss.rst | 5 | ||||
-rw-r--r-- | sites/docs/api/ssh_gss.rst | 14 | ||||
-rw-r--r-- | sites/docs/index.rst | 2 | ||||
-rw-r--r-- | sites/www/changelog.rst | 1 | ||||
-rwxr-xr-x | test.py | 34 | ||||
-rw-r--r-- | tests/stub_sftp.py | 1 | ||||
-rw-r--r-- | tests/test_buffered_pipe.py | 1 | ||||
-rw-r--r-- | tests/test_gssapi.py | 167 | ||||
-rw-r--r-- | tests/test_kex_gss.py | 143 | ||||
-rw-r--r-- | tests/test_ssh_gss.py | 136 |
30 files changed, 2378 insertions, 53 deletions
@@ -5,6 +5,7 @@ dist/ paramiko.egg-info/ test.log docs/ +demos/*.log !sites/docs _build .coverage @@ -72,6 +72,24 @@ Bugs & Support Please file bug reports at https://github.com/paramiko/paramiko/. There is currently no mailing list but we plan to create a new one ASAP. +Kerberos Support +---------------- + +If you want paramiko to do kerberos authentication or key exchange using GSS-API or SSPI, you +need the following python packages: + +- pyasn1 0.1.7 or better +- python-gssapi 0.4.0 or better (Unix) +- pywin32 2.1.8 or better (Windows) + +So you have to install pyasn1 and python-gssapi on Unix or pywin32 on Windows. +To enable GSS-API / SSPI authentication or key exchange see the demos or paramiko docs. +Note: If you use Microsoft SSPI for kerberos authentication and credential +delegation in paramiko, make sure that the target host is trusted for +delegation in the active directory configuration. For details see: +http://technet.microsoft.com/en-us/library/cc738491%28v=ws.10%29.aspx + + Demo ---- diff --git a/demos/demo_server.py b/demos/demo_server.py index bb35258b..74e4677e 100644 --- a/demos/demo_server.py +++ b/demos/demo_server.py @@ -66,9 +66,39 @@ class Server (paramiko.ServerInterface): if (username == 'robey') and (key == self.good_pub_key): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED + + def check_auth_gssapi_with_mic(self, username, + gss_authenticated=paramiko.AUTH_FAILED, + cc_file=None): + """ + @note: We are just checking in L{AuthHandler} that the given user is + a valid krb5 principal! + We don't check if the krb5 principal is allowed to log in on + the server, because there is no way to do that in python. So + if you develop your own SSH server with paramiko for a certain + platform like Linux, you should call C{krb5_kuserok()} in your + local kerberos library to make sure that the krb5_principal has + an account on the server and is allowed to log in as a user. + @see: U{krb5_kuserok() man page <http://www.unix.com/man-page/all/3/krb5_kuserok/>} + """ + if gss_authenticated == paramiko.AUTH_SUCCESSFUL: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def check_auth_gssapi_keyex(self, username, + gss_authenticated=paramiko.AUTH_FAILED, + cc_file=None): + if gss_authenticated == paramiko.AUTH_SUCCESSFUL: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def enable_auth_gssapi(self): + UseGSSAPI = True + GSSAPICleanupCredentials = False + return UseGSSAPI def get_allowed_auths(self, username): - return 'password,publickey' + return 'gssapi-keyex,gssapi-with-mic,password,publickey' def check_channel_shell_request(self, channel): self.event.set() @@ -79,6 +109,8 @@ class Server (paramiko.ServerInterface): return True +DoGSSAPIKeyExchange = True + # now connect try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -101,7 +133,8 @@ except Exception as e: print('Got a connection!') try: - t = paramiko.Transport(client) + t = paramiko.Transport(client, gss_kex=DoGSSAPIKeyExchange) + t.set_gss_host(socket.getfqdn("")) try: t.load_server_moduli() except: diff --git a/demos/demo_sftp.py b/demos/demo_sftp.py index a34f2b19..2cb44701 100755..100644 --- a/demos/demo_sftp.py +++ b/demos/demo_sftp.py @@ -34,6 +34,11 @@ from paramiko.py3compat import input # setup logging paramiko.util.log_to_file('demo_sftp.log') +# Paramiko client configuration +UseGSSAPI = True # enable GSS-API / SSPI authentication +DoGSSAPIKeyExchange = True +Port = 22 + # get hostname username = '' if len(sys.argv) > 1: @@ -45,10 +50,10 @@ else: if len(hostname) == 0: print('*** Hostname required.') sys.exit(1) -port = 22 + if hostname.find(':') >= 0: hostname, portstr = hostname.split(':') - port = int(portstr) + Port = int(portstr) # get username @@ -57,7 +62,10 @@ if username == '': username = input('Username [%s]: ' % default_username) if len(username) == 0: username = default_username -password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) +if not UseGSSAPI: + password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) +else: + password = None # get host key, if we know one @@ -81,8 +89,9 @@ if hostname in host_keys: # now, connect and use paramiko Transport to negotiate SSH2 across the connection try: - t = paramiko.Transport((hostname, port)) - t.connect(username=username, password=password, hostkey=hostkey) + t = paramiko.Transport((hostname, Port)) + t.connect(hostkey, username, password, gss_host=socket.getfqdn(hostname), + gss_auth=UseGSSAPI, gss_kex=DoGSSAPIKeyExchange) sftp = paramiko.SFTPClient.from_transport(t) # dirlist on remote host diff --git a/demos/demo_simple.py b/demos/demo_simple.py index ae631e43..7f179b90 100755 --- a/demos/demo_simple.py +++ b/demos/demo_simple.py @@ -36,6 +36,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 +Port = 22 # get hostname username = '' @@ -48,10 +52,10 @@ else: if len(hostname) == 0: print('*** Hostname required.') sys.exit(1) -port = 22 + if hostname.find(':') >= 0: hostname, portstr = hostname.split(':') - port = int(portstr) + Port = int(portstr) # get username @@ -60,7 +64,8 @@ if username == '': username = input('Username [%s]: ' % default_username) if len(username) == 0: username = default_username -password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) +if not UseGSSAPI or (not UseGSSAPI and not DoGSSAPIKeyExchange): + password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) # now, connect and use paramiko Client to negotiate SSH2 across the connection @@ -69,7 +74,18 @@ try: client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.WarningPolicy()) print('*** Connecting...') - client.connect(hostname, port, username, password) + if not UseGSSAPI or (not UseGSSAPI and not DoGSSAPIKeyExchange): + client.connect(hostname, Port, username, password) + else: + # SSPI works only with the FQDN of the target host + hostname = socket.getfqdn(hostname) + try: + client.connect(hostname, Port, username, gss_auth=UseGSSAPI, + gss_kex=DoGSSAPIKeyExchange) + except Exception: + password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) + client.connect(hostname, Port, username, password) + chan = client.invoke_shell() print(repr(client.get_transport())) print('*** Here we go!\n') diff --git a/dev-requirements.txt b/dev-requirements.txt index 91ae8549..5207903f 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -6,4 +6,4 @@ invoke>=0.7.0 invocations>=0.5.0 sphinx>=1.1.3 alabaster>=0.4.0 -releases>=0.5.2 +releases>=0.5.2
\ No newline at end of file diff --git a/paramiko/__init__.py b/paramiko/__init__.py index b1d9aaa9..f4ce937b 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -31,6 +31,7 @@ __license__ = "GNU Lesser General Public License (LGPL)" from paramiko.transport import SecurityOptions, Transport from paramiko.client import SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, WarningPolicy from paramiko.auth_handler import AuthHandler +from paramiko.ssh_gss import GSSAuth, GSS_AUTH_AVAILABLE from paramiko.channel import Channel, ChannelFile from paramiko.ssh_exception import SSHException, PasswordRequiredException, \ BadAuthenticationType, ChannelException, BadHostKeyException, \ diff --git a/paramiko/agent.py b/paramiko/agent.py index 5a08d452..4f463449 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -43,6 +43,7 @@ cSSH2_AGENTC_SIGN_REQUEST = byte_chr(13) SSH2_AGENT_SIGN_RESPONSE = 14 + class AgentSSH(object): def __init__(self): self._conn = None diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 57babef0..cb06da2d 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -28,20 +28,27 @@ from paramiko.common import cMSG_SERVICE_REQUEST, cMSG_DISCONNECT, \ cMSG_USERAUTH_INFO_REQUEST, WARNING, AUTH_FAILED, cMSG_USERAUTH_PK_OK, \ cMSG_USERAUTH_INFO_RESPONSE, MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT, \ MSG_USERAUTH_REQUEST, MSG_USERAUTH_SUCCESS, MSG_USERAUTH_FAILURE, \ - MSG_USERAUTH_BANNER, MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE + MSG_USERAUTH_BANNER, MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE, \ + cMSG_USERAUTH_GSSAPI_RESPONSE, cMSG_USERAUTH_GSSAPI_TOKEN, \ + cMSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, cMSG_USERAUTH_GSSAPI_ERROR, \ + cMSG_USERAUTH_GSSAPI_ERRTOK, cMSG_USERAUTH_GSSAPI_MIC,\ + MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN, \ + MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, MSG_USERAUTH_GSSAPI_ERROR, \ + MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC from paramiko.message import Message from paramiko.py3compat import bytestring from paramiko.ssh_exception import SSHException, AuthenticationException, \ BadAuthenticationType, PartialAuthentication from paramiko.server import InteractiveQuery +from paramiko.ssh_gss import GSSAuth class AuthHandler (object): """ Internal class to handle the mechanics of authentication. """ - + def __init__(self, transport): self.transport = weakref.proxy(transport) self.username = None @@ -56,7 +63,10 @@ class AuthHandler (object): # for server mode: self.auth_username = None self.auth_fail_count = 0 - + # for GSSAPI + self.gss_host = None + self.gss_deleg_creds = True + def is_authenticated(self): return self.authenticated @@ -97,7 +107,7 @@ class AuthHandler (object): self._request_auth() finally: self.transport.lock.release() - + def auth_interactive(self, username, handler, event, submethods=''): """ response_list = handler(title, instructions, prompt_list) @@ -112,7 +122,29 @@ class AuthHandler (object): self._request_auth() finally: self.transport.lock.release() - + + def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds, event): + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = 'gssapi-with-mic' + self.username = username + self.gss_host = gss_host + self.gss_deleg_creds = gss_deleg_creds + self._request_auth() + finally: + self.transport.lock.release() + + def auth_gssapi_keyex(self, username, event): + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = 'gssapi-keyex' + self.username = username + self._request_auth() + finally: + self.transport.lock.release() + def abort(self): if self.auth_event is not None: self.auth_event.set() @@ -211,6 +243,82 @@ class AuthHandler (object): elif self.auth_method == 'keyboard-interactive': m.add_string('') m.add_string(self.submethods) + elif self.auth_method == "gssapi-with-mic": + sshgss = GSSAuth(self.auth_method, self.gss_deleg_creds) + m.add_bytes(sshgss.ssh_gss_oids()) + # send the supported GSSAPI OIDs to the server + self.transport._send_message(m) + ptype, m = self.transport.packetizer.read_message() + if ptype == MSG_USERAUTH_BANNER: + self._parse_userauth_banner(m) + ptype, m = self.transport.packetizer.read_message() + if ptype == MSG_USERAUTH_GSSAPI_RESPONSE: + """ + Read the mechanism selected by the server. + We send just the Kerberos V5 OID, so the server can only + respond with this OID. + """ + 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,)) + 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) + """ + 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. + """ + if next_token is None: + break + else: + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN) + m.add_string(next_token) + self.transport.send_message(m) + else: + raise SSHException("Received Package: %s" % MSG_NAMES[ptype]) + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_MIC) + # send the MIC to the server + m.add_string(sshgss.ssh_get_mic(self.transport.session_id)) + elif ptype == MSG_USERAUTH_GSSAPI_ERRTOK: + """ + RFC 4462 says we are not required to implement GSS-API error + messages. + :see: `RFC 4462 Section 3.8 <http://www.ietf.org/rfc/rfc4462.txt>`_ + """ + raise SSHException("Server returned an error token") + elif ptype == MSG_USERAUTH_GSSAPI_ERROR: + maj_status = m.get_int() + min_status = m.get_int() + err_msg = m.get_string() + lang_tag = m.get_string() # we don't care! + raise SSHException("GSS-API Error:\nMajor Status: %s\n\ + Minor Status: %s\ \nError Message:\ + %s\n") % (str(maj_status), + str(min_status), + err_msg) + elif ptype == MSG_USERAUTH_FAILURE: + self._parse_userauth_failure(m) + return + else: + raise SSHException("Received Package: %s" % MSG_NAMES[ptype]) + elif self.auth_method == 'gssapi-keyex' and\ + self.transport.gss_kex_used: + kexgss = self.transport.kexgss_ctxt + kexgss.set_username(self.username) + mic_token = kexgss.ssh_get_mic(self.transport.session_id) + m.add_string(mic_token) elif self.auth_method == 'none': pass else: @@ -278,6 +386,8 @@ class AuthHandler (object): self._disconnect_no_more_auth() return self.auth_username = username + # check if GSS-API authentication is enabled + gss_auth = self.transport.server_object.enable_auth_gssapi() if method == 'none': result = self.transport.server_object.check_auth_none(username) @@ -343,6 +453,107 @@ class AuthHandler (object): # make interactive query instead of response self._interactive_query(result) return + elif method == "gssapi-with-mic" and gss_auth: + sshgss = GSSAuth(method) + """ + OpenSSH sends just one OID. It's the Kerveros V5 OID and that's + the only OID we support. + """ + # read the number of OID mechanisms supported by the client + mechs = m.get_int() + """ + We can't accept more than one OID, so if the SSH client send more + than one disconnect + """ + if mechs > 1: + self.transport._log(INFO, + 'Disconnect: Received more than one GSS-API OID mechanism') + self._disconnect_no_more_auth() + desired_mech = m.get_string() + mech_ok = sshgss.ssh_check_mech(desired_mech) + # if we don't support the mechanism, disconnect. + if not mech_ok: + self.transport._log(INFO, + 'Disconnect: Received an invalid GSS-API OID mechanism') + self._disconnect_no_more_auth() + # send the Kerberos V5 GSSAPI OID to the client + supported_mech = sshgss.ssh_gss_oids("server") + """ + RFC 4462 says we are not required to implement GSS-API error + messages. + :see: `RFC 4462 Section 3.8 <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: + raise SSHException("Client asked to handle paket %s" + %MSG_NAMES[ptype]) + # check MIC + ptype, m = self.transport.packetizer.read_message() + if ptype == MSG_USERAUTH_GSSAPI_MIC: + break + mic_token = m.get_string() + try: + retval = sshgss.ssh_check_mic(mic_token, + self.transport.session_id, + username) + except Exception: + result = AUTH_FAILED + self._send_auth_result(username, method, result) + raise + if retval == 0: + """ + :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) + else: + result = AUTH_FAILED + elif method == "gssapi-keyex" and gss_auth: + mic_token = m.get_string() + sshgss = self.transport.kexgss_ctxt + if sshgss is None: + # If there is no valid context, we reject the authentication + result = AUTH_FAILED + self._send_auth_result(username, method, result) + try: + retval = sshgss.ssh_check_mic(mic_token, + self.transport.session_id, + self.auth_username) + except Exception: + result = AUTH_FAILED + self._send_auth_result(username, method, result) + raise + if retval == 0: + result = AUTH_SUCCESSFUL + self.transport.server_object.check_auth_gssapi_keyex(username, + result) + else: + result = AUTH_FAILED else: result = self.transport.server_object.check_auth_none(username) # okay, send result diff --git a/paramiko/client.py b/paramiko/client.py index c1bf4735..539299ea 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -171,7 +171,8 @@ class SSHClient (object): def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=None, key_filename=None, timeout=None, allow_agent=True, look_for_keys=True, - compress=False, sock=None): + compress=False, sock=None, gss_auth=False, gss_kex=False, + gss_deleg_creds=True, gss_host=None): """ Connect to an SSH server and authenticate to it. The server's host key is checked against the system host keys (see `load_system_host_keys`) @@ -210,6 +211,10 @@ class SSHClient (object): :param socket sock: an open socket or socket-like object (such as a `.Channel`) to use for communication to the target host + :param bool gss_auth: ``True`` if you want to use GSS-API authentication + :param bool gss_kex: Perform GSS-API Key Exchange and user authentication + :param bool gss_deleg_creds: Delegate GSS-API client credentials or not + :param str gss_host: The targets name in the kerberos database. default: hostname :raises BadHostKeyException: if the server's host key could not be verified @@ -235,8 +240,14 @@ class SSHClient (object): pass retry_on_signal(lambda: sock.connect(addr)) - t = self._transport = Transport(sock) + t = self._transport = Transport(sock, gss_kex, gss_deleg_creds) t.use_compression(compress=compress) + if gss_kex and gss_host is None: + t.set_gss_host(hostname) + elif gss_kex and gss_host is not None: + t.set_gss_host(gss_host) + else: + pass if self._log_channel is not None: t.set_log_channel(self._log_channel) t.start_client() @@ -249,17 +260,27 @@ class SSHClient (object): server_hostkey_name = hostname else: server_hostkey_name = "[%s]:%d" % (hostname, port) - our_server_key = self._system_host_keys.get(server_hostkey_name, {}).get(keytype, None) - if our_server_key is None: - our_server_key = self._host_keys.get(server_hostkey_name, {}).get(keytype, None) - if our_server_key is None: - # will raise exception if the key is rejected; let that fall out - self._policy.missing_host_key(self, server_hostkey_name, server_key) - # if the callback returns, assume the key is ok - our_server_key = server_key - - if server_key != our_server_key: - raise BadHostKeyException(hostname, server_key, our_server_key) + + """ + 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 out client. + """ + if not self._transport.use_gss_kex: + our_server_key = self._system_host_keys.get(server_hostkey_name, + {}).get(keytype, None) + if our_server_key is None: + our_server_key = self._host_keys.get(server_hostkey_name, + {}).get(keytype, None) + if our_server_key is None: + # will raise exception if the key is rejected; let that fall out + self._policy.missing_host_key(self, server_hostkey_name, + server_key) + # if the callback returns, assume the key is ok + our_server_key = server_key + + if server_key != our_server_key: + raise BadHostKeyException(hostname, server_key, our_server_key) if username is None: username = getpass.getuser() @@ -270,7 +291,10 @@ class SSHClient (object): key_filenames = [key_filename] else: key_filenames = key_filename - self._auth(username, password, pkey, key_filenames, allow_agent, look_for_keys) + if gss_host is None: + gss_host = hostname + self._auth(username, password, pkey, key_filenames, allow_agent, + look_for_keys, gss_auth, gss_kex, gss_deleg_creds, gss_host) def close(self): """ @@ -354,7 +378,8 @@ class SSHClient (object): """ return self._transport - def _auth(self, username, password, pkey, key_filenames, allow_agent, look_for_keys): + def _auth(self, username, password, pkey, key_filenames, allow_agent, + look_for_keys, gss_auth, gss_kex, gss_deleg_creds, gss_host): """ Try, in order: @@ -370,6 +395,31 @@ class SSHClient (object): two_factor = False allowed_types = [] + """ + If GSS-API support and GSS-PI Key Exchange was performed, we attempt + authentication with gssapi-keyex. + """ + if gss_kex and self._transport.gss_kex_used: + try: + self._transport.auth_gssapi_keyex(username) + return + except Exception as e: + saved_exception = e + + """ + Try GSS-API authentication (gssapi-with-mic) only if GSS-API Key + Exchange is not performed, because if we use GSS-API for the key + exchange, there is already a fully established GSS-API context, so + why should we do that again? + """ + if gss_auth: + try: + self._transport.auth_gssapi_with_mic(username, gss_host, + gss_deleg_creds) + return + except Exception as e: + saved_exception = e + if pkey is not None: try: self._log(DEBUG, 'Trying SSH key %s' % hexlify(pkey.get_fingerprint())) diff --git a/paramiko/common.py b/paramiko/common.py index 18298922..22ee8810 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -29,6 +29,9 @@ MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \ MSG_USERAUTH_BANNER = range(50, 54) MSG_USERAUTH_PK_OK = 60 MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE = range(60, 62) +MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN = range(60, 62) +MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, MSG_USERAUTH_GSSAPI_ERROR,\ +MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC = range(63, 67) MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE = range(80, 83) MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \ MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA, \ @@ -50,6 +53,12 @@ cMSG_USERAUTH_BANNER = byte_chr(MSG_USERAUTH_BANNER) cMSG_USERAUTH_PK_OK = byte_chr(MSG_USERAUTH_PK_OK) cMSG_USERAUTH_INFO_REQUEST = byte_chr(MSG_USERAUTH_INFO_REQUEST) cMSG_USERAUTH_INFO_RESPONSE = byte_chr(MSG_USERAUTH_INFO_RESPONSE) +cMSG_USERAUTH_GSSAPI_RESPONSE = byte_chr(MSG_USERAUTH_GSSAPI_RESPONSE) +cMSG_USERAUTH_GSSAPI_TOKEN = byte_chr(MSG_USERAUTH_GSSAPI_TOKEN) +cMSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE = byte_chr(MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE) +cMSG_USERAUTH_GSSAPI_ERROR = byte_chr(MSG_USERAUTH_GSSAPI_ERROR) +cMSG_USERAUTH_GSSAPI_ERRTOK = byte_chr(MSG_USERAUTH_GSSAPI_ERRTOK) +cMSG_USERAUTH_GSSAPI_MIC = byte_chr(MSG_USERAUTH_GSSAPI_MIC) cMSG_GLOBAL_REQUEST = byte_chr(MSG_GLOBAL_REQUEST) cMSG_REQUEST_SUCCESS = byte_chr(MSG_REQUEST_SUCCESS) cMSG_REQUEST_FAILURE = byte_chr(MSG_REQUEST_FAILURE) @@ -80,6 +89,8 @@ MSG_NAMES = { 32: 'kex32', 33: 'kex33', 34: 'kex34', + 40: 'kex40', + 41: 'kex41', MSG_USERAUTH_REQUEST: 'userauth-request', MSG_USERAUTH_FAILURE: 'userauth-failure', MSG_USERAUTH_SUCCESS: 'userauth-success', @@ -99,7 +110,13 @@ MSG_NAMES = { MSG_CHANNEL_CLOSE: 'channel-close', MSG_CHANNEL_REQUEST: 'channel-request', MSG_CHANNEL_SUCCESS: 'channel-success', - MSG_CHANNEL_FAILURE: 'channel-failure' + MSG_CHANNEL_FAILURE: 'channel-failure', + MSG_USERAUTH_GSSAPI_RESPONSE: 'userauth-gssapi-response', + MSG_USERAUTH_GSSAPI_TOKEN: 'userauth-gssapi-token', + MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE: 'userauth-gssapi-exchange-complete', + MSG_USERAUTH_GSSAPI_ERROR: 'userauth-gssapi-error', + MSG_USERAUTH_GSSAPI_ERRTOK: 'userauth-gssapi-error-token', + MSG_USERAUTH_GSSAPI_MIC: 'userauth-gssapi-mic' } diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 7ccceea6..a88f00d2 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -34,16 +34,16 @@ from paramiko.ssh_exception import SSHException _MSG_KEXDH_INIT, _MSG_KEXDH_REPLY = range(30, 32) c_MSG_KEXDH_INIT, c_MSG_KEXDH_REPLY = [byte_chr(c) for c in range(30, 32)] -# draft-ietf-secsh-transport-09.txt, page 17 -P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF -G = 2 - b7fffffffffffffff = byte_chr(0x7f) + max_byte * 7 b0000000000000000 = zero_byte * 8 class KexGroup1(object): + # draft-ietf-secsh-transport-09.txt, page 17 + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF + G = 2 + name = 'diffie-hellman-group1-sha1' def __init__(self, transport): @@ -56,11 +56,11 @@ class KexGroup1(object): self._generate_x() if self.transport.server_mode: # compute f = g^x mod p, but don't send it yet - self.f = pow(G, self.x, P) + self.f = pow(self.G, self.x, self.P) self.transport._expect_packet(_MSG_KEXDH_INIT) return # compute e = g^x mod p (where g=2), and send it - self.e = pow(G, self.x, P) + self.e = pow(self.G, self.x, self.P) m = Message() m.add_byte(c_MSG_KEXDH_INIT) m.add_mpint(self.e) @@ -94,10 +94,10 @@ class KexGroup1(object): # client mode host_key = m.get_string() self.f = m.get_mpint() - if (self.f < 1) or (self.f > P - 1): + if (self.f < 1) or (self.f > self.P - 1): raise SSHException('Server kex "f" is out of range') sig = m.get_binary() - K = pow(self.f, self.x, P) + K = pow(self.f, self.x, self.P) # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) hm = Message() hm.add(self.transport.local_version, self.transport.remote_version, @@ -113,9 +113,9 @@ class KexGroup1(object): def _parse_kexdh_init(self, m): # server mode self.e = m.get_mpint() - if (self.e < 1) or (self.e > P - 1): + if (self.e < 1) or (self.e > self.P - 1): raise SSHException('Client kex "e" is out of range') - K = pow(self.e, self.x, P) + K = pow(self.e, self.x, self.P) key = self.transport.get_server_key().asbytes() # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) hm = Message() diff --git a/paramiko/kex_group14.py b/paramiko/kex_group14.py new file mode 100644 index 00000000..a914aeaf --- /dev/null +++ b/paramiko/kex_group14.py @@ -0,0 +1,33 @@ +# Copyright (C) 2013 Torsten Landschoff <torsten@debian.org> +# +# 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 distrubuted 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. + +""" +Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of +2048 bit key halves, using a known "p" prime and "g" generator. +""" + +from paramiko.kex_group1 import KexGroup1 + + +class KexGroup14(KexGroup1): + + # http://tools.ietf.org/html/rfc3526#section-3 + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF + G = 2 + + name = 'diffie-hellman-group14-sha1' diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py new file mode 100644 index 00000000..02f943ba --- /dev/null +++ b/paramiko/kex_gss.py @@ -0,0 +1,634 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss <sebastian.deiss@t-online.de> +# +# +# 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. + + +""" +This module provides GSS-API / SSPI Key Exchange for Paramiko as defined in +RFC 4462 with the following restrictions: +Credential delegation is not supported in server mode, +To Use this module, you need the following additional python packages: +`pyasn1 >= 0.1.7 <https://pypi.python.org/pypi/pyasn1>`_, +`python-gssapi >= 0.4.0 (Unix) <https://pypi.python.org/pypi/python-gssapi>`_, +`pywin32 2.1.8 (Windows) <http://sourceforge.net/projects/pywin32/>`_. + +:summary: SSH2 GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange Module +:version: 0.1 +:author: Sebastian Deiss +:contact: https://github.com/SebastianDeiss/paramiko/issues +:organization: science + computing ag + `EMail <mailto:a.kruis@science-computing.de>`_ +:copyright: (C) 2003-2007 Robey Pointer, (C) 2013-2014 `science + computing ag + <https://www.science-computing.de>`_ +:license: GNU Lesser General Public License (LGPL) +:see: `.ssh_gss` + +Created on 12.12.2013 +""" + + +from Crypto.Hash import SHA +from paramiko.common import * +from paramiko import util +from paramiko.message import Message +from paramiko.py3compat import byte_chr, long, byte_mask, byte_ord +from paramiko.ssh_exception import SSHException + + +MSG_KEXGSS_INIT, MSG_KEXGSS_CONTINUE, MSG_KEXGSS_COMPLETE, MSG_KEXGSS_HOSTKEY,\ +MSG_KEXGSS_ERROR = range(30, 35) +MSG_KEXGSS_GROUPREQ, MSG_KEXGSS_GROUP = range(40, 42) +c_MSG_KEXGSS_INIT, c_MSG_KEXGSS_CONTINUE, c_MSG_KEXGSS_COMPLETE,\ +c_MSG_KEXGSS_HOSTKEY, c_MSG_KEXGSS_ERROR = [byte_chr(c) for c in range(30, 35)] +c_MSG_KEXGSS_GROUPREQ, c_MSG_KEXGSS_GROUP = [byte_chr(c) for c in range(40, 42)] + + +class KexGSSGroup1(object): + """ + GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange + as defined in `RFC 4462 Section 2 <http://www.ietf.org/rfc/rfc4462.txt>`_ + + :note: RFC 4462 says we are not required to implement GSS-API error + messages. + If an error occurs an exception will be thrown and the connection + will be terminated. + :see: `RFC 4462 Section 2.2 <http://www.ietf.org/rfc/rfc4462.txt>`_ + """ + # draft-ietf-secsh-transport-09.txt, page 17 + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF + G = 2 + b7fffffffffffffff = byte_chr(0x7f) + max_byte * 7 + b0000000000000000 = zero_byte * 8 + NAME = "gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==" + + def __init__(self, transport): + self.transport = transport + self.kexgss = self.transport.kexgss_ctxt + self.gss_host = None + self.x = 0 + self.e = 0 + self.f = 0 + + def start_kex(self): + """ + 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 + self.f = pow(self.G, self.x, self.P) + self.transport._expect_packet(MSG_KEXGSS_INIT) + return + # compute e = g^x mod p (where g=2), and send it + self.e = pow(self.G, self.x, self.P) + # Initialize GSS-API Key Exchange + self.gss_host = self.transport.gss_host + m = Message() + m.add_byte(c_MSG_KEXGSS_INIT) + m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host)) + m.add_mpint(self.e) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_HOSTKEY, + MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR) + + def parse_next(self, ptype, m): + """ + Parse the next packet. + + :param char ptype: The type of the incomming packet + :param `.Message` m: The paket content + """ + if self.transport.server_mode and (ptype == MSG_KEXGSS_INIT): + return self._parse_kexgss_init(m) + elif not self.transport.server_mode and (ptype == MSG_KEXGSS_HOSTKEY): + return self._parse_kexgss_hostkey(m) + elif self.transport.server_mode and (ptype == MSG_KEXGSS_CONTINUE): + return self._parse_kexgss_continue(m) + elif not self.transport.server_mode and (ptype == MSG_KEXGSS_COMPLETE): + return self._parse_kexgss_complete(m) + elif ptype == MSG_KEXGSS_ERROR: + return self._parse_kexgss_error(m) + raise SSHException('GSS KexGroup1 asked to handle packet type %d' + % ptype) + + # ## internals... + + def _generate_x(self): + """ + generate an "x" (1 < x < q), where q is (p-1)/2. + p is a 128-byte (1024-bit) number, where the first 64 bits are 1. + therefore q can be approximated as a 2^1023. we drop the subset of + potential x where the first 63 bits are 1, because some of those will be + larger than q (but this is a tiny tiny subset of potential x). + """ + while 1: + x_bytes = self.transport.rng.read(128) + x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:] + if (x_bytes[:8] != self.b7fffffffffffffff) and \ + (x_bytes[:8] != self.b0000000000000000): + break + self.x = util.inflate_long(x_bytes) + + def _parse_kexgss_hostkey(self, m): + """ + Parse the SSH2_MSG_KEXGSS_HOSTKEY message (client mode). + + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message + """ + # client mode + host_key = m.get_string() + self.transport.host_key = host_key + sig = m.get_string() + self.transport._verify_key(host_key, sig) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE) + + def _parse_kexgss_continue(self, m): + """ + Parse the SSH2_MSG_KEXGSS_CONTINUE message. + + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_CONTINUE message + """ + if not self.transport.server_mode: + srv_token = m.get_string() + m = Message() + m.add_byte(c_MSG_KEXGSS_CONTINUE) + m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host, + recv_token=srv_token)) + self.transport.send_message(m) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR) + else: + pass + + def _parse_kexgss_complete(self, m): + """ + Parse the SSH2_MSG_KEXGSS_COMPLETE message (client mode). + + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_COMPLETE message + """ + # client mode + if self.transport.host_key is None: + self.transport.host_key = NullHostKey() + self.f = m.get_mpint() + if (self.f < 1) or (self.f > self.P - 1): + raise SSHException('Server kex "f" is out of range') + mic_token = m.get_string() + """ + This must be TRUE, if there is a GSS-API token in this + message. + """ + bool = m.get_boolean() + srv_token = None + if bool: + srv_token = m.get_string() + K = pow(self.f, self.x, self.P) + # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) + hm = Message() + hm.add(self.transport.local_version, self.transport.remote_version, + self.transport.local_kex_init, self.transport.remote_kex_init) + hm.add_string(self.transport.host_key.__str__()) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + self.transport._set_K_H(K, SHA.new(str(hm)).digest()) + 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) + else: + self.kexgss.ssh_check_mic(mic_token, + self.transport.session_id) + self.transport._activate_outbound() + + def _parse_kexgss_init(self, m): + """ + Parse the SSH2_MSG_KEXGSS_INIT message (server mode). + + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_INIT message + """ + # server mode + client_token = m.get_string() + self.e = m.get_mpint() + if (self.e < 1) or (self.e > self.P - 1): + raise SSHException('Client kex "e" is out of range') + K = pow(self.e, self.x, self.P) + self.transport.host_key = NullHostKey() + key = self.transport.host_key.__str__() + # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) + hm = Message() + hm.add(self.transport.remote_version, self.transport.local_version, + self.transport.remote_kex_init, self.transport.local_kex_init) + hm.add_string(key) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + H = SHA.new(hm.asbytes()).digest() + self.transport._set_K_H(K, H) + srv_token = self.kexgss.ssh_accept_sec_context(self.gss_host, + client_token) + m = Message() + if self.kexgss._gss_srv_ctxt_status: + mic_token = self.kexgss.ssh_get_mic(self.transport.session_id, + gss_kex=True) + m.add_byte(c_MSG_KEXGSS_COMPLETE) + m.add_mpint(self.f) + m.add_string(mic_token) + if srv_token is not None: + m.add_boolean(True) + m.add_string(srv_token) + else: + m.add_boolean(False) + self.transport._send_message(m) + self.transport._activate_outbound() + else: + m.add_byte(c_MSG_KEXGSS_CONTINUE) + m.add_string(srv_token) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR) + + def _parse_kexgss_error(self, m): + """ + Parse the SSH2_MSG_KEXGSS_ERROR message (client mode). + The server may send a GSS-API error message. if it does, we display + the error by throwing an exception (client mode). + + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_ERROR message + :raise SSHException: Contains GSS-API major and minor status as well as + the error message and the language tag of the + message + """ + maj_status = m.get_int() + min_status = m.get_int() + err_msg = m.get_string() + lang_tag = m.get_string() # we don't care about the language! + raise SSHException("GSS-API Error:\nMajor Status: %s\nMinor Status: %s\ + \nError Message: %s\n") % (str(maj_status), + str(min_status), + err_msg) + + +class KexGSSGroup14(KexGSSGroup1): + """ + GSS-API / SSPI Authenticated Diffie-Hellman Group14 Key Exchange + as defined in `RFC 4462 Section 2 <http://www.ietf.org/rfc/rfc4462.txt>`_ + + :note: RFC 4462 says we are not required to implement GSS-API error + messages. + If an error occurs an exception will be thrown and the connection + will be terminated. + :see: `RFC 4462 Section 2.2 <http://www.ietf.org/rfc/rfc4462.txt>`_ + """ + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF + G = 2 + NAME = "gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==" + + +class KexGSSGex(object): + """ + GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange + as defined in `RFC 4462 Section 2 <http://www.ietf.org/rfc/rfc4462.txt>`_ + + :note: RFC 4462 says we are not required to implement GSS-API error + messages. + If an error occurs an exception will be thrown and the connection + will be terminated. + :see: `RFC 4462 Section 2.2 <http://www.ietf.org/rfc/rfc4462.txt>`_ + """ + NAME = "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==" + min_bits = 1024 + max_bits = 8192 + preferred_bits = 2048 + + def __init__(self, transport): + self.transport = transport + self.kexgss = self.transport.kexgss_ctxt + self.gss_host = None + self.p = None + self.q = None + self.g = None + self.x = None + self.e = None + self.f = None + self.old_style = False + + def start_kex(self): + """ + 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 + # request a bit range: we accept (min_bits) to (max_bits), but prefer + # (preferred_bits). according to the spec, we shouldn't pull the + # minimum up above 1024. + self.gss_host = self.transport.gss_host + m = Message() + m.add_byte(c_MSG_KEXGSS_GROUPREQ) + m.add_int(self.min_bits) + m.add_int(self.preferred_bits) + m.add_int(self.max_bits) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_GROUP) + + def parse_next(self, ptype, m): + """ + Parse the next packet. + + :param char ptype: The type of the incomming packet + :param `.Message` m: The paket content + """ + if ptype == MSG_KEXGSS_GROUPREQ: + return self._parse_kexgss_groupreq(m) + elif ptype == MSG_KEXGSS_GROUP: + return self._parse_kexgss_group(m) + elif ptype == MSG_KEXGSS_INIT: + return self._parse_kexgss_gex_init(m) + elif ptype == MSG_KEXGSS_HOSTKEY: + return self._parse_kexgss_hostkey(m) + elif ptype == MSG_KEXGSS_CONTINUE: + return self._parse_kexgss_continue(m) + elif ptype == MSG_KEXGSS_COMPLETE: + return self._parse_kexgss_complete(m) + elif ptype == MSG_KEXGSS_ERROR: + return self._parse_kexgss_error(m) + raise SSHException('KexGex asked to handle packet type %d' % ptype) + + # ## internals... + + def _generate_x(self): + # generate an "x" (1 < x < (p-1)/2). + q = (self.p - 1) // 2 + qnorm = util.deflate_long(q, 0) + qhbyte = byte_ord(qnorm[0]) + byte_count = len(qnorm) + qmask = 0xff + while not (qhbyte & 0x80): + qhbyte <<= 1 + qmask >>= 1 + while True: + x_bytes = self.transport.rng.read(byte_count) + x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:] + x = util.inflate_long(x_bytes, 1) + if (x > 1) and (x < q): + break + self.x = x + + def _parse_kexgss_groupreq(self, m): + """ + Parse the SSH2_MSG_KEXGSS_GROUPREQ message (server mode). + + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_GROUPREQ message + """ + minbits = m.get_int() + preferredbits = m.get_int() + maxbits = m.get_int() + # smoosh the user's preferred size into our own limits + if preferredbits > self.max_bits: + preferredbits = self.max_bits + if preferredbits < self.min_bits: + preferredbits = self.min_bits + # fix min/max if they're inconsistent. technically, we could just pout + # and hang up, but there's no harm in giving them the benefit of the + # doubt and just picking a bitsize for them. + if minbits > preferredbits: + minbits = preferredbits + if maxbits < preferredbits: + maxbits = preferredbits + # now save a copy + self.min_bits = minbits + self.preferred_bits = preferredbits + self.max_bits = maxbits + # generate prime + pack = self.transport._get_modulus_pack() + if pack is None: + raise SSHException('Can\'t do server-side gex with no modulus pack') + self.transport._log(DEBUG, 'Picking p (%d <= %d <= %d bits)' % (minbits, preferredbits, maxbits)) + self.g, self.p = pack.get_modulus(minbits, preferredbits, maxbits) + m = Message() + m.add_byte(c_MSG_KEXGSS_GROUP) + m.add_mpint(self.p) + m.add_mpint(self.g) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_INIT) + + def _parse_kexgss_group(self, m): + """ + Parse the SSH2_MSG_KEXGSS_GROUP message (client mode). + + :param `Message` m: The content of the SSH2_MSG_KEXGSS_GROUP message + """ + self.p = m.get_mpint() + self.g = m.get_mpint() + # reject if p's bit length < 1024 or > 8192 + bitlen = util.bit_length(self.p) + if (bitlen < 1024) or (bitlen > 8192): + raise SSHException('Server-generated gex p (don\'t ask) is out of range (%d bits)' % bitlen) + self.transport._log(DEBUG, 'Got server p (%d bits)' % bitlen) + self._generate_x() + # now compute e = g^x mod p + self.e = pow(self.g, self.x, self.p) + m = Message() + m.add_byte(c_MSG_KEXGSS_INIT) + m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host)) + m.add_mpint(self.e) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_HOSTKEY, + MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR) + + def _parse_kexgss_gex_init(self, m): + """ + Parse the SSH2_MSG_KEXGSS_INIT message (server mode). + + :param `Message` m: The content of the SSH2_MSG_KEXGSS_INIT message + """ + client_token = m.get_string() + self.e = m.get_mpint() + if (self.e < 1) or (self.e > self.p - 1): + raise SSHException('Client kex "e" is out of range') + self._generate_x() + self.f = pow(self.g, self.x, self.p) + K = pow(self.e, self.x, self.p) + self.transport.host_key = NullHostKey() + key = self.transport.host_key.__str__() + # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K) + hm = Message() + hm.add(self.transport.remote_version, self.transport.local_version, + self.transport.remote_kex_init, self.transport.local_kex_init, + key) + hm.add_int(self.min_bits) + hm.add_int(self.preferred_bits) + hm.add_int(self.max_bits) + hm.add_mpint(self.p) + hm.add_mpint(self.g) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + H = SHA.new(hm.asbytes()).digest() + self.transport._set_K_H(K, H) + srv_token = self.kexgss.ssh_accept_sec_context(self.gss_host, + client_token) + m = Message() + if self.kexgss._gss_srv_ctxt_status: + mic_token = self.kexgss.ssh_get_mic(self.transport.session_id, + gss_kex=True) + m.add_byte(c_MSG_KEXGSS_COMPLETE) + m.add_mpint(self.f) + m.add_string(mic_token) + if srv_token is not None: + m.add_boolean(True) + m.add_string(srv_token) + else: + m.add_boolean(False) + self.transport._send_message(m) + self.transport._activate_outbound() + else: + m.add_byte(c_MSG_KEXGSS_CONTINUE) + m.add_string(srv_token) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR) + + def _parse_kexgss_hostkey(self, m): + """ + Parse the SSH2_MSG_KEXGSS_HOSTKEY message (client mode). + + :param `Message` m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message + """ + # client mode + host_key = m.get_string() + self.transport.host_key = host_key + sig = m.get_string() + self.transport._verify_key(host_key, sig) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE) + + def _parse_kexgss_continue(self, m): + """ + Parse the SSH2_MSG_KEXGSS_CONTINUE message. + + :param `Message` m: The content of the SSH2_MSG_KEXGSS_CONTINUE message + """ + if not self.transport.server_mode: + srv_token = m.get_string() + m = Message() + m.add_byte(c_MSG_KEXGSS_CONTINUE) + m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host, + recv_token=srv_token)) + self.transport.send_message(m) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR) + else: + pass + + def _parse_kexgss_complete(self, m): + """ + Parse the SSH2_MSG_KEXGSS_COMPLETE message (client mode). + + :param `Message` m: The content of the SSH2_MSG_KEXGSS_COMPLETE message + """ + if self.transport.host_key is None: + self.transport.host_key = NullHostKey() + self.f = m.get_mpint() + mic_token = m.get_string() + """ + This must be TRUE, if there is a GSS-API token in this + message. + """ + bool = m.get_boolean() + srv_token = None + if bool: + srv_token = m.get_string() + if (self.f < 1) or (self.f > self.p - 1): + raise SSHException('Server kex "f" is out of range') + K = pow(self.f, self.x, self.p) + # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K) + hm = Message() + hm.add(self.transport.local_version, self.transport.remote_version, + self.transport.local_kex_init, self.transport.remote_kex_init, + self.transport.host_key.__str__()) + if not self.old_style: + hm.add_int(self.min_bits) + hm.add_int(self.preferred_bits) + if not self.old_style: + hm.add_int(self.max_bits) + hm.add_mpint(self.p) + hm.add_mpint(self.g) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + H = SHA.new(hm.asbytes()).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) + else: + self.kexgss.ssh_check_mic(mic_token, + self.transport.session_id) + self.transport._activate_outbound() + + def _parse_kexgss_error(self, m): + """ + Parse the SSH2_MSG_KEXGSS_ERROR message (client mode). + The server may send a GSS-API error message. if it does, we display + the error by throwing an exception (client mode). + + :param `Message` m: The content of the SSH2_MSG_KEXGSS_ERROR message + :raise SSHException: Contains GSS-API major and minor status as well as + the error message and the language tag of the + message + """ + maj_status = m.get_int() + min_status = m.get_int() + err_msg = m.get_string() + lang_tag = m.get_string() # we don't care about the language! + raise SSHException("GSS-API Error:\nMajor Status: %s\nMinor Status: %s\ + \nError Message: %s\n") % (str(maj_status), + str(min_status), + err_msg) + + +class NullHostKey(object): + """ + This class represents the Null Host Key for GSS-API Key Exchange + as defined in `RFC 4462 Section 5 <http://www.ietf.org/rfc/rfc4462.txt>`_ + """ + def __init__(self): + self.key = "" + + def __str__(self): + return self.key + + def get_name(self): + return self.key diff --git a/paramiko/message.py b/paramiko/message.py index da6acf8e..b893e76d 100644 --- a/paramiko/message.py +++ b/paramiko/message.py @@ -148,6 +148,19 @@ class Message (object): @return: a 32-bit unsigned integer. @rtype: int """ + byte = self.get_bytes(1) + if byte == max_byte: + return util.inflate_long(self.get_binary()) + byte += self.get_bytes(3) + return struct.unpack('>I', byte)[0] + + def get_size(self): + """ + Fetch an int from the stream. + + @return: a 32-bit unsigned integer. + @rtype: int + """ return struct.unpack('>I', self.get_bytes(4))[0] def get_int64(self): @@ -257,6 +270,20 @@ class Message (object): self.packet.write(struct.pack('>I', n)) return self + def add_int(self, n): + """ + Add an integer to the stream. + + @param n: integer to add + @type n: int + """ + if n >= Message.big_int: + self.packet.write(max_byte) + self.add_string(util.deflate_long(n)) + else: + self.packet.write(struct.pack('>I', n)) + return self + def add_int64(self, n): """ Add a 64-bit int to the stream. diff --git a/paramiko/pipe.py b/paramiko/pipe.py index b0cfcf24..4f62d7c5 100644 --- a/paramiko/pipe.py +++ b/paramiko/pipe.py @@ -28,6 +28,7 @@ will trigger as readable in `select <select.select>`. import sys import os import socket +from paramiko.py3compat import b def make_pipe(): diff --git a/paramiko/primes.py b/paramiko/primes.py index 8e02e80c..7415c182 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -25,6 +25,7 @@ import os from paramiko import util from paramiko.py3compat import byte_mask, long from paramiko.ssh_exception import SSHException +from paramiko.common import * def _roll_random(n): diff --git a/paramiko/server.py b/paramiko/server.py index 496cd60c..cf396b15 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -229,6 +229,80 @@ class ServerInterface (object): :rtype: int or `.InteractiveQuery` """ return AUTH_FAILED + + def check_auth_gssapi_with_mic(self, username, + gss_authenticated=AUTH_FAILED, + cc_file=None): + """ + Authenticate the given user to the server if he is a valid krb5 + principal. + + :param str username: The username of the authenticating client + :param int gss_authenticated: The result of the krb5 authentication + :param str cc_filename: The krb5 client credentials cache filename + :return: `.AUTH_FAILED` if the user is not authenticated otherwise + `.AUTH_SUCCESSFUL` + :rtype: int + :note: Kerberos credential delegation is not supported. + :see: `.ssh_gss` + :note: : We are just checking in L{AuthHandler} that the given user is + a valid krb5 principal! + We don't check if the krb5 principal is allowed to log in on + the server, because there is no way to do that in python. So + if you develop your own SSH server with paramiko for a cetain + plattform like Linux, you should call C{krb5_kuserok()} in your + local kerberos library to make sure that the krb5_principal has + an account on the server and is allowed to log in as a user. + :see: `http://www.unix.com/man-page/all/3/krb5_kuserok/` + """ + if gss_authenticated == AUTH_SUCCESSFUL: + return AUTH_SUCCESSFUL + return AUTH_FAILED + + def check_auth_gssapi_keyex(self, username, + gss_authenticated=AUTH_FAILED, + cc_file=None): + """ + Authenticate the given user to the server if he is a valid krb5 + principal and GSS-API Key Exchange was performed. + If GSS-API Key Exchange was not performed, this authentication method + won't be available. + + :param str username: The username of the authenticating client + :param int gss_authenticated: The result of the krb5 authentication + :param str cc_filename: The krb5 client credentials cache filename + :return: `.AUTH_FAILED` if the user is not authenticated otherwise + `.AUTH_SUCCESSFUL` + :rtype: int + :note: Kerberos credential delegation is not supported. + :see: `.ssh_gss` `.kex_gss` + :note: : We are just checking in L{AuthHandler} that the given user is + a valid krb5 principal! + We don't check if the krb5 principal is allowed to log in on + the server, because there is no way to do that in python. So + if you develop your own SSH server with paramiko for a cetain + plattform like Linux, you should call C{krb5_kuserok()} in your + local kerberos library to make sure that the krb5_principal has + an account on the server and is allowed to log in as a user. + :see: `http://www.unix.com/man-page/all/3/krb5_kuserok/` + """ + if gss_authenticated == AUTH_SUCCESSFUL: + return AUTH_SUCCESSFUL + return AUTH_FAILED + + def enable_auth_gssapi(self): + """ + Overwrite this function in your SSH server to enable GSSAPI + authentication. + The default implementation always returns false. + + :return: True if GSSAPI authentication is enabled otherwise false + :rtype: Boolean + :see: : `.ssh_gss` + """ + UseGSSAPI = False + GSSAPICleanupCredentials = False + return UseGSSAPI def check_port_forward_request(self, address, port): """ diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py new file mode 100644 index 00000000..58a64a56 --- /dev/null +++ b/paramiko/ssh_gss.py @@ -0,0 +1,598 @@ +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss <sebastian.deiss@t-online.de> +# +# +# 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. + +""" +This module provides GSS-API / SSPI authentication for Paramiko as defined in +RFC 4462 with the following restrictions: +Credential delegation is not supported in server mode, +GSS-API key exchange is supported, but not implemented in Paramiko. +To Use this module, you need the following additional python packages: +`pyasn1 >= 0.1.7 <https://pypi.python.org/pypi/pyasn1>`_, +`python-gssapi >= 0.4.0 (Unix) <https://pypi.python.org/pypi/python-gssapi>`_, +`pywin32 2.1.8 (Windows) <http://sourceforge.net/projects/pywin32/>`_. + +:summary: SSH2 GSS-API / SSPI authentication module +:version: 0.1 +:author: Sebastian Deiss +:contact: https://github.com/SebastianDeiss/paramiko/issues +:organization: science + computing ag + `EMail <mailto:a.kruis@science-computing.de>`_ +:copyright: (C) 2013-2014 `science + computing ag + <https://www.science-computing.de>`_ +:license: GNU Lesser General Public License (LGPL) +:see: `.kex_gss` + +Created on 07.11.2013 +""" + +import struct +import os +import sys + +''' +:var bool GSS_AUTH_AVAILABLE: Constraint that indicates if GSS-API / SSPI is + Available. +''' +GSS_AUTH_AVAILABLE = True + +try: + from pyasn1.type.univ import ObjectIdentifier + from pyasn1.codec.der import encoder, decoder +except ImportError: + GSS_AUTH_AVAILABLE = False + class ObjectIdentifier(object): + def __init__(self, *args): + raise NotImplementedError("Module pyasn1 not importable") + + class decoder(object): + def decode(self): + raise NotImplementedError("Module pyasn1 not importable") + + class encoder(object): + def encode(self): + raise NotImplementedError("Module pyasn1 not importable") + +from paramiko.common import MSG_USERAUTH_REQUEST +from paramiko.ssh_exception import SSHException + +""" +:var str _API: Constraint for the used API +""" +_API = "MIT" + +try: + import gssapi +except (ImportError, OSError): + try: + import sspicon + import sspi + _API = "SSPI" + except ImportError: + GSS_AUTH_AVAILABLE = False + _API = None + + +def GSSAuth(auth_method, gss_deleg_creds=True): + """ + Provide SSH2 GSS-API / SSPI authentication for Paramiko. + + :param str auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + :param bool gss_deleg_creds: Delegate client credentials or not. + We delegate credentials by default. + :return: Either an `._SSH_GSSAPI` (Unix) object or an + `_SSH_SSPI` (Windows) object + :rtype: Object + + :raise ImportError: If no GSS-API / SSPI module could be imported. + + :see: `RFC 4462 <http://www.ietf.org/rfc/rfc4462.txt>`_ + :note: Check for the available API and return either an `._SSH_GSSAPI` + (MIT GSSAPI) object or an `._SSH_SSPI` (MS SSPI) object. If you + get python-gssapi working on Windows, python-gssapi + will be used and a `._SSH_GSSAPI` object will be returned. + If there is no supported API available, + ``None`` will be returned. + """ + if _API == "MIT": + return _SSH_GSSAPI(auth_method, gss_deleg_creds) + elif _API == "SSPI" and os.name == "nt": + return _SSH_SSPI(auth_method, gss_deleg_creds) + else: + raise ImportError("Unable to import a GSS-API / SSPI module!") + + +class _SSH_GSSAuth(object): + """ + Contains the shared variables and methods of `._SSH_GSSAPI` and + `._SSH_SSPI`. + """ + def __init__(self, auth_method, gss_deleg_creds): + """ + :param str auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + :param bool gss_deleg_creds: Delegate client credentials or not + """ + self._auth_method = auth_method + self._gss_deleg_creds = gss_deleg_creds + self._gss_host = None + self._username = None + self._session_id = None + self._service = "ssh-connection" + """ + OpenSSH supports Kerberos V5 mechanism only for GSS-API authentication, + so we also support the krb5 mechanism only. + """ + self._krb5_mech = "1.2.840.113554.1.2.2" + + # client mode + self._gss_ctxt = None + self._gss_ctxt_status = False + + # server mode + self._gss_srv_ctxt = None + self._gss_srv_ctxt_status = False + self.cc_file = None + + def set_service(self, service): + """ + This is just a setter to use a non default service. + I added this method, because RFC 4462 doesn't specify "ssh-connection" + as the only service value. + + :param str service: The desired SSH service + :rtype: Void + """ + if service.find("ssh-"): + self._service = service + + def set_username(self, username): + """ + Setter for C{username}. If GSS-API Key Exchange is performed, the + username is not set by C{ssh_init_sec_context}. + + :param str username: The name of the user who attempts to login + :rtype: Void + """ + self._username = username + + def ssh_gss_oids(self, mode="client"): + """ + This method returns a single OID, because we only support the + Kerberos V5 mechanism. + + :param str mode: Client for client mode and server for server mode + :return: A byte sequence containing the number of supported + OIDs, the length of the OID and the actual OID encoded with + DER + :rtype: Bytes + :note: In server mode we just return the OID length and the DER encoded + OID. + """ + OIDs = self._make_uint32(1) + krb5_OID = encoder.encode(ObjectIdentifier(self._krb5_mech)) + OID_len = self._make_uint32(len(krb5_OID)) + if mode == "server": + return OID_len + krb5_OID + return OIDs + OID_len + krb5_OID + + def ssh_check_mech(self, desired_mech): + """ + Check if the given OID is the Kerberos V5 OID (server mode). + + :param str desired_mech: The desired GSS-API mechanism of the client + :return: ``True`` if the given OID is supported, otherwise C{False} + :rtype: Boolean + """ + mech, __ = decoder.decode(desired_mech) + if mech.__str__() != self._krb5_mech: + return False + return True + + # Internals + #-------------------------------------------------------------------------- + def _make_uint32(self, integer): + """ + Create a 32 bit unsigned integer (The byte sequence of an integer). + + :param int integer: The integer value to convert + :return: The byte sequence of an 32 bit integer + :rtype: Bytes + """ + return struct.pack("!I", integer) + + def _ssh_build_mic(self, session_id, username, service, auth_method): + """ + Create the SSH2 MIC filed for gssapi-with-mic. + + :param str session_id: The SSH session ID + :param str username: The name of the user who attempts to login + :param str service: The requested SSH service + :param str auth_method: The requested SSH authentication mechanism + :return: The MIC as defined in RFC 4462. The contents of the + MIC field are: + string session_identifier, + byte SSH_MSG_USERAUTH_REQUEST, + string user-name, + string service (ssh-connection), + string authentication-method + (gssapi-with-mic or gssapi-keyex) + :rtype: Bytes + """ + mic = self._make_uint32(len(session_id)) + mic += session_id + mic += struct.pack('B', MSG_USERAUTH_REQUEST) + mic += self._make_uint32(len(username)) + mic += username.encode() + mic += self._make_uint32(len(service)) + mic += service.encode() + mic += self._make_uint32(len(auth_method)) + mic += auth_method.encode() + return mic + + +class _SSH_GSSAPI(_SSH_GSSAuth): + """ + Implementation of the GSS-API MIT Kerberos Authentication for SSH2. + + :see: `.GSSAuth` + """ + def __init__(self, auth_method, gss_deleg_creds): + """ + :param str auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + :param bool gss_deleg_creds: Delegate client credentials or not + """ + _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds) + + if self._gss_deleg_creds: + self._gss_flags = (gssapi.C_PROT_READY_FLAG, + gssapi.C_INTEG_FLAG, + gssapi.C_MUTUAL_FLAG, + gssapi.C_DELEG_FLAG) + else: + self._gss_flags = (gssapi.C_PROT_READY_FLAG, + gssapi.C_INTEG_FLAG, + gssapi.C_MUTUAL_FLAG) + + def ssh_init_sec_context(self, target, desired_mech=None, + username=None, recv_token=None): + """ + Initialize a GSS-API context. + + :param str username: The name of the user who attempts to login + :param str target: The hostname of the target to connect to + :param str desired_mech: The negotiated GSS-API mechanism + ("pseudo negotiated" mechanism, because we + support just the krb5 mechanism :-)) + :param str recv_token: The GSS-API token received from the Server + :raise SSHException: Is raised if the desired mechanism of the client + is not supported + :return: A ``String`` if the GSS-API has returned a token or ``None`` if + no token was returned + :rtype: String or None + """ + self._username = username + self._gss_host = target + targ_name = gssapi.Name("host@" + self._gss_host, + gssapi.C_NT_HOSTBASED_SERVICE) + ctx = gssapi.Context() + ctx.flags = self._gss_flags + if desired_mech is None: + krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech) + else: + mech, __ = decoder.decode(desired_mech) + if mech.__str__() != self._krb5_mech: + raise SSHException("Unsupported mechanism OID.") + else: + krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech) + token = None + try: + if recv_token is None: + self._gss_ctxt = gssapi.InitContext(peer_name=targ_name, + mech_type=krb5_mech, + req_flags=ctx.flags) + token = self._gss_ctxt.step(token) + else: + token = self._gss_ctxt.step(recv_token) + except gssapi.GSSException: + raise gssapi.GSSException("{0} Target: {1}".format(sys.exc_info()[1], + self._gss_host)) + self._gss_ctxt_status = self._gss_ctxt.established + return token + + def ssh_get_mic(self, session_id, gss_kex=False): + """ + Create the MIC token for a SSH2 message. + + :param str session_id: The SSH session ID + :param bool gss_kex: Generate the MIC for GSS-API Key Exchange or not + :return: gssapi-with-mic: + Returns the MIC token from GSS-API for the message we created + with ``_ssh_build_mic``. + gssapi-keyex: + Returns the MIC token from GSS-API with the SSH session ID as + message. + :rtype: String + :see: `._ssh_build_mic` + """ + self._session_id = session_id + if not gss_kex: + mic_field = self._ssh_build_mic(self._session_id, + self._username, + self._service, + self._auth_method) + mic_token = self._gss_ctxt.get_mic(mic_field) + else: + # for key exchange with gssapi-keyex + mic_token = self._gss_srv_ctxt.get_mic(self._session_id) + return mic_token + + def ssh_accept_sec_context(self, hostname, recv_token, username=None): + """ + Accept a GSS-API context (server mode). + + :param str hostname: The servers hostname + :param str username: The name of the user who attempts to login + :param str recv_token: The GSS-API Token received from the server, + if it's not the initial call. + :return: A ``String`` if the GSS-API has returned a token or ``None`` + if no token was returned + :rtype: String or None + """ + # hostname and username are not required for GSSAPI, but for SSPI + self._gss_host = hostname + self._username = username + if self._gss_srv_ctxt is None: + self._gss_srv_ctxt = gssapi.AcceptContext() + token = self._gss_srv_ctxt.step(recv_token) + self._gss_srv_ctxt_status = self._gss_srv_ctxt.established + return token + + def ssh_check_mic(self, mic_token, session_id, username=None): + """ + Verify the MIC token for a SSH2 message. + + :param str mic_token: The MIC token received from the client + :param str session_id: The SSH session ID + :param str username: The name of the user who attempts to login + :return: 0 if the MIC check was successful and 1 if it fails + :rtype: int + """ + self._session_id = session_id + self._username = username + if self._username is not None: + # server mode + mic_field = self._ssh_build_mic(self._session_id, + self._username, + self._service, + self._auth_method) + mic_status = self._gss_srv_ctxt.verify_mic(mic_field, + mic_token) + else: + # for key exchange with gssapi-keyex + # client mode + mic_status = self._gss_ctxt.verify_mic(self._session_id, + mic_token) + return mic_status + + @property + def credentials_delegated(self): + """ + Checks if credentials are delegated (server mode). + + :return: ``True`` if credentials are delegated, otherwise ``False`` + :rtype: bool + """ + if self._gss_srv_ctxt.delegated_cred is not None: + return True + return False + + def save_client_creds(self, client_token): + """ + Save the Client token in a file. This is used by the SSH server + to store the client credentials if credentials are delegated + (server mode). + + :param str client_token: The GSS-API token received form the client + :raise NotImplementedError: Credential delegation is currently not + supported in server mode + """ + raise NotImplementedError + + +class _SSH_SSPI(_SSH_GSSAuth): + """ + Implementation of the Microsoft SSPI Kerberos Authentication for SSH2. + + :see: `.GSSAuth` + """ + def __init__(self, auth_method, gss_deleg_creds): + """ + :param str auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + :param bool gss_deleg_creds: Delegate client credentials or not + """ + _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds) + + if self._gss_deleg_creds: + self._gss_flags = sspicon.ISC_REQ_INTEGRITY |\ + sspicon.ISC_REQ_MUTUAL_AUTH |\ + sspicon.ISC_REQ_DELEGATE + else: + self._gss_flags = sspicon.ISC_REQ_INTEGRITY |\ + sspicon.ISC_REQ_MUTUAL_AUTH + + def ssh_init_sec_context(self, target, desired_mech=None, + username=None, recv_token=None): + """ + Initialize a SSPI context. + + :param str username: The name of the user who attempts to login + :param str target: The FQDN of the target to connect to + :param str desired_mech: The negotiated SSPI mechanism + ("pseudo negotiated" mechanism, because we + support just the krb5 mechanism :-)) + :param recv_token: The SSPI token received from the Server + :raise SSHException: Is raised if the desired mechanism of the client + is not supported + :return: A ``String`` if the SSPI has returned a token or ``None`` if + no token was returned + :rtype: String or None + """ + self._username = username + self._gss_host = target + error = 0 + targ_name = "host/" + self._gss_host + if desired_mech is not None: + mech, __ = decoder.decode(desired_mech) + if mech.__str__() != self._krb5_mech: + raise SSHException("Unsupported mechanism OID.") + try: + if recv_token is None: + self._gss_ctxt = sspi.ClientAuth("Kerberos", + scflags=self._gss_flags, + 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)) + if error == 0: + """ + if the status is GSS_COMPLETE (error = 0) the context is fully + established an we can set _gss_ctxt_status to True. + """ + self._gss_ctxt_status = True + token = None + """ + You won't get another token if the context is fully established, + so i set token to None instead of "" + """ + return token + + def ssh_get_mic(self, session_id, gss_kex=False): + """ + Create the MIC token for a SSH2 message. + + :param str session_id: The SSH session ID + :param bool gss_kex: Generate the MIC for Key Exchange with SSPI or not + :return: gssapi-with-mic: + Returns the MIC token from SSPI for the message we created + with ``_ssh_build_mic``. + gssapi-keyex: + Returns the MIC token from SSPI with the SSH session ID as + message. + :rtype: String + :see: `._ssh_build_mic` + """ + self._session_id = session_id + if not gss_kex: + mic_field = self._ssh_build_mic(self._session_id, + self._username, + self._service, + self._auth_method) + mic_token = self._gss_ctxt.sign(mic_field) + else: + # for key exchange with gssapi-keyex + mic_token = self._gss_srv_ctxt.sign(self._session_id) + return mic_token + + def ssh_accept_sec_context(self, hostname, username, recv_token): + """ + Accept a SSPI context (server mode). + + :param str hostname: The servers FQDN + :param str username: The name of the user who attempts to login + :param str recv_token: The SSPI Token received from the server, + if it's not the initial call. + :return: A ``String`` if the SSPI has returned a token or ``None`` if + no token was returned + :rtype: String or None + """ + self._gss_host = hostname + self._username = username + targ_name = "host/" + self._gss_host + self._gss_srv_ctxt = sspi.ServerAuth("Kerberos", spn=targ_name) + error, token = self._gss_srv_ctxt.authorize(recv_token) + token = token[0].Buffer + if error == 0: + self._gss_srv_ctxt_status = True + token = None + return token + + def ssh_check_mic(self, mic_token, session_id, username=None): + """ + Verify the MIC token for a SSH2 message. + + :param str mic_token: The MIC token received from the client + :param str session_id: The SSH session ID + :param str username: The name of the user who attempts to login + :return: 0 if the MIC check was successful + :rtype: int + """ + self._session_id = session_id + self._username = username + mic_status = 1 + if username is not None: + # server mode + mic_field = self._ssh_build_mic(self._session_id, + self._username, + self._service, + self._auth_method) + mic_status = self._gss_srv_ctxt.verify(mic_field, + mic_token) + else: + # for key exchange with gssapi-keyex + # client mode + mic_status = self._gss_ctxt.verify(self._session_id, + mic_token) + """ + The SSPI method C{verify} has no return value, so if no SSPI error + is returned, set C{mic_status} to 0. + """ + mic_status = 0 + return mic_status + + @property + def credentials_delegated(self): + """ + Checks if credentials are delegated (server mode). + + :return: ``True`` if credentials are delegated, otherwise ``False`` + :rtype: Boolean + """ + return ( + self._gss_flags & sspicon.ISC_REQ_DELEGATE + ) and ( + self._gss_srv_ctxt_status or (self._gss_flags) + ) + + def save_client_creds(self, client_token): + """ + Save the Client token in a file. This is used by the SSH server + to store the client credentails if credentials are delegated + (server mode). + + :param str client_token: The SSPI token received form the client + :raise NotImplementedError: Credential delegation is currently not + supported in server mode + """ + raise NotImplementedError diff --git a/paramiko/transport.py b/paramiko/transport.py index 406626a7..86c9130c 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -31,6 +31,7 @@ from hashlib import md5, sha1 import paramiko from paramiko import util from paramiko.auth_handler import AuthHandler +from paramiko.ssh_gss import GSSAuth from paramiko.channel import Channel from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ cMSG_GLOBAL_REQUEST, DEBUG, MSG_KEXINIT, MSG_IGNORE, MSG_DISCONNECT, \ @@ -47,6 +48,8 @@ from paramiko.compress import ZlibCompressor, ZlibDecompressor from paramiko.dsskey import DSSKey from paramiko.kex_gex import KexGex from paramiko.kex_group1 import KexGroup1 +from paramiko.kex_group14 import KexGroup14 +from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14, NullHostKey from paramiko.message import Message from paramiko.packet import Packetizer, NeedRekeyException from paramiko.primes import ModulusPack @@ -92,7 +95,7 @@ class Transport (threading.Thread): 'aes256-cbc', '3des-cbc', 'arcfour128', 'arcfour256') _preferred_macs = ('hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96') _preferred_keys = ('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256') - _preferred_kex = ('diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1') + _preferred_kex = ( 'diffie-hellman-group1-sha1', 'diffie-hellman-group14-sha1', 'diffie-hellman-group-exchange-sha1' ) _preferred_compression = ('none',) _cipher_info = { @@ -121,7 +124,11 @@ class Transport (threading.Thread): _kex_info = { 'diffie-hellman-group1-sha1': KexGroup1, + 'diffie-hellman-group14-sha1': KexGroup14, 'diffie-hellman-group-exchange-sha1': KexGex, + 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup1, + 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup14, + 'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGex } _compression_info = { @@ -135,7 +142,7 @@ class Transport (threading.Thread): _modulus_pack = None - def __init__(self, sock): + def __init__(self, sock, gss_kex=False, gss_deleg_creds=True): """ Create a new SSH session over an existing socket, or socket-like object. This only creates the `.Transport` object; it doesn't begin the @@ -214,6 +221,21 @@ class Transport (threading.Thread): self.host_key_type = None self.host_key = None + # GSS-API / SSPI Key Exchange + self.use_gss_kex = gss_kex + # This will be set to True if GSS-API Key Exchange was performed + self.gss_kex_used = False + self.kexgss_ctxt = None + 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-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==', + 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==', + 'diffie-hellman-group-exchange-sha1', + 'diffie-hellman-group1-sha1', + 'diffie-hellman-group14-sha1') + # state used during negotiation self.kex_engine = None self.H = None @@ -287,6 +309,7 @@ class Transport (threading.Thread): .. versionadded:: 1.5.3 """ + self.sock.close() self.close() def get_security_options(self): @@ -298,6 +321,17 @@ class Transport (threading.Thread): """ return SecurityOptions(self) + def set_gss_host(self, gss_host): + """ + Setter for C{gss_host} if GSS-API Key Exchange is performed. + + :param str gss_host: The targets name in the kerberos database + Default: The name of the host to connect to + :rtype: Void + """ + # We need the FQDN to get this working with SSPI + self.gss_host = socket.getfqdn(gss_host) + def start_client(self, event=None): """ Negotiate a new SSH2 session as a client. This is the first step after @@ -835,7 +869,8 @@ class Transport (threading.Thread): self.lock.release() return chan - def connect(self, hostkey=None, username='', password=None, pkey=None): + def connect(self, hostkey=None, username='', password=None, pkey=None, + gss_host=None, gss_auth=False, gss_kex=False, gss_deleg_creds=True): """ Negotiate an SSH2 session, and optionally verify the server's host key and authenticate using a password or private key. This is a shortcut @@ -865,6 +900,10 @@ class Transport (threading.Thread): :param .PKey pkey: a private key to use for authentication, if you want to use private key authentication; otherwise ``None``. + :param str gss_host: The targets name in the kerberos database. default: hostname + :param bool gss_auth: ``True`` if you want to use GSS-API authentication + :param bool gss_kex: Perform GSS-API Key Exchange and user authentication + :param bool gss_deleg_creds: Delegate GSS-API client credentials or not :raises SSHException: if the SSH2 negotiation fails, the host key supplied by the server is incorrect, or authentication fails. @@ -875,7 +914,9 @@ class Transport (threading.Thread): self.start_client() # check host key if we were given one - if hostkey is not None: + # If GSS-API Key Exchange was performed, we are not required to check + # the host key. + if (hostkey is not None) and not gss_kex: key = self.get_remote_server_key() if (key.get_name() != hostkey.get_name()) or (key.asbytes() != hostkey.asbytes()): self._log(DEBUG, 'Bad host key from server') @@ -884,13 +925,19 @@ class Transport (threading.Thread): raise SSHException('Bad host key from server') self._log(DEBUG, 'Host key verified (%s)' % hostkey.get_name()) - if (pkey is not None) or (password is not None): - if password is not None: - self._log(DEBUG, 'Attempting password auth...') - self.auth_password(username, password) - else: + if (pkey is not None) or (password is not None) or gss_auth or gss_kex: + if gss_auth: + self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-with-mic)') + self.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds) + elif gss_kex: + self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-keyex)') + self.auth_gssapi_keyex(username) + elif pkey is not None: self._log(DEBUG, 'Attempting public-key auth...') self.auth_publickey(username, pkey) + else: + self._log(DEBUG, 'Attempting password auth...') + self.auth_password(username, password) return @@ -1172,6 +1219,55 @@ class Transport (threading.Thread): self.auth_handler.auth_interactive(username, handler, my_event, submethods) return self.auth_handler.wait_for_response(my_event) + def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds): + """ + Authenticate to the Server using GSS-API / SSPI. + + :param str username: The username to authenticate as + :param str gss_host: The target host + :param bool gss_deleg_creds: Delegate credentials or not + :return: list of auth types permissible for the next stage of + authentication (normally empty) + :rtype: list + :raise BadAuthenticationType: if gssapi-with-mic isn't + allowed by the server (and no event was passed in) + :raise AuthenticationException: if the authentication failed (and no + event was passed in) + :raise SSHException: if there was a network error + """ + if (not self.active) or (not self.initial_kex_done): + # we should never try to authenticate unless we're on a secure link + raise SSHException('No existing session') + my_event = threading.Event() + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds, my_event) + return self.auth_handler.wait_for_response(my_event) + + def auth_gssapi_keyex(self, username): + """ + Authenticate to the Server with GSS-API / SSPI if GSS-API Key Exchange + was the used key exchange method. + + :param str username: The username to authenticate as + :param str gss_host: The target host + :param bool gss_deleg_creds: Delegate credentials or not + :return: list of auth types permissible for the next stage of + authentication (normally empty) + :rtype: list + :raise BadAuthenticationType: if GSS-API Key Exchange was not performed + (and no event was passed in) + :raise AuthenticationException: if the authentication failed (and no + event was passed in) + :raise SSHException: if there was a network error + """ + if (not self.active) or (not self.initial_kex_done): + # we should never try to authenticate unless we're on a secure link + raise SSHException('No existing session') + my_event = threading.Event() + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_gssapi_keyex(username, my_event) + return self.auth_handler.wait_for_response(my_event) + def set_log_channel(self, name): """ Set the channel for this transport's logging. The default is @@ -1434,7 +1530,7 @@ class Transport (threading.Thread): if ptype not in self._expected_packet: raise SSHException('Expecting packet from %r, got %d' % (self._expected_packet, ptype)) self._expected_packet = tuple() - if (ptype >= 30) and (ptype <= 39): + if (ptype >= 30) and (ptype <= 41): self.kex_engine.parse_next(ptype, m) continue diff --git a/sites/docs/api/kex_gss.rst b/sites/docs/api/kex_gss.rst new file mode 100644 index 00000000..a662be01 --- /dev/null +++ b/sites/docs/api/kex_gss.rst @@ -0,0 +1,5 @@ +GSS-API Key Exchange Module +=========================== + +.. automodule:: paramiko.kex_gss + :member-order: bysource diff --git a/sites/docs/api/ssh_gss.rst b/sites/docs/api/ssh_gss.rst new file mode 100644 index 00000000..1b08c7f8 --- /dev/null +++ b/sites/docs/api/ssh_gss.rst @@ -0,0 +1,14 @@ +Paramiko GSS-API Interface +========================== + +.. automodule:: paramiko.ssh_gss + :member-order: bysource + +.. autoclass:: _SSH_GSSAuth + :member-order: bysource + +.. autoclass:: _SSH_GSSAPI + :member-order: bysource + +.. autoclass:: _SSH_SSPI + :member-order: bysource diff --git a/sites/docs/index.rst b/sites/docs/index.rst index f336b393..87265d95 100644 --- a/sites/docs/index.rst +++ b/sites/docs/index.rst @@ -50,6 +50,8 @@ Authentication & keys api/agent api/hostkeys api/keys + api/ssh_gss + api/kex_gss Other primary functions diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index c9b7bcaf..0680eb38 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :feature:`250` GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and user authentication. * :bug:`-` Added self.args for exception classes. Used for unpickling. Related to (`Fabric #986 <https://github.com/fabric/fabric/issues/986>`_, `Fabric #714 <https://github.com/fabric/fabric/issues/714>`_). Thanks to Alex @@ -44,6 +44,10 @@ from tests.test_packetizer import PacketizerTest from tests.test_auth import AuthTest from tests.test_transport import TransportTest from tests.test_client import SSHClientTest +from test_client import SSHClientTest +from test_gssapi import GSSAPITest +from test_ssh_gss import GSSAuthTest +from test_kex_gss import GSSKexTest default_host = 'localhost' default_user = os.environ.get('USER', 'nobody') @@ -85,6 +89,17 @@ def main(): help='skip SFTP client/server tests, which can be slow') parser.add_option('--no-big-file', action='store_false', dest='use_big_file', default=True, help='skip big file SFTP tests, which are slow as molasses') + parser.add_option('--gssapi-test', action='store_true', dest='gssapi_test', default=False, + help='Test the used APIs for GSS-API / SSPI authentication') + parser.add_option('--test-gssauth', action='store_true', dest='test_gssauth', default=False, + help='Test GSS-API / SSPI authentication for SSHv2. To test this, you need kerberos a infrastructure.\ + Note: Paramiko needs access to your krb5.keytab file. Make it readable for Paramiko or\ + copy the used key to another file and set the environment variable KRB5_KTNAME to this file.') + parser.add_option('--test-gssapi-keyex', action='store_true', dest='test_gsskex', default=False, + help='Test GSS-API / SSPI authenticated iffie-Hellman Key Exchange and user\ + authentication. To test this, you need kerberos a infrastructure.\ + Note: Paramiko needs access to your krb5.keytab file. Make it readable for Paramiko or\ + copy the used key to another file and set the environment variable KRB5_KTNAME to this file.') parser.add_option('-R', action='store_false', dest='use_loopback_sftp', default=True, help='perform SFTP tests against a remote server (by default, SFTP tests ' + 'are done through a loopback socket)') @@ -101,6 +116,16 @@ def main(): parser.add_option('-P', '--sftp-passwd', dest='password', type='string', default=default_passwd, metavar='<password>', help='[with -R] (optional) password to unlock the private key for remote sftp tests') + parser.add_option('--krb5_principal', dest='krb5_principal', type='string', + metavar='<krb5_principal>', + help='The krb5 principal (your username) for GSS-API / SSPI authentication') + parser.add_option('--targ_name', dest='targ_name', type='string', + metavar='<targ_name>', + help='Target name for GSS-API / SSPI authentication.\ + This is the hosts name you are running the test on in the kerberos database.') + parser.add_option('--server_mode', action='store_true', dest='server_mode', default=False, + help='Usage with --gssapi-test. Test the available GSS-API / SSPI server mode to.\ + Note: you need to have access to the kerberos keytab file.') options, args = parser.parse_args() @@ -136,6 +161,15 @@ def main(): suite.addTest(unittest.makeSuite(SFTPTest)) if options.use_big_file: suite.addTest(unittest.makeSuite(BigSFTPTest)) + if options.gssapi_test: + GSSAPITest.init(options.targ_name, options.server_mode) + suite.addTest(unittest.makeSuite(GSSAPITest)) + if options.test_gssauth: + GSSAuthTest.init(options.krb5_principal, options.targ_name) + suite.addTest(unittest.makeSuite(GSSAuthTest)) + if options.test_gsskex: + GSSKexTest.init(options.krb5_principal, options.targ_name) + suite.addTest(unittest.makeSuite(GSSKexTest)) verbosity = 1 if options.verbose: verbosity = 2 diff --git a/tests/stub_sftp.py b/tests/stub_sftp.py index 47644433..24380ba1 100644 --- a/tests/stub_sftp.py +++ b/tests/stub_sftp.py @@ -21,6 +21,7 @@ A stub SFTP server for loopback SFTP testing. """ import os +import sys from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes, \ SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED from paramiko.common import o666 diff --git a/tests/test_buffered_pipe.py b/tests/test_buffered_pipe.py index a53081a9..b66edff9 100644 --- a/tests/test_buffered_pipe.py +++ b/tests/test_buffered_pipe.py @@ -24,6 +24,7 @@ import threading import time from paramiko.buffered_pipe import BufferedPipe, PipeTimeout from paramiko import pipe +from paramiko.py3compat import b from tests.util import ParamikoTest diff --git a/tests/test_gssapi.py b/tests/test_gssapi.py new file mode 100644 index 00000000..e9ef99a9 --- /dev/null +++ b/tests/test_gssapi.py @@ -0,0 +1,167 @@ +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss <sebastian.deiss@t-online.de> +# +# +# 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. + +""" +Test the used APIs for GSS-API / SSPI authentication + +@author: Sebastian Deiss +@contact: U{https://github.com/SebastianDeiss/paramiko/issues} +@organization: science + computing ag + (U{EMail<mailto:a.kruis@science-computing.de>}) +@copyright: (C) 2013-2014 U{science + computing ag + <https://www.science-computing.de>} +@license: GNU Lesser General Public License (LGPL) + +Created on 04.12.2013 +""" + +import unittest +import socket + + +class GSSAPITest(unittest.TestCase): + + def init(hostname=None, srv_mode=False): + global krb5_mech, targ_name, server_mode + krb5_mech = "1.2.840.113554.1.2.2" + targ_name = hostname + server_mode = srv_mode + + init = staticmethod(init) + + def test_1_pyasn1(self): + """ + Test the used methods of pyasn1. + """ + from pyasn1.type.univ import ObjectIdentifier + from pyasn1.codec.der import encoder, decoder + oid = encoder.encode(ObjectIdentifier(krb5_mech)) + mech, __ = decoder.decode(oid) + self.assertEquals(krb5_mech, mech.__str__()) + + def test_2_gssapi_sspi(self): + """ + Test the used methods of python-gssapi or sspi, sspicon from pywin32. + """ + _API = "MIT" + try: + import gssapi + except ImportError: + import sspicon + import sspi + _API = "SSPI" + + c_token = None + gss_ctxt_status = False + mic_msg = b"G'day Mate!" + + if _API == "MIT": + if server_mode: + gss_flags = (gssapi.C_PROT_READY_FLAG, + gssapi.C_INTEG_FLAG, + gssapi.C_MUTUAL_FLAG, + gssapi.C_DELEG_FLAG) + else: + gss_flags = (gssapi.C_PROT_READY_FLAG, + gssapi.C_INTEG_FLAG, + gssapi.C_DELEG_FLAG) + """ + Initialize a GSS-API context. + """ + ctx = gssapi.Context() + ctx.flags = gss_flags + krb5_oid = gssapi.OID.mech_from_string(krb5_mech) + target_name = gssapi.Name("host@" + targ_name, + gssapi.C_NT_HOSTBASED_SERVICE) + gss_ctxt = gssapi.InitContext(peer_name=target_name, + mech_type=krb5_oid, + req_flags=ctx.flags) + if server_mode: + c_token = gss_ctxt.step(c_token) + gss_ctxt_status = gss_ctxt.established + self.assertEquals(False, gss_ctxt_status) + """ + Accept a GSS-API context. + """ + gss_srv_ctxt = gssapi.AcceptContext() + s_token = gss_srv_ctxt.step(c_token) + gss_ctxt_status = gss_srv_ctxt.established + self.assertNotEquals(None, s_token) + self.assertEquals(True, gss_ctxt_status) + """ + Establish the client context + """ + c_token = gss_ctxt.step(s_token) + self.assertEquals(None, c_token) + else: + while not gss_ctxt.established: + c_token = gss_ctxt.step(c_token) + self.assertNotEquals(None, c_token) + """ + Build MIC + """ + mic_token = gss_ctxt.get_mic(mic_msg) + + if server_mode: + """ + Check MIC + """ + status = gss_srv_ctxt.verify_mic(mic_msg, mic_token) + self.assertEquals(0, status) + else: + gss_flags = sspicon.ISC_REQ_INTEGRITY |\ + sspicon.ISC_REQ_MUTUAL_AUTH |\ + sspicon.ISC_REQ_DELEGATE + """ + Initialize a GSS-API context. + """ + target_name = "host/" + socket.getfqdn(targ_name) + gss_ctxt = sspi.ClientAuth("Kerberos", + scflags=gss_flags, + targetspn=target_name) + if server_mode: + error, token = gss_ctxt.authorize(c_token) + c_token = token[0].Buffer + self.assertEquals(0, error) + """ + Accept a GSS-API context. + """ + gss_srv_ctxt = sspi.ServerAuth("Kerberos", spn=target_name) + error, token = gss_srv_ctxt.authorize(c_token) + s_token = token[0].Buffer + """ + Establish the context. + """ + error, token = gss_ctxt.authorize(s_token) + c_token = token[0].Buffer + self.assertEquals(None, c_token) + self.assertEquals(0, error) + """ + Build MIC + """ + mic_token = gss_ctxt.sign(mic_msg) + """ + Check MIC + """ + gss_srv_ctxt.verify(mic_msg, mic_token) + else: + error, token = gss_ctxt.authorize(c_token) + c_token = token[0].Buffer + self.assertNotEquals(0, error) diff --git a/tests/test_kex_gss.py b/tests/test_kex_gss.py new file mode 100644 index 00000000..e160eb35 --- /dev/null +++ b/tests/test_kex_gss.py @@ -0,0 +1,143 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss <sebastian.deiss@t-online.de> +# +# +# 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. + +""" +Unit Tests for the GSS-API / SSPI SSHv2 Diffie-Hellman Key Exchange and user +authentication + +@author: Sebastian Deiss +@contact: U{https://github.com/SebastianDeiss/paramiko/issues} +@organization: science + computing ag + (U{EMail<mailto:a.kruis@science-computing.de>}) +@copyright: (C) 2003-2009 Robey Pointer, (C) 2013-2014 U{science + computing ag + <https://www.science-computing.de>} +@license: GNU Lesser General Public License (LGPL) + +Created on 08.01.2014 +""" + + +import socket +import threading +import unittest + +import paramiko + + +class NullServer (paramiko.ServerInterface): + + def get_allowed_auths(self, username): + return 'gssapi-keyex' + + def check_auth_gssapi_keyex(self, username, + gss_authenticated=paramiko.AUTH_FAILED, + cc_file=None): + if gss_authenticated == paramiko.AUTH_SUCCESSFUL: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def enable_auth_gssapi(self): + UseGSSAPI = True + return UseGSSAPI + + def check_channel_request(self, kind, chanid): + return paramiko.OPEN_SUCCEEDED + + def check_channel_exec_request(self, channel, command): + if command != 'yes': + return False + return True + + +class GSSKexTest(unittest.TestCase): + + def init(username, hostname): + global krb5_principal, targ_name + krb5_principal = username + targ_name = hostname + + init = staticmethod(init) + + def setUp(self): + self.username = krb5_principal + self.hostname = socket.getfqdn(targ_name) + self.sockl = socket.socket() + self.sockl.bind((targ_name, 0)) + self.sockl.listen(1) + self.addr, self.port = self.sockl.getsockname() + self.event = threading.Event() + thread = threading.Thread(target=self._run) + thread.start() + + def tearDown(self): + for attr in "tc ts socks sockl".split(): + if hasattr(self, attr): + getattr(self, attr).close() + + def _run(self): + self.socks, addr = self.sockl.accept() + self.ts = paramiko.Transport(self.socks, True) + host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') + self.ts.add_server_key(host_key) + self.ts.set_gss_host(targ_name) + try: + self.ts.load_server_moduli() + except: + print ('(Failed to load moduli -- gex will be unsupported.)') + server = NullServer() + self.ts.start_server(self.event, server) + + 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. + """ + 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), + 'ssh-rsa', public_host_key) + self.tc.connect(self.hostname, self.port, username=self.username, + gss_auth=True, gss_kex=True) + + self.event.wait(1.0) + self.assert_(self.event.isSet()) + self.assert_(self.ts.is_active()) + self.assertEquals(self.username, self.ts.get_username()) + self.assertEquals(True, self.ts.is_authenticated()) + + stdin, stdout, stderr = self.tc.exec_command('yes') + schan = self.ts.accept(1.0) + + schan.send('Hello there.\n') + schan.send_stderr('This is on stderr.\n') + schan.close() + + self.assertEquals('Hello there.\n', stdout.readline()) + self.assertEquals('', stdout.readline()) + self.assertEquals('This is on stderr.\n', stderr.readline()) + self.assertEquals('', stderr.readline()) + + stdin.close() + stdout.close() + stderr.close() diff --git a/tests/test_ssh_gss.py b/tests/test_ssh_gss.py new file mode 100644 index 00000000..98e280ec --- /dev/null +++ b/tests/test_ssh_gss.py @@ -0,0 +1,136 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss <sebastian.deiss@t-online.de> +# +# +# 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. + +""" +Unit Tests for the GSS-API / SSPI SSHv2 Authentication (gssapi-with-mic) + +@author: Sebastian Deiss +@contact: U{https://github.com/SebastianDeiss/paramiko/issues} +@organization: science + computing ag + (U{EMail<mailto:a.kruis@science-computing.de>}) +@copyright: (C) 2003-2007 Robey Pointer, (C) 2013-2014 U{science + computing ag + <https://www.science-computing.de>} +@license: GNU Lesser General Public License (LGPL) + +Created on 04.12.2013 +""" + +import socket +import threading +import unittest + +import paramiko + + +class NullServer (paramiko.ServerInterface): + + def get_allowed_auths(self, username): + return 'gssapi-with-mic' + + def check_auth_gssapi_with_mic(self, username, + gss_authenticated=paramiko.AUTH_FAILED, + cc_file=None): + if gss_authenticated == paramiko.AUTH_SUCCESSFUL: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def enable_auth_gssapi(self): + UseGSSAPI = True + GSSAPICleanupCredentials = True + return UseGSSAPI + + def check_channel_request(self, kind, chanid): + return paramiko.OPEN_SUCCEEDED + + def check_channel_exec_request(self, channel, command): + if command != 'yes': + return False + return True + + +class GSSAuthTest(unittest.TestCase): + + def init(username, hostname): + global krb5_principal, targ_name + krb5_principal = username + targ_name = hostname + + init = staticmethod(init) + + def setUp(self): + self.username = krb5_principal + self.hostname = socket.getfqdn(targ_name) + self.sockl = socket.socket() + self.sockl.bind((targ_name, 0)) + self.sockl.listen(1) + self.addr, self.port = self.sockl.getsockname() + self.event = threading.Event() + thread = threading.Thread(target=self._run) + thread.start() + + def tearDown(self): + for attr in "tc ts socks sockl".split(): + if hasattr(self, attr): + getattr(self, attr).close() + + def _run(self): + self.socks, addr = self.sockl.accept() + self.ts = paramiko.Transport(self.socks) + host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') + self.ts.add_server_key(host_key) + server = NullServer() + self.ts.start_server(self.event, server) + + def test_1_gss_auth(self): + """ + Verify that Paramiko can handle SSHv2 GSS-API / SSPI authentication + (gssapi-with-mic) in client and server mode. + """ + 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), + 'ssh-rsa', public_host_key) + self.tc.connect(self.hostname, self.port, username=self.username, + gss_auth=True) + + self.event.wait(1.0) + self.assert_(self.event.isSet()) + self.assert_(self.ts.is_active()) + self.assertEquals(self.username, self.ts.get_username()) + self.assertEquals(True, self.ts.is_authenticated()) + + stdin, stdout, stderr = self.tc.exec_command('yes') + schan = self.ts.accept(1.0) + + schan.send('Hello there.\n') + schan.send_stderr('This is on stderr.\n') + schan.close() + + self.assertEquals('Hello there.\n', stdout.readline()) + self.assertEquals('', stdout.readline()) + self.assertEquals('This is on stderr.\n', stderr.readline()) + self.assertEquals('', stderr.readline()) + + stdin.close() + stdout.close() + stderr.close() |