From e7f41de2f2dac5d03404f35edc5514f12e42c49f Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Tue, 11 Feb 2014 13:01:49 +0100 Subject: Merge branch scottkmaxwell:py3-support-without-py25 into SebastianDeiss:gssapi-py3-support --- demos/demo_server.py | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) (limited to 'demos/demo_server.py') diff --git a/demos/demo_server.py b/demos/demo_server.py index 915b0c67..bb35258b 100644 --- a/demos/demo_server.py +++ b/demos/demo_server.py @@ -27,6 +27,7 @@ import threading import traceback import paramiko +from paramiko.py3compat import b, u, decodebytes # setup logging @@ -35,17 +36,17 @@ paramiko.util.log_to_file('demo_server.log') host_key = paramiko.RSAKey(filename='test_rsa.key') #host_key = paramiko.DSSKey(filename='test_dss.key') -print 'Read key: ' + hexlify(host_key.get_fingerprint()) +print('Read key: ' + u(hexlify(host_key.get_fingerprint()))) class Server (paramiko.ServerInterface): # 'data' is the output of base64.encodestring(str(key)) # (using the "user_rsa_key" files) - data = 'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp' + \ - 'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC' + \ - 'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT' + \ - 'UWT10hcuO4Ks8=' - good_pub_key = paramiko.RSAKey(data=base64.decodestring(data)) + data = (b'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp' + b'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC' + b'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT' + b'UWT10hcuO4Ks8=') + good_pub_key = paramiko.RSAKey(data=decodebytes(data)) def __init__(self): self.event = threading.Event() @@ -61,7 +62,7 @@ class Server (paramiko.ServerInterface): return paramiko.AUTH_FAILED def check_auth_publickey(self, username, key): - print 'Auth attempt with key: ' + hexlify(key.get_fingerprint()) + print('Auth attempt with key: ' + u(hexlify(key.get_fingerprint()))) if (username == 'robey') and (key == self.good_pub_key): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED @@ -83,47 +84,47 @@ try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('', 2200)) -except Exception, e: - print '*** Bind failed: ' + str(e) +except Exception as e: + print('*** Bind failed: ' + str(e)) traceback.print_exc() sys.exit(1) try: sock.listen(100) - print 'Listening for connection ...' + print('Listening for connection ...') client, addr = sock.accept() -except Exception, e: - print '*** Listen/accept failed: ' + str(e) +except Exception as e: + print('*** Listen/accept failed: ' + str(e)) traceback.print_exc() sys.exit(1) -print 'Got a connection!' +print('Got a connection!') try: t = paramiko.Transport(client) try: t.load_server_moduli() except: - print '(Failed to load moduli -- gex will be unsupported.)' + print('(Failed to load moduli -- gex will be unsupported.)') raise t.add_server_key(host_key) server = Server() try: t.start_server(server=server) - except paramiko.SSHException, x: - print '*** SSH negotiation failed.' + except paramiko.SSHException: + print('*** SSH negotiation failed.') sys.exit(1) # wait for auth chan = t.accept(20) if chan is None: - print '*** No channel.' + print('*** No channel.') sys.exit(1) - print 'Authenticated!' + print('Authenticated!') server.event.wait(10) if not server.event.isSet(): - print '*** Client never asked for a shell.' + print('*** Client never asked for a shell.') sys.exit(1) chan.send('\r\n\r\nWelcome to my dorky little BBS!\r\n\r\n') @@ -135,8 +136,8 @@ try: chan.send('\r\nI don\'t like you, ' + username + '.\r\n') chan.close() -except Exception, e: - print '*** Caught exception: ' + str(e.__class__) + ': ' + str(e) +except Exception as e: + print('*** Caught exception: ' + str(e.__class__) + ': ' + str(e)) traceback.print_exc() try: t.close() -- cgit v1.2.3 From 3e1f9f09b1da0397f82e4ee9e1886f5271705e29 Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Tue, 11 Feb 2014 13:08:11 +0100 Subject: GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and user authentication with Python 3 support Add Python 3 support for the GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and user authentication. This patch supersedes pull request #250. --- .gitignore | 1 + README | 18 ++ demos/demo_server.py | 37 ++- demos/demo_sftp.py | 19 +- demos/demo_simple.py | 19 +- paramiko/__init__.py | 1 + paramiko/auth_handler.py | 203 ++++++++++++- paramiko/client.py | 84 +++++- paramiko/common.py | 13 +- paramiko/kex_group1.py | 29 +- paramiko/kex_group14.py | 33 +++ paramiko/kex_gss.py | 649 +++++++++++++++++++++++++++++++++++++++++ paramiko/server.py | 80 +++++ paramiko/ssh_gss.py | 619 +++++++++++++++++++++++++++++++++++++++ paramiko/transport.py | 116 +++++++- sites/_shared_static/logo.png | Bin 0 -> 6401 bytes sites/shared_conf.py | 41 +++ sites/www/_templates/rss.xml | 19 ++ sites/www/blog.py | 140 +++++++++ sites/www/blog.rst | 16 + sites/www/blog/first-post.rst | 7 + sites/www/blog/second-post.rst | 7 + sites/www/changelog.rst | 114 ++++++++ sites/www/conf.py | 35 +++ sites/www/contact.rst | 11 + sites/www/contributing.rst | 19 ++ sites/www/index.rst | 38 +++ sites/www/installing.rst | 105 +++++++ test.py | 46 +++ tests/test_gssapi.py | 167 +++++++++++ tests/test_kex_gss.py | 143 +++++++++ tests/test_ssh_gss.py | 136 +++++++++ 32 files changed, 2915 insertions(+), 50 deletions(-) mode change 100755 => 100644 demos/demo_sftp.py create mode 100644 paramiko/kex_group14.py create mode 100644 paramiko/kex_gss.py create mode 100644 paramiko/ssh_gss.py create mode 100644 sites/_shared_static/logo.png create mode 100644 sites/shared_conf.py create mode 100644 sites/www/_templates/rss.xml create mode 100644 sites/www/blog.py create mode 100644 sites/www/blog.rst create mode 100644 sites/www/blog/first-post.rst create mode 100644 sites/www/blog/second-post.rst create mode 100644 sites/www/changelog.rst create mode 100644 sites/www/conf.py create mode 100644 sites/www/contact.rst create mode 100644 sites/www/contributing.rst create mode 100644 sites/www/index.rst create mode 100644 sites/www/installing.rst create mode 100644 tests/test_gssapi.py create mode 100644 tests/test_kex_gss.py create mode 100644 tests/test_ssh_gss.py (limited to 'demos/demo_server.py') diff --git a/.gitignore b/.gitignore index 4b578950..bdf72de4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ paramiko.egg-info/ test.log docs/ +demos/*.log diff --git a/README b/README index 666b31a7..2712e9ff 100644 --- a/README +++ b/README @@ -71,6 +71,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 } + """ + 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 old mode 100755 new mode 100644 index a34f2b19..2cb44701 --- 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..100e15f5 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: + password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) # now, connect and use paramiko Client to negotiate SSH2 across the connection @@ -69,7 +74,13 @@ 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: + client.connect(hostname, Port, username, password) + else: + # SSPI works only with the FQDN of the target host + hostname = socket.getfqdn(hostname) + client.connect(hostname, Port, username, gss_auth=UseGSSAPI, + gss_kex=DoGSSAPIKeyExchange) chan = client.invoke_shell() print(repr(client.get_transport())) print('*** Here we go!\n') diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 8a814061..51329271 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -65,6 +65,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 from paramiko.channel import Channel, ChannelFile from paramiko.ssh_exception import SSHException, PasswordRequiredException, \ BadAuthenticationType, ChannelException, BadHostKeyException, \ diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 5046cac5..1910a7f1 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -32,13 +32,14 @@ from paramiko.message import Message 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 @@ -52,7 +53,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 @@ -109,6 +113,28 @@ class AuthHandler (object): 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() @@ -209,6 +235,76 @@ 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_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: U{RFC 4462 Section 3.8} + """ + 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) + 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: @@ -276,6 +372,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) @@ -341,6 +439,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: U{RFC 4462 Section 3.8 } + """ + 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 40ef7f0b..b786d17b 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -232,7 +232,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 L{load_system_host_keys}) @@ -278,6 +279,14 @@ class SSHClient (object): @param sock: an open socket or socket-like object (such as a L{Channel}) to use for communication to the target host @type sock: socket + @param gss_auth: C{True} if you want to use GSS-API authentication + @type gss_auth: Boolean + @param gss_kex: Perform GSS-API Key Exchange and user authentication + @type gss_kex: Boolean + @param gss_deleg_creds: Delegate GSS-API client credentials or not + @type gss_deleg_creds: Boolean + @param gss_host: The targets name in the kerberos database. default: hostname + @type gss_host: String @raise BadHostKeyException: if the server's host key could not be verified @@ -303,8 +312,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() @@ -317,17 +332,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() @@ -338,7 +363,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): """ @@ -428,7 +456,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: @@ -525,6 +554,31 @@ class SSHClient (object): elif two_factor: raise SSHException('Two-factor authentication requires a password') + """ + 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 and not self._transport.gss_kex_used: + try: + self._transport.auth_gssapi_with_mic(username, gss_host, + gss_deleg_creds) + return + except SSHException as e: + saved_exception = e + + """ + If GSS-API support and GSS-PI Key Exchange was performed, we attempt + authentication with gssapi-keyex. + """ + if gss_auth and gss_kex and self._transport.gss_kex_used: + try: + self._transport.auth_gssapi_keyex(username) + return + except SSHException as e: + saved_exception = e + # if we got an auth-failed exception earlier, re-raise it if saved_exception is not None: raise saved_exception diff --git a/paramiko/common.py b/paramiko/common.py index e30df73a..de7e38d2 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -28,6 +28,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, \ @@ -54,6 +57,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', @@ -73,7 +78,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 05693a1f..25eb2566 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -32,15 +32,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 + + b7fffffffffffffff = byte_chr(0x7f) + max_byte * 7 + b0000000000000000 = zero_byte * 8 + name = 'diffie-hellman-group1-sha1' def __init__(self, transport): @@ -53,11 +54,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) @@ -84,8 +85,8 @@ class KexGroup1(object): while 1: x_bytes = self.transport.rng.read(128) x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:] - if (x_bytes[:8] != b7fffffffffffffff) and \ - (x_bytes[:8] != b0000000000000000): + if (x_bytes[:8] != self.b7fffffffffffffff) and \ + (x_bytes[:8] != self.b0000000000000000): break self.x = util.inflate_long(x_bytes) @@ -93,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, @@ -112,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 +# +# 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..1fb7aea6 --- /dev/null +++ b/paramiko/kex_gss.py @@ -0,0 +1,649 @@ +# Copyright (C) 2003-2007 Robey Pointer +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss +# +# +# 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: +U{pyasn1 >= 0.1.7 }, +U{python-gssapi >= 0.4.0 (Unix) }, +U{pywin32 2.1.8 (Windows) }. + +@summary: SSH2 GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange Module +@version: 0.1 +@author: Sebastian Deiss +@contact: U{https://github.com/SebastianDeiss/paramiko/issues} +@organization: science + computing ag + (U{EMail}) +@copyright: (C) 2003-2007 Robey Pointer, (C) 2013-2014 U{science + computing ag + } +@license: GNU Lesser General Public License (LGPL) +@see: L{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.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 U{RFC 4462 Section 2 } + + @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: U{RFC 4462 Section 2.2 } + """ + # 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 ptype: The type of the incomming packet + @type ptype: Char + @param m: The paket content + @type m: L{Message} + """ + 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 m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message + @type m: L{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 m: The content of the SSH2_MSG_KEXGSS_CONTINUE message + @type m: L{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 m: The content of the SSH2_MSG_KEXGSS_COMPLETE message + @type m: L{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 m: The content of the SSH2_MSG_KEXGSS_INIT message + @type m: L{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 m: The content of the SSH2_MSG_KEXGSS_ERROR message + @type m: L{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 U{RFC 4462 Section 2 } + + @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: U{RFC 4462 Section 2.2 } + """ + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF + G = 2 + NAME = "gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==" + + +class KexGSSGex(object): + """ + GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange + as defined in U{RFC 4462 Section 2 } + + @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: U{RFC 4462 Section 2.2 } + """ + 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 ptype: The type of the incomming packet + @type ptype: Char + @param m: The paket content + @type m: L{Message} + """ + 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 m: The content of the SSH2_MSG_KEXGSS_GROUPREQ message + @type m: L{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 m: The content of the SSH2_MSG_KEXGSS_GROUP message + @type m: L{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 m: The content of the SSH2_MSG_KEXGSS_INIT message + @type m: L{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 m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message + @type m: L{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 m: The content of the SSH2_MSG_KEXGSS_CONTINUE message + @type m: L{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 m: The content of the SSH2_MSG_KEXGSS_COMPLETE message + @type m: L{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 m: The content of the SSH2_MSG_KEXGSS_ERROR message + @type m: L{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 U{RFC 4462 Section 5 } + """ + def __init__(self): + self.key = "" + + def __str__(self): + return self.key + + def get_name(self): + return self.key diff --git a/paramiko/server.py b/paramiko/server.py index a922201d..38decf5b 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -276,6 +276,86 @@ class ServerInterface (object): @rtype: int or L{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 username: The username of the authenticating client + @type username: String + @param gss_authenticated: The result of the krb5 authentication + @type gss_authenticated: Integer + @param cc_filename: The krb5 client credentials cache filename + @type cc_filename: String + @return: L{AUTH_FAILED} if the user is not authenticated otherwise + L{AUTH_SUCCESSFUL} + @rtype: Integer + @note: Kerberos credential delegation is not supported. + @see: L{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: U{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 username: The username of the authenticating client + @type username: String + @param gss_authenticated: The result of the krb5 authentication + @type gss_authenticated: Integer + @param cc_filename: The krb5 client credentials cache filename + @type cc_filename: String + @return: L{AUTH_FAILED} if the user is not authenticated otherwise + L{AUTH_SUCCESSFUL} + @rtype: Integer + @note: Kerberos credential delegation is not supported. + @see: L{ssh_gss}, L{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 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{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..72beeaae --- /dev/null +++ b/paramiko/ssh_gss.py @@ -0,0 +1,619 @@ +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss +# +# +# 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: +U{pyasn1 >= 0.1.7 }, +U{python-gssapi >= 0.4.0 (Unix) }, +U{pywin32 2.1.8 (Windows) }. + +@summary: SSH2 GSS-API / SSPI authentication module +@version: 0.1 +@author: Sebastian Deiss +@contact: U{https://github.com/SebastianDeiss/paramiko/issues} +@organization: science + computing ag + (U{EMail}) +@copyright: (C) 2013-2014 U{science + computing ag + } +@license: GNU Lesser General Public License (LGPL) +@see: L{kex_gss} + +Created on 07.11.2013 +""" + +import struct +import os +try: + from pyasn1.type.univ import ObjectIdentifier + from pyasn1.codec.der import encoder, decoder +except ImportError: + 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") + +from paramiko.common import MSG_USERAUTH_REQUEST +from paramiko.ssh_exception import SSHException + +""" +@var _API: constraint for the used API +@type _API: String +""" +_API = "MIT" + +try: + import gssapi +except ImportError: + try: + import sspicon + import sspi + _API = "SSPI" + except ImportError: + _API = None + + +def GSSAuth(auth_method, gss_deleg_creds=True): + """ + Provide SSH2 GSS-API / SSPI authentication for Paramiko. + + @param auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + @type auth_method: String + @param gss_deleg_creds: Delegate client credentials or not. + We delegate credentials by default. + @type gss_deleg_creds: Boolean + @return: Either an L{_SSH_GSSAPI} (Unix) object or an + L{_SSH_SSPI} (Windows) object + @rtype: Object + + @raise ImportError: If no GSS-API / SSPI module could be imported. + + @see: U{RFC 4462 } + @note: Check for the available API and return either an L{_SSH_GSSAPI} + (MIT GSSAPI) object or an L{_SSH_SSPI} (MS SSPI) object. If you + get python-gssapi working on Windows, python-gssapi + will be used and a L{_SSH_GSSAPI} object will be returned. + If there is no supported API available, + C{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 L{_SSH_GSSAPI} and + L{_SSH_SSPI}. + """ + def __init__(self, auth_method, gss_deleg_creds): + """ + @param auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + @type auth_method: String + @param gss_deleg_creds: Delegate client credentials or not + @type gss_deleg_creds: Boolean + """ + 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 service: The desired SSH service + @type service: String + @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 username: The name of the user who attempts to login + @type username: String + @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 mode: Client for client mode and server for server mode + @param mode: String + @return: A byte sequence containing the number of supported + OIDs, the length of the OID and the actual OID encoded with + DER + @note: In server mode we just return the OID length and the DER encoded + OID. + @rtype: Bytes + """ + 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 desired_mech: The desired GSS-API mechanism of the client + @type desired_mech: String + @return: C{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 integer: The integer value to convert + @type integer: Integer + @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 session_id: The SSH session ID + @type session_id: String + @param username: The name of the user who attempts to login + @type username: String + @param service: The requested SSH service + @type service: String + @param auth_method: The requested SSH authentication mechanism + @type auth_method: String + @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 += str.encode(username) + mic += self._make_uint32(len(service)) + mic += str.encode(service) + mic += self._make_uint32(len(auth_method)) + mic += str.encode(auth_method) + return mic + + +class _SSH_GSSAPI(_SSH_GSSAuth): + """ + Implementation of the GSS-API MIT Kerberos Authentication for SSH2. + + @see: L{GSSAuth} + """ + def __init__(self, auth_method, gss_deleg_creds): + """ + @param auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + @type auth_method: String + @param gss_deleg_creds: Delegate client credentials or not + @type gss_deleg_creds: Boolean + """ + _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 username: The name of the user who attempts to login + @type username: String + @param target: The hostname of the target to connect to + @type target: String + @param desired_mech: The negotiated GSS-API mechanism + ("pseudo negotiated" mechanism, because we + support just the krb5 mechanism :-)) + @type desired_mech: String + @param recv_token: The GSS-API token received from the Server + @type recv_token: String + @raise SSHException: Is raised if the desired mechanism of the client + is not supported + @return: A C{String} if the GSS-API has returned a token or C{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 + 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) + 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 session_id: The SSH session ID + @type session_id: String + @param gss_kex: Generate the MIC for GSS-API Key Exchange or not + @type gss_kex: Boolean + @return: gssapi-with-mic: + Returns the MIC token from GSS-API for the message we created + with C{_ssh_build_mic}. + gssapi-keyex: + Returns the MIC token from GSS-API with the SSH session ID as + message. + @rtype: String + @see: L{_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 hostname: The servers hostname + @type hostname: String + @param username: The name of the user who attempts to login + @type username: String + @param recv_token: The GSS-API Token received from the server, if it's + not the initial call + @type recv_token: String + @return: A C{String} if the GSS-API has returned a token or C{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 mic_token: The MIC token received from the client + @type mic_token: String + @param session_id: The SSH session ID + @type session_id: String + @param username: The name of the user who attempts to login + @type username: String + @return: 0 if the MIC check was successful and 1 if it fails + @rtype: Integer + """ + 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: C{True} if credentials are delegated, otherwise C{False} + @rtype: Boolean + """ + 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 client_token: The GSS-API token received form the client + @type client_token: String + @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: L{GSSAuth} + """ + def __init__(self, auth_method, gss_deleg_creds): + """ + @param auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + @type auth_method: String + @param gss_deleg_creds: Delegate client credentials or not + @type gss_deleg_creds: Boolean + """ + _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 username: The name of the user who attempts to login + @type username: String + @param target: The FQDN of the target to connect to + @type target: String + @param desired_mech: The negotiated SSPI mechanism + ("pseudo negotiated" mechanism, because we + support just the krb5 mechanism :-)) + @type desired_mech: String + @param recv_token: The SSPI token received from the Server + @type recv_token: String + @raise SSHException: Is raised if the desired mechanism of the client + is not supported + @return: A C{String} if the SSPI has returned a token or C{None} if + no token was returned + @rtype: String or None + """ + self._username = username + self._gss_host = target + 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.") + 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 + 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 session_id: The SSH session ID + @type session_id: String + @param gss_kex: Generate the MIC for Key Exchange with SSPI or not + @type gss_kex: Boolean + @return: gssapi-with-mic: + Returns the MIC token from SSPI for the message we created + with C{_ssh_build_mic}. + gssapi-keyex: + Returns the MIC token from SSPI with the SSH session ID as + message. + @rtype: String + @see: L{_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 hostname: The servers FQDN + @type hostname: String + @param username: The name of the user who attempts to login + @type username: String + @param recv_token: The SSPI Token received from the server, if it's not + the initial call + @type recv_token: String + @return: A C{String} if the SSPI has returned a token or C{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 mic_token: The MIC token received from the client + @type mic_token: String + @param session_id: The SSH session ID + @type session_id: String + @param username: The name of the user who attempts to login + @type username: String + @return: 0 if the MIC check was successful + @rtype: Integer + """ + 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: C{True} if credentials are delegated, otherwise C{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 client_token: The SSPI token received form the client + @type client_token: String + @raise NotImplementedError: Credential delegation is currently not + supported in server mode + """ + raise NotImplementedError diff --git a/paramiko/transport.py b/paramiko/transport.py index 6ab9274c..d46a6c6f 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -32,12 +32,15 @@ import weakref 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 * 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 @@ -205,7 +208,7 @@ class Transport (threading.Thread): '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 = { @@ -234,7 +237,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 = { @@ -249,7 +256,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 @@ -328,6 +335,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 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 @@ -418,6 +440,18 @@ 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 gss_host: The targets name in the kerberos database + Default: The name of the host to connect to + @type gss_host: String + @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 @@ -970,7 +1004,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 @@ -1000,6 +1035,14 @@ class Transport (threading.Thread): @param pkey: a private key to use for authentication, if you want to use private key authentication; otherwise C{None}. @type pkey: L{PKey} + @param gss_host: The host to authenticate to + @type gss_host: str + @param gss_auth: Enable or Disable GSSAPI Authentication + @type gss_auth: Boolean + @param gss_kex: Perform GSS-API Key Exchange and user authentication + @type gss_kex: Boolean + @param gss_deleg_creds: Use delegated credentails with GSSAPI + @type gss_deleg_cred: Boolean @raise SSHException: if the SSH2 negotiation fails, the host key supplied by the server is incorrect, or authentication fails. @@ -1010,7 +1053,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') @@ -1019,13 +1064,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 (pkey is not None) or (password is not None) or gss_auth or gss_kex: if password is not None: self._log(DEBUG, 'Attempting password auth...') self.auth_password(username, password) - else: + elif pkey is not None: self._log(DEBUG, 'Attempting public-key auth...') self.auth_publickey(username, pkey) + elif gss_auth: + self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-with-mic)') + self.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds) + else: + self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-keyex)') + self.auth_gssapi_keyex(username) return @@ -1308,6 +1359,57 @@ 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 username: The username to authenticate as + @type username: String + @param gss_host: The target host + @type gss_host: String + @param gss_deleg_creds: Delegate credentials or not + @type gss_deleg_creds: Boolean + @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 username: The username to authenticate as + @type username: String + @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 @@ -1582,7 +1684,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/_shared_static/logo.png b/sites/_shared_static/logo.png new file mode 100644 index 00000000..bc76697e Binary files /dev/null and b/sites/_shared_static/logo.png differ diff --git a/sites/shared_conf.py b/sites/shared_conf.py new file mode 100644 index 00000000..86ecdfe8 --- /dev/null +++ b/sites/shared_conf.py @@ -0,0 +1,41 @@ +from datetime import datetime +import os +import sys + +import alabaster + + +# Alabaster theme + mini-extension +html_theme_path = [alabaster.get_path()] +extensions = ['alabaster'] +# Paths relative to invoking conf.py - not this shared file +html_static_path = ['../_shared_static'] +html_theme = 'alabaster' +html_theme_options = { + 'description': "A Python implementation of SSHv2.", + 'github_user': 'paramiko', + 'github_repo': 'paramiko', + 'gittip_user': 'bitprophet', + 'analytics_id': 'UA-18486793-2', + + 'link': '#3782BE', + 'link_hover': '#3782BE', +} +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'searchbox.html', + 'donate.html', + ] +} + +# Regular settings +project = u'Paramiko' +year = datetime.now().year +copyright = u'%d Jeff Forcier' % year +master_doc = 'index' +templates_path = ['_templates'] +exclude_trees = ['_build'] +source_suffix = '.rst' +default_role = 'obj' diff --git a/sites/www/_templates/rss.xml b/sites/www/_templates/rss.xml new file mode 100644 index 00000000..f6f9cbd1 --- /dev/null +++ b/sites/www/_templates/rss.xml @@ -0,0 +1,19 @@ + + + + + {{ title }} + {{ link }} + {{ description }} + {{ date }} + {% for link, title, desc, date in posts %} + + {{ link }} + {{ link }} + <![CDATA[{{ title }}]]> + + {{ date }} + + {% endfor %} + + diff --git a/sites/www/blog.py b/sites/www/blog.py new file mode 100644 index 00000000..3b129ebf --- /dev/null +++ b/sites/www/blog.py @@ -0,0 +1,140 @@ +from collections import namedtuple +from datetime import datetime +import time +import email.utils + +from sphinx.util.compat import Directive +from docutils import nodes + + +class BlogDateDirective(Directive): + """ + Used to parse/attach date info to blog post documents. + + No nodes generated, since none are needed. + """ + has_content = True + + def run(self): + # Tag parent document with parsed date value. + self.state.document.blog_date = datetime.strptime( + self.content[0], "%Y-%m-%d" + ) + # Don't actually insert any nodes, we're already done. + return [] + +class blog_post_list(nodes.General, nodes.Element): + pass + +class BlogPostListDirective(Directive): + """ + Simply spits out a 'blog_post_list' temporary node for replacement. + + Gets replaced at doctree-resolved time - only then will all blog post + documents be written out (& their date directives executed). + """ + def run(self): + return [blog_post_list('')] + + +Post = namedtuple('Post', 'name doc title date opener') + +def get_posts(app): + # Obtain blog posts + post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs) + posts = map(lambda x: (x, app.env.get_doctree(x)), post_names) + # Obtain common data used for list page & RSS + data = [] + for post, doc in sorted(posts, key=lambda x: x[1].blog_date, reverse=True): + # Welp. No "nice" way to get post title. Thanks Sphinx. + title = doc[0][0][0] + # Date. This may or may not end up reflecting the required + # *input* format, but doing it here gives us flexibility. + date = doc.blog_date + # 1st paragraph as opener. TODO: allow a role or something marking + # where to actually pull from? + opener = doc.traverse(nodes.paragraph)[0] + data.append(Post(post, doc, title, date, opener)) + return data + +def replace_blog_post_lists(app, doctree, fromdocname): + """ + Replace blog_post_list nodes with ordered list-o-links to posts. + """ + # Obtain blog posts + post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs) + posts = map(lambda x: (x, app.env.get_doctree(x)), post_names) + # Build "list" of links/etc + post_links = [] + for post, doc, title, date, opener in get_posts(app): + # Link itself + uri = app.builder.get_relative_uri(fromdocname, post) + link = nodes.reference('', '', refdocname=post, refuri=uri) + # Title, bolded. TODO: use 'topic' or something maybe? + link.append(nodes.strong('', title)) + date = date.strftime("%Y-%m-%d") + # Meh @ not having great docutils nodes which map to this. + html = '
%s
' % date + timestamp = nodes.raw(text=html, format='html') + # NOTE: may group these within another element later if styling + # necessitates it + group = [timestamp, nodes.paragraph('', '', link), opener] + post_links.extend(group) + + # Replace temp node(s) w/ expanded list-o-links + for node in doctree.traverse(blog_post_list): + node.replace_self(post_links) + +def rss_timestamp(timestamp): + # Use horribly inappropriate module for its magical daylight-savings-aware + # timezone madness. Props to Tinkerer for the idea. + return email.utils.formatdate( + time.mktime(timestamp.timetuple()), + localtime=True + ) + +def generate_rss(app): + # Meh at having to run this subroutine like 3x per build. Not worth trying + # to be clever for now tho. + posts_ = get_posts(app) + # LOL URLs + root = app.config.rss_link + if not root.endswith('/'): + root += '/' + # Oh boy + posts = [ + ( + root + app.builder.get_target_uri(x.name), + x.title, + str(x.opener[0]), # Grab inner text element from paragraph + rss_timestamp(x.date), + ) + for x in posts_ + ] + location = 'blog/rss.xml' + context = { + 'title': app.config.project, + 'link': root, + 'atom': root + location, + 'description': app.config.rss_description, + # 'posts' is sorted by date already + 'date': rss_timestamp(posts_[0].date), + 'posts': posts, + } + yield (location, context, 'rss.xml') + +def setup(app): + # Link in RSS feed back to main website, e.g. 'http://paramiko.org' + app.add_config_value('rss_link', None, '') + # Ditto for RSS description field + app.add_config_value('rss_description', None, '') + # Interprets date metadata in blog post documents + app.add_directive('date', BlogDateDirective) + # Inserts blog post list node (in e.g. a listing page) for replacement + # below + app.add_node(blog_post_list) + app.add_directive('blog-posts', BlogPostListDirective) + # Performs abovementioned replacement + app.connect('doctree-resolved', replace_blog_post_lists) + # Generates RSS page from whole cloth at page generation step + app.connect('html-collect-pages', generate_rss) diff --git a/sites/www/blog.rst b/sites/www/blog.rst new file mode 100644 index 00000000..af9651e4 --- /dev/null +++ b/sites/www/blog.rst @@ -0,0 +1,16 @@ +==== +Blog +==== + +.. blog-posts directive gets replaced with an ordered list of blog posts. + +.. blog-posts:: + + +.. The following toctree ensures blog posts get processed. + +.. toctree:: + :hidden: + :glob: + + blog/* diff --git a/sites/www/blog/first-post.rst b/sites/www/blog/first-post.rst new file mode 100644 index 00000000..7b075073 --- /dev/null +++ b/sites/www/blog/first-post.rst @@ -0,0 +1,7 @@ +=========== +First post! +=========== + +A blog post. + +.. date:: 2013-12-04 diff --git a/sites/www/blog/second-post.rst b/sites/www/blog/second-post.rst new file mode 100644 index 00000000..c4463f33 --- /dev/null +++ b/sites/www/blog/second-post.rst @@ -0,0 +1,7 @@ +=========== +Another one +=========== + +.. date:: 2013-12-05 + +Indeed! diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst new file mode 100644 index 00000000..2680086e --- /dev/null +++ b/sites/www/changelog.rst @@ -0,0 +1,114 @@ +========= +Changelog +========= + +* :feature:`250` GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and user authentication. +* :bug:`193` (and its attentant PRs :issue:`230` & :issue:`253`): Fix SSH agent + problems present on Windows. Thanks to David Hobbs for initial report and to + Aarni Koskela & Olle Lundberg for the patches. +* :release:`1.12.1 <2014-01-08>` +* :release:`1.11.3 <2014-01-08>` 176 +* :release:`1.10.5 <2014-01-08>` 176 +* :bug:`225` Note ecdsa requirement in README. Thanks to Amaury Rodriguez for + the catch. +* :bug:`176` Fix AttributeError bugs in known_hosts file (re)loading. Thanks + to Nathan Scowcroft for the patch & Martin Blumenstingl for the initial test + case. +* :release:`1.12.0 <2013-09-27>` +* :release:`1.11.2 <2013-09-27>` +* :release:`1.10.4 <2013-09-27>` 199, 200, 179 +* :feature:`152` Add tentative support for ECDSA keys. *This adds the ecdsa + module as a new dependency of Paramiko.* The module is available at + [warner/python-ecdsa on Github](https://github.com/warner/python-ecdsa) and + [ecdsa on PyPI](https://pypi.python.org/pypi/ecdsa). + + * Note that you might still run into problems with key negotiation -- + Paramiko picks the first key that the server offers, which might not be + what you have in your known_hosts file. + * Mega thanks to Ethan Glasser-Camp for the patch. + +* :feature:`136` Add server-side support for the SSH protocol's 'env' command. + Thanks to Benjamin Pollack for the patch. +* :bug:`156` Fix potential deadlock condition when using Channel objects as + sockets (e.g. when using SSH gatewaying). Thanks to Steven Noonan and Frank + Arnold for catch & patch. +* :bug:`179` Fix a missing variable causing errors when an ssh_config file has + a non-default AddressFamily set. Thanks to Ed Marshall & Tomaz Muraus for + catch & patch. +* :bug:`200` Fix an exception-causing typo in ``demo_simple.py``. Thanks to Alex + Buchanan for catch & Dave Foster for patch. +* :bug:`199` Typo fix in the license header cross-project. Thanks to Armin + Ronacher for catch & patch. +* :release:`1.11.1 <2013-09-20>` +* :release:`1.10.3 <2013-09-20>` +* :bug:`162` Clean up HMAC module import to avoid deadlocks in certain uses of + SSHClient. Thanks to Gernot Hillier for the catch & suggested fix. +* :bug:`36` Fix the port-forwarding demo to avoid file descriptor errors. + Thanks to Jonathan Halcrow for catch & patch. +* :bug:`168` Update config handling to properly handle multiple 'localforward' + and 'remoteforward' keys. Thanks to Emre Yılmaz for the patch. +* :release:`1.11.0 <2013-07-26>` +* :release:`1.10.2 <2013-07-26>` +* :bug:`98 major` On Windows, when interacting with the PuTTY PAgeant, Paramiko + now creates the shared memory map with explicit Security Attributes of the + user, which is the same technique employed by the canonical PuTTY library to + avoid permissions issues when Paramiko is running under a different UAC + context than the PuTTY Ageant process. Thanks to Jason R. Coombs for the + patch. +* :support:`100` Remove use of PyWin32 in ``win_pageant`` module. Module was + already dependent on ctypes for constructing appropriate structures and had + ctypes implementations of all functionality. Thanks to Jason R. Coombs for + the patch. +* :bug:`87 major` Ensure updates to ``known_hosts`` files account for any + updates to said files after Paramiko initially read them. (Includes related + fix to guard against duplicate entries during subsequent ``known_hosts`` + loads.) Thanks to ``@sunweaver`` for the contribution. +* :bug:`153` (also :issue:`67`) Warn on parse failure when reading known_hosts + file. Thanks to ``@glasserc`` for patch. +* :bug:`146` Indentation fixes for readability. Thanks to Abhinav Upadhyay for + catch & patch. +* :release:`1.10.1 <2013-04-05>` +* :bug:`142` (`Fabric #811 `_) + SFTP put of empty file will still return the attributes of the put file. + Thanks to Jason R. Coombs for the patch. +* :bug:`154` (`Fabric #876 `_) + Forwarded SSH agent connections left stale local pipes lying around, which + could cause local (and sometimes remote or network) resource starvation when + running many agent-using remote commands. Thanks to Kevin Tegtmeier for catch + & patch. +* :release:`1.10.0 <2013-03-01>` +* :feature:`66` Batch SFTP writes to help speed up file transfers. Thanks to + Olle Lundberg for the patch. +* :bug:`133 major` Fix handling of window-change events to be on-spec and not + attempt to wait for a response from the remote sshd; this fixes problems with + less common targets such as some Cisco devices. Thanks to Phillip Heller for + catch & patch. +* :feature:`93` Overhaul SSH config parsing to be in line with ``man + ssh_config`` (& the behavior of ``ssh`` itself), including addition of parameter + expansion within config values. Thanks to Olle Lundberg for the patch. +* :feature:`110` Honor SSH config ``AddressFamily`` setting when looking up + local host's FQDN. Thanks to John Hensley for the patch. +* :feature:`128` Defer FQDN resolution until needed, when parsing SSH config + files. Thanks to Parantapa Bhattacharya for catch & patch. +* :bug:`102 major` Forego random padding for packets when running under + ``*-ctr`` ciphers. This corrects some slowdowns on platforms where random + byte generation is inefficient (e.g. Windows). Thanks to ``@warthog618`` for + catch & patch, and Michael van der Kolff for code/technique review. +* :feature:`127` Turn ``SFTPFile`` into a context manager. Thanks to Michael + Williamson for the patch. +* :feature:`116` Limit ``Message.get_bytes`` to an upper bound of 1MB to protect + against potential DoS vectors. Thanks to ``@mvschaik`` for catch & patch. +* :feature:`115` Add convenience ``get_pty`` kwarg to ``Client.exec_command`` so + users not manually controlling a channel object can still toggle PTY + creation. Thanks to Michael van der Kolff for the patch. +* :feature:`71` Add ``SFTPClient.putfo`` and ``.getfo`` methods to allow direct + uploading/downloading of file-like objects. Thanks to Eric Buehl for the + patch. +* :feature:`113` Add ``timeout`` parameter to ``SSHClient.exec_command`` for + easier setting of the command's internal channel object's timeout. Thanks to + Cernov Vladimir for the patch. +* :support:`94` Remove duplication of SSH port constant. Thanks to Olle + Lundberg for the catch. +* :feature:`80` Expose the internal "is closed" property of the file transfer + class ``BufferedFile`` as ``.closed``, better conforming to Python's file + interface. Thanks to ``@smunaut`` and James Hiscock for catch & patch. diff --git a/sites/www/conf.py b/sites/www/conf.py new file mode 100644 index 00000000..481acdff --- /dev/null +++ b/sites/www/conf.py @@ -0,0 +1,35 @@ +# Obtain shared config values +import sys +import os +from os.path import abspath, join, dirname + +sys.path.append(abspath(join(dirname(__file__), '..'))) +from shared_conf import * + +# Local blog extension +sys.path.append(abspath('.')) +extensions.append('blog') +rss_link = 'http://paramiko.org' +rss_description = 'Paramiko project news' + +# Releases changelog extension +extensions.append('releases') +releases_release_uri = "https://github.com/paramiko/paramiko/tree/%s" +releases_issue_uri = "https://github.com/paramiko/paramiko/issues/%s" + +# Intersphinx for referencing API/usage docs +extensions.append('sphinx.ext.intersphinx') +# Default is 'local' building, but reference the public docs site when building +# under RTD. +target = join(dirname(__file__), '..', 'docs', '_build') +if os.environ.get('READTHEDOCS') == 'True': + # TODO: switch to docs.paramiko.org post go-live of sphinx API docs + target = 'http://paramiko-docs.readthedocs.org/en/latest/' +#intersphinx_mapping = { +# 'docs': (target, None), +#} + +# Sister-site links to API docs +html_theme_options['extra_nav_links'] = { + "API Docs": 'http://docs.paramiko.org', +} diff --git a/sites/www/contact.rst b/sites/www/contact.rst new file mode 100644 index 00000000..2b6583f5 --- /dev/null +++ b/sites/www/contact.rst @@ -0,0 +1,11 @@ +======= +Contact +======= + +You can get in touch with the developer & user community in any of the +following ways: + +* IRC: ``#paramiko`` on Freenode +* Mailing list: ``paramiko@librelist.com`` (see `the LibreList homepage + `_ for usage details). +* This website - a blog section is forthcoming. diff --git a/sites/www/contributing.rst b/sites/www/contributing.rst new file mode 100644 index 00000000..2b752cc5 --- /dev/null +++ b/sites/www/contributing.rst @@ -0,0 +1,19 @@ +============ +Contributing +============ + +How to get the code +=================== + +Our primary Git repository is on Github at `paramiko/paramiko +`_; please follow their instructions for +cloning to your local system. (If you intend to submit patches/pull requests, +we recommend forking first, then cloning your fork. Github has excellent +documentation for all this.) + + +How to submit bug reports or new code +===================================== + +Please see `this project-agnostic contribution guide +`_ - we follow it explicitly. diff --git a/sites/www/index.rst b/sites/www/index.rst new file mode 100644 index 00000000..7fefedd2 --- /dev/null +++ b/sites/www/index.rst @@ -0,0 +1,38 @@ +Welcome to Paramiko! +==================== + +Paramiko is a Python (2.5+) implementation of the SSHv2 protocol [#]_, +providing both client and server functionality. While it leverages a Python C +extension for low level cryptography (`PyCrypto `_), +Paramiko itself is a pure Python interface around SSH networking concepts. + +This website covers project information for Paramiko such as the changelog, +contribution guidelines, development roadmap, news/blog, and so forth. Detailed +usage and API documentation can be found at our code documentation site, +`docs.paramiko.org `_. + +.. toctree:: + changelog + installing + contributing + contact + +.. Hide blog in hidden toctree for now (to avoid warnings.) + +.. toctree:: + :hidden: + + blog + + +.. rubric:: Footnotes + +.. [#] + SSH is defined in RFCs + `4251 `_, + `4252 `_, + `4253 `_, and + `4254 `_; + the primary working implementation of the protocol is the `OpenSSH project + `_. Paramiko implements a large portion of the SSH + feature set, but there are occasional gaps. diff --git a/sites/www/installing.rst b/sites/www/installing.rst new file mode 100644 index 00000000..0d4dc1ac --- /dev/null +++ b/sites/www/installing.rst @@ -0,0 +1,105 @@ +========== +Installing +========== + +Paramiko itself +=============== + +The recommended way to get Invoke is to **install the latest stable release** +via `pip `_:: + + $ pip install paramiko + +.. note:: + Users who want the bleeding edge can install the development version via + ``pip install paramiko==dev``. + +We currently support **Python 2.5/2.6/2.7**, with support for Python 3 coming +soon. Users on Python 2.4 or older are urged to upgrade. Paramiko *may* work on +Python 2.4 still, but there is no longer any support guarantee. + +Paramiko has two dependencies: the pure-Python ECDSA module `ecdsa`, and the +PyCrypto C extension. `ecdsa` is easily installable from wherever you +obtained Paramiko's package; PyCrypto may require more work. Read on for +details. + +PyCrypto +======== + +`PyCrypto `_ provides the low-level +(C-based) encryption algorithms we need to implement the SSH protocol. There +are a couple gotchas associated with installing PyCrypto: its compatibility +with Python's package tools, and the fact that it is a C-based extension. + +.. _pycrypto-and-pip: + +Possible gotcha on older Python and/or pip versions +--------------------------------------------------- + +We strongly recommend using ``pip`` to as it is newer and generally better than +``easy_install``. However, a combination of bugs in specific (now rather old) +versions of Python, ``pip`` and PyCrypto can prevent installation of PyCrypto. +Specifically: + +* Python = 2.5.x +* PyCrypto >= 2.1 (required for most modern versions of Paramiko) +* ``pip`` < 0.8.1 + +When all three criteria are met, you may encounter ``No such file or +directory`` IOErrors when trying to ``pip install paramiko`` or ``pip install +PyCrypto``. + +The fix is to make sure at least one of the above criteria is not met, by doing +the following (in order of preference): + +* Upgrade to ``pip`` 0.8.1 or above, e.g. by running ``pip install -U pip``. +* Upgrade to Python 2.6 or above. +* Downgrade to Paramiko 1.7.6 or 1.7.7, which do not require PyCrypto >= 2.1, + and install PyCrypto 2.0.1 (the oldest version on PyPI which works with + Paramiko 1.7.6/1.7.7) + + +C extension +----------- + +Unless you are installing from a precompiled source such as a Debian apt +repository or RedHat RPM, or using :ref:`pypm `, you will also need the +ability to build Python C-based modules from source in order to install +PyCrypto. Users on **Unix-based platforms** such as Ubuntu or Mac OS X will +need the traditional C build toolchain installed (e.g. Developer Tools / XCode +Tools on the Mac, or the ``build-essential`` package on Ubuntu or Debian Linux +-- basically, anything with ``gcc``, ``make`` and so forth) as well as the +Python development libraries, often named ``python-dev`` or similar. + +For **Windows** users we recommend using :ref:`pypm`, installing a C +development environment such as `Cygwin `_ or obtaining a +precompiled Win32 PyCrypto package from `voidspace's Python modules page +`_. + +.. note:: + Some Windows users whose Python is 64-bit have found that the PyCrypto + dependency ``winrandom`` may not install properly, leading to ImportErrors. + In this scenario, you'll probably need to compile ``winrandom`` yourself + via e.g. MS Visual Studio. See `Fabric #194 + `_ for info. + + +.. _pypm: + +ActivePython and PyPM +===================== + +Windows users who already have ActiveState's `ActivePython +`_ distribution installed +may find Paramiko is best installed with `its package manager, PyPM +`_. Below is example output from an +installation of Paramiko via ``pypm``:: + + C:\> pypm install paramiko + The following packages will be installed into "%APPDATA%\Python" (2.7): + paramiko-1.7.8 pycrypto-2.4 + Get: [pypm-free.activestate.com] paramiko 1.7.8 + Get: [pypm-free.activestate.com] pycrypto 2.4 + Installing paramiko-1.7.8 + Installing pycrypto-2.4 + C:\> diff --git a/test.py b/test.py index bd966d1e..a954ef27 100755 --- a/test.py +++ b/test.py @@ -44,6 +44,22 @@ 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_message import MessageTest +from test_file import BufferedFileTest +from test_buffered_pipe import BufferedPipeTest +from test_util import UtilTest +from test_hostkeys import HostKeysTest +from test_pkey import KeyTest +from test_kex import KexTest +from test_packetizer import PacketizerTest +from test_auth import AuthTest +from test_transport import TransportTest +from test_sftp import SFTPTest +from test_sftp_big import BigSFTPTest +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 +101,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 +128,16 @@ def main(): parser.add_option('-P', '--sftp-passwd', dest='password', type='string', default=default_passwd, metavar='', 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='', + help='The krb5 principal (your username) for GSS-API / SSPI authentication') + parser.add_option('--targ_name', dest='targ_name', type='string', + metavar='', + 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 +173,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/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 +# +# +# 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}) +@copyright: (C) 2013-2014 U{science + computing ag + } +@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 +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss +# +# +# 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}) +@copyright: (C) 2003-2009 Robey Pointer, (C) 2013-2014 U{science + computing ag + } +@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 +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss +# +# +# 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}) +@copyright: (C) 2003-2007 Robey Pointer, (C) 2013-2014 U{science + computing ag + } +@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() -- cgit v1.2.3 From f8355eed688b1b2e0a0c23ab9a210fd8b8be4a96 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 17:10:56 -0700 Subject: Fix outdated reference syntax in a demo --- demos/demo_server.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) (limited to 'demos/demo_server.py') diff --git a/demos/demo_server.py b/demos/demo_server.py index 74e4677e..5b3d5164 100644 --- a/demos/demo_server.py +++ b/demos/demo_server.py @@ -71,15 +71,18 @@ class Server (paramiko.ServerInterface): 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 } + .. note:: + We are just checking in `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 ``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. + + .. seealso:: + `krb5_kuserok() man page + `_ """ if gss_authenticated == paramiko.AUTH_SUCCESSFUL: return paramiko.AUTH_SUCCESSFUL -- cgit v1.2.3