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