summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorSebastian Deiss <s.deiss@science-computing.de>2014-02-11 13:08:11 +0100
committerSebastian Deiss <s.deiss@science-computing.de>2014-02-11 13:08:11 +0100
commit3e1f9f09b1da0397f82e4ee9e1886f5271705e29 (patch)
tree44fea1d9636830f32d95f144a8c20fbf4b2f30ad
parente7f41de2f2dac5d03404f35edc5514f12e42c49f (diff)
GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and user
authentication with Python 3 support Add Python 3 support for the GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and user authentication. This patch supersedes pull request #250.
-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.py19
-rw-r--r--paramiko/__init__.py1
-rw-r--r--paramiko/auth_handler.py203
-rw-r--r--paramiko/client.py84
-rw-r--r--paramiko/common.py13
-rw-r--r--paramiko/kex_group1.py29
-rw-r--r--paramiko/kex_group14.py33
-rw-r--r--paramiko/kex_gss.py649
-rw-r--r--paramiko/server.py80
-rw-r--r--paramiko/ssh_gss.py619
-rw-r--r--paramiko/transport.py116
-rw-r--r--sites/_shared_static/logo.pngbin0 -> 6401 bytes
-rw-r--r--sites/shared_conf.py41
-rw-r--r--sites/www/_templates/rss.xml19
-rw-r--r--sites/www/blog.py140
-rw-r--r--sites/www/blog.rst16
-rw-r--r--sites/www/blog/first-post.rst7
-rw-r--r--sites/www/blog/second-post.rst7
-rw-r--r--sites/www/changelog.rst114
-rw-r--r--sites/www/conf.py35
-rw-r--r--sites/www/contact.rst11
-rw-r--r--sites/www/contributing.rst19
-rw-r--r--sites/www/index.rst38
-rw-r--r--sites/www/installing.rst105
-rwxr-xr-xtest.py46
-rw-r--r--tests/test_gssapi.py167
-rw-r--r--tests/test_kex_gss.py143
-rw-r--r--tests/test_ssh_gss.py136
32 files changed, 2915 insertions, 50 deletions
diff --git a/.gitignore b/.gitignore
index 4b578950..bdf72de4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ dist/
paramiko.egg-info/
test.log
docs/
+demos/*.log
diff --git a/README b/README
index 666b31a7..2712e9ff 100644
--- a/README
+++ b/README
@@ -71,6 +71,24 @@ Bugs & Support
Please file bug reports at https://github.com/paramiko/paramiko/. There is currently no mailing list but we plan to create a new one ASAP.
+Kerberos Support
+----------------
+
+If you want paramiko to do kerberos authentication or key exchange using GSS-API or SSPI, you
+need the following python packages:
+
+- pyasn1 0.1.7 or better
+- python-gssapi 0.4.0 or better (Unix)
+- pywin32 2.1.8 or better (Windows)
+
+So you have to install pyasn1 and python-gssapi on Unix or pywin32 on Windows.
+To enable GSS-API / SSPI authentication or key exchange see the demos or paramiko docs.
+Note: If you use Microsoft SSPI for kerberos authentication and credential
+delegation in paramiko, make sure that the target host is trusted for
+delegation in the active directory configuration. For details see:
+http://technet.microsoft.com/en-us/library/cc738491%28v=ws.10%29.aspx
+
+
Demo
----
diff --git a/demos/demo_server.py b/demos/demo_server.py
index bb35258b..74e4677e 100644
--- a/demos/demo_server.py
+++ b/demos/demo_server.py
@@ -66,9 +66,39 @@ class Server (paramiko.ServerInterface):
if (username == 'robey') and (key == self.good_pub_key):
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
+
+ def check_auth_gssapi_with_mic(self, username,
+ gss_authenticated=paramiko.AUTH_FAILED,
+ cc_file=None):
+ """
+ @note: We are just checking in L{AuthHandler} that the given user is
+ a valid krb5 principal!
+ We don't check if the krb5 principal is allowed to log in on
+ the server, because there is no way to do that in python. So
+ if you develop your own SSH server with paramiko for a certain
+ platform like Linux, you should call C{krb5_kuserok()} in your
+ local kerberos library to make sure that the krb5_principal has
+ an account on the server and is allowed to log in as a user.
+ @see: U{krb5_kuserok() man page <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..100e15f5 100755
--- a/demos/demo_simple.py
+++ b/demos/demo_simple.py
@@ -36,6 +36,10 @@ except ImportError:
# setup logging
paramiko.util.log_to_file('demo_simple.log')
+# Paramiko client configuration
+UseGSSAPI = True # enable GSS-API / SSPI authentication
+DoGSSAPIKeyExchange = True
+Port = 22
# get hostname
username = ''
@@ -48,10 +52,10 @@ else:
if len(hostname) == 0:
print('*** Hostname required.')
sys.exit(1)
-port = 22
+
if hostname.find(':') >= 0:
hostname, portstr = hostname.split(':')
- port = int(portstr)
+ Port = int(portstr)
# get username
@@ -60,7 +64,8 @@ if username == '':
username = input('Username [%s]: ' % default_username)
if len(username) == 0:
username = default_username
-password = getpass.getpass('Password for %s@%s: ' % (username, hostname))
+if not UseGSSAPI:
+ password = getpass.getpass('Password for %s@%s: ' % (username, hostname))
# now, connect and use paramiko Client to negotiate SSH2 across the connection
@@ -69,7 +74,13 @@ try:
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.WarningPolicy())
print('*** Connecting...')
- client.connect(hostname, port, username, password)
+ if not UseGSSAPI:
+ client.connect(hostname, Port, username, password)
+ else:
+ # SSPI works only with the FQDN of the target host
+ hostname = socket.getfqdn(hostname)
+ client.connect(hostname, Port, username, gss_auth=UseGSSAPI,
+ gss_kex=DoGSSAPIKeyExchange)
chan = client.invoke_shell()
print(repr(client.get_transport()))
print('*** Here we go!\n')
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index 8a814061..51329271 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -65,6 +65,7 @@ __license__ = "GNU Lesser General Public License (LGPL)"
from paramiko.transport import SecurityOptions, Transport
from paramiko.client import SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, WarningPolicy
from paramiko.auth_handler import AuthHandler
+from paramiko.ssh_gss import GSSAuth
from paramiko.channel import Channel, ChannelFile
from paramiko.ssh_exception import SSHException, PasswordRequiredException, \
BadAuthenticationType, ChannelException, BadHostKeyException, \
diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py
index 5046cac5..1910a7f1 100644
--- a/paramiko/auth_handler.py
+++ b/paramiko/auth_handler.py
@@ -32,13 +32,14 @@ from paramiko.message import Message
from paramiko.ssh_exception import SSHException, AuthenticationException, \
BadAuthenticationType, PartialAuthentication
from paramiko.server import InteractiveQuery
+from paramiko.ssh_gss import GSSAuth
class AuthHandler (object):
"""
Internal class to handle the mechanics of authentication.
"""
-
+
def __init__(self, transport):
self.transport = weakref.proxy(transport)
self.username = None
@@ -52,7 +53,10 @@ class AuthHandler (object):
# for server mode:
self.auth_username = None
self.auth_fail_count = 0
-
+ # for GSSAPI
+ self.gss_host = None
+ self.gss_deleg_creds = True
+
def is_authenticated(self):
return self.authenticated
@@ -109,6 +113,28 @@ class AuthHandler (object):
finally:
self.transport.lock.release()
+ def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds, event):
+ self.transport.lock.acquire()
+ try:
+ self.auth_event = event
+ self.auth_method = 'gssapi-with-mic'
+ self.username = username
+ self.gss_host = gss_host
+ self.gss_deleg_creds = gss_deleg_creds
+ self._request_auth()
+ finally:
+ self.transport.lock.release()
+
+ def auth_gssapi_keyex(self, username, event):
+ self.transport.lock.acquire()
+ try:
+ self.auth_event = event
+ self.auth_method = 'gssapi-keyex'
+ self.username = username
+ self._request_auth()
+ finally:
+ self.transport.lock.release()
+
def abort(self):
if self.auth_event is not None:
self.auth_event.set()
@@ -209,6 +235,76 @@ class AuthHandler (object):
elif self.auth_method == 'keyboard-interactive':
m.add_string('')
m.add_string(self.submethods)
+ elif self.auth_method == "gssapi-with-mic":
+ sshgss = GSSAuth(self.auth_method, self.gss_deleg_creds)
+ m.add_bytes(sshgss.ssh_gss_oids())
+ # send the supported GSSAPI OIDs to the server
+ self.transport._send_message(m)
+ ptype, m = self.transport.packetizer.read_message()
+ if ptype == MSG_USERAUTH_GSSAPI_RESPONSE:
+ """
+ Read the mechanism selected by the server.
+ We send just the Kerberos V5 OID, so the server can only
+ respond with this OID.
+ """
+ mech = m.get_string()
+ m = Message()
+ m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
+ m.add_string(sshgss.ssh_init_sec_context(self.gss_host,
+ mech,
+ self.username,))
+ self.transport._send_message(m)
+ while True:
+ ptype, m = self.transport.packetizer.read_message()
+ if ptype == MSG_USERAUTH_GSSAPI_TOKEN:
+ srv_token = m.get_string()
+ next_token = sshgss.ssh_init_sec_context(self.gss_host,
+ mech,
+ self.username,
+ srv_token)
+ """
+ After this step the GSSAPI should not return any
+ token. If it does, we keep sending the token to the
+ server until no more token is returned.
+ """
+ if next_token is None:
+ break
+ else:
+ m = Message()
+ m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
+ m.add_string(next_token)
+ self.transport.send_message(m)
+ else:
+ raise SSHException("Received Package: %s" % MSG_NAMES[ptype])
+ m = Message()
+ m.add_byte(cMSG_USERAUTH_GSSAPI_MIC)
+ # send the MIC to the server
+ m.add_string(sshgss.ssh_get_mic(self.transport.session_id))
+ elif ptype == MSG_USERAUTH_GSSAPI_ERRTOK:
+ """
+ RFC 4462 says we are not required to implement GSS-API error
+ messages.
+ @see: U{RFC 4462 Section 3.8<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)
+ else:
+ raise SSHException("Received Package: %s" % MSG_NAMES[ptype])
+ elif self.auth_method == 'gssapi-keyex' and\
+ self.transport.gss_kex_used:
+ kexgss = self.transport.kexgss_ctxt
+ kexgss.set_username(self.username)
+ mic_token = kexgss.ssh_get_mic(self.transport.session_id)
+ m.add_string(mic_token)
elif self.auth_method == 'none':
pass
else:
@@ -276,6 +372,8 @@ class AuthHandler (object):
self._disconnect_no_more_auth()
return
self.auth_username = username
+ # check if GSS-API authentication is enabled
+ gss_auth = self.transport.server_object.enable_auth_gssapi()
if method == 'none':
result = self.transport.server_object.check_auth_none(username)
@@ -341,6 +439,107 @@ class AuthHandler (object):
# make interactive query instead of response
self._interactive_query(result)
return
+ elif method == "gssapi-with-mic" and gss_auth:
+ sshgss = GSSAuth(method)
+ """
+ OpenSSH sends just one OID. It's the Kerveros V5 OID and that's
+ the only OID we support.
+ """
+ # read the number of OID mechanisms supported by the client
+ mechs = m.get_int()
+ """
+ We can't accept more than one OID, so if the SSH client send more
+ than one disconnect
+ """
+ if mechs > 1:
+ self.transport._log(INFO,
+ 'Disconnect: Received more than one GSS-API OID mechanism')
+ self._disconnect_no_more_auth()
+ desired_mech = m.get_string()
+ mech_ok = sshgss.ssh_check_mech(desired_mech)
+ # if we don't support the mechanism, disconnect.
+ if not mech_ok:
+ self.transport._log(INFO,
+ 'Disconnect: Received an invalid GSS-API OID mechanism')
+ self._disconnect_no_more_auth()
+ # send the Kerberos V5 GSSAPI OID to the client
+ supported_mech = sshgss.ssh_gss_oids("server")
+ """
+ RFC 4462 says we are not required to implement GSS-API error
+ messages.
+ @see: U{RFC 4462 Section 3.8 <www.ietf.org/rfc/rfc4462.txt>}
+ """
+ while True:
+ m = Message()
+ m.add_byte(cMSG_USERAUTH_GSSAPI_RESPONSE)
+ m.add_bytes(supported_mech)
+ self.transport._send_message(m)
+ ptype, m = self.transport.packetizer.read_message()
+ if ptype == MSG_USERAUTH_GSSAPI_TOKEN:
+ client_token = m.get_string()
+ # use the client token as input to establish a secure
+ # context.
+ try:
+ token = sshgss.ssh_accept_sec_context(self.gss_host,
+ client_token,
+ username)
+ except Exception:
+ result = AUTH_FAILED
+ self._send_auth_result(username, method, result)
+ raise
+ if token is not None:
+ m = Message()
+ m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
+ m.add_string(token)
+ self.transport._send_message(m)
+ else:
+ raise SSHException("Client asked to handle paket %s"
+ %MSG_NAMES[ptype])
+ # check MIC
+ ptype, m = self.transport.packetizer.read_message()
+ if ptype == MSG_USERAUTH_GSSAPI_MIC:
+ break
+ mic_token = m.get_string()
+ try:
+ retval = sshgss.ssh_check_mic(mic_token,
+ self.transport.session_id,
+ username)
+ except Exception:
+ result = AUTH_FAILED
+ self._send_auth_result(username, method, result)
+ raise
+ if retval == 0:
+ """
+ @todo: Implement client credential saving
+ The OpenSSH server is able to create a TGT with the delegated
+ client credentials, but this is not supported by GSS-API.
+ """
+ result = AUTH_SUCCESSFUL
+ self.transport.server_object.check_auth_gssapi_with_mic(username,
+ result)
+ else:
+ result = AUTH_FAILED
+ elif method == "gssapi-keyex" and gss_auth:
+ mic_token = m.get_string()
+ sshgss = self.transport.kexgss_ctxt
+ if sshgss is None:
+ # If there is no valid context, we reject the authentication
+ result = AUTH_FAILED
+ self._send_auth_result(username, method, result)
+ try:
+ retval = sshgss.ssh_check_mic(mic_token,
+ self.transport.session_id,
+ self.auth_username)
+ except Exception:
+ result = AUTH_FAILED
+ self._send_auth_result(username, method, result)
+ raise
+ if retval == 0:
+ result = AUTH_SUCCESSFUL
+ self.transport.server_object.check_auth_gssapi_keyex(username,
+ result)
+ else:
+ result = AUTH_FAILED
else:
result = self.transport.server_object.check_auth_none(username)
# okay, send result
diff --git a/paramiko/client.py b/paramiko/client.py
index 40ef7f0b..b786d17b 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -232,7 +232,8 @@ class SSHClient (object):
def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=None,
key_filename=None, timeout=None, allow_agent=True, look_for_keys=True,
- compress=False, sock=None):
+ compress=False, sock=None, gss_auth=False, gss_kex=False,
+ gss_deleg_creds=True, gss_host=None):
"""
Connect to an SSH server and authenticate to it. The server's host key
is checked against the system host keys (see L{load_system_host_keys})
@@ -278,6 +279,14 @@ class SSHClient (object):
@param sock: an open socket or socket-like object (such as a
L{Channel}) to use for communication to the target host
@type sock: socket
+ @param gss_auth: C{True} if you want to use GSS-API authentication
+ @type gss_auth: Boolean
+ @param gss_kex: Perform GSS-API Key Exchange and user authentication
+ @type gss_kex: Boolean
+ @param gss_deleg_creds: Delegate GSS-API client credentials or not
+ @type gss_deleg_creds: Boolean
+ @param gss_host: The targets name in the kerberos database. default: hostname
+ @type gss_host: String
@raise BadHostKeyException: if the server's host key could not be
verified
@@ -303,8 +312,14 @@ class SSHClient (object):
pass
retry_on_signal(lambda: sock.connect(addr))
- t = self._transport = Transport(sock)
+ t = self._transport = Transport(sock, gss_kex, gss_deleg_creds)
t.use_compression(compress=compress)
+ if gss_kex and gss_host is None:
+ t.set_gss_host(hostname)
+ elif gss_kex and gss_host is not None:
+ t.set_gss_host(gss_host)
+ else:
+ pass
if self._log_channel is not None:
t.set_log_channel(self._log_channel)
t.start_client()
@@ -317,17 +332,27 @@ class SSHClient (object):
server_hostkey_name = hostname
else:
server_hostkey_name = "[%s]:%d" % (hostname, port)
- our_server_key = self._system_host_keys.get(server_hostkey_name, {}).get(keytype, None)
- if our_server_key is None:
- our_server_key = self._host_keys.get(server_hostkey_name, {}).get(keytype, None)
- if our_server_key is None:
- # will raise exception if the key is rejected; let that fall out
- self._policy.missing_host_key(self, server_hostkey_name, server_key)
- # if the callback returns, assume the key is ok
- our_server_key = server_key
-
- if server_key != our_server_key:
- raise BadHostKeyException(hostname, server_key, our_server_key)
+
+ """
+ If GSS-API Key Exchange is performed we are not required to check the
+ host key, because the host is authenticated via GSS-API / SSPI as well
+ as out client.
+ """
+ if not self._transport.use_gss_kex:
+ our_server_key = self._system_host_keys.get(server_hostkey_name,
+ {}).get(keytype, None)
+ if our_server_key is None:
+ our_server_key = self._host_keys.get(server_hostkey_name,
+ {}).get(keytype, None)
+ if our_server_key is None:
+ # will raise exception if the key is rejected; let that fall out
+ self._policy.missing_host_key(self, server_hostkey_name,
+ server_key)
+ # if the callback returns, assume the key is ok
+ our_server_key = server_key
+
+ if server_key != our_server_key:
+ raise BadHostKeyException(hostname, server_key, our_server_key)
if username is None:
username = getpass.getuser()
@@ -338,7 +363,10 @@ class SSHClient (object):
key_filenames = [ key_filename ]
else:
key_filenames = key_filename
- self._auth(username, password, pkey, key_filenames, allow_agent, look_for_keys)
+ if gss_host is None:
+ gss_host = hostname
+ self._auth(username, password, pkey, key_filenames, allow_agent,
+ look_for_keys, gss_auth, gss_kex, gss_deleg_creds, gss_host)
def close(self):
"""
@@ -428,7 +456,8 @@ class SSHClient (object):
"""
return self._transport
- def _auth(self, username, password, pkey, key_filenames, allow_agent, look_for_keys):
+ def _auth(self, username, password, pkey, key_filenames, allow_agent,
+ look_for_keys, gss_auth, gss_kex, gss_deleg_creds, gss_host):
"""
Try, in order:
@@ -525,6 +554,31 @@ class SSHClient (object):
elif two_factor:
raise SSHException('Two-factor authentication requires a password')
+ """
+ Try GSS-API authentication (gssapi-with-mic) only if GSS-API Key
+ Exchange is not performed, because if we use GSS-API for the key
+ exchange, there is already a fully established GSS-API context, so
+ why should we do that again?
+ """
+ if gss_auth and not self._transport.gss_kex_used:
+ try:
+ self._transport.auth_gssapi_with_mic(username, gss_host,
+ gss_deleg_creds)
+ return
+ except SSHException as e:
+ saved_exception = e
+
+ """
+ If GSS-API support and GSS-PI Key Exchange was performed, we attempt
+ authentication with gssapi-keyex.
+ """
+ if gss_auth and gss_kex and self._transport.gss_kex_used:
+ try:
+ self._transport.auth_gssapi_keyex(username)
+ return
+ except SSHException as e:
+ saved_exception = e
+
# if we got an auth-failed exception earlier, re-raise it
if saved_exception is not None:
raise saved_exception
diff --git a/paramiko/common.py b/paramiko/common.py
index e30df73a..de7e38d2 100644
--- a/paramiko/common.py
+++ b/paramiko/common.py
@@ -28,6 +28,9 @@ MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \
MSG_USERAUTH_BANNER = range(50, 54)
MSG_USERAUTH_PK_OK = 60
MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE = range(60, 62)
+MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN = range(60, 62)
+MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, MSG_USERAUTH_GSSAPI_ERROR,\
+MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC = range(63, 67)
MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE = range(80, 83)
MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \
MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA, \
@@ -54,6 +57,8 @@ MSG_NAMES = {
32: 'kex32',
33: 'kex33',
34: 'kex34',
+ 40: 'kex40',
+ 41: 'kex41',
MSG_USERAUTH_REQUEST: 'userauth-request',
MSG_USERAUTH_FAILURE: 'userauth-failure',
MSG_USERAUTH_SUCCESS: 'userauth-success',
@@ -73,7 +78,13 @@ MSG_NAMES = {
MSG_CHANNEL_CLOSE: 'channel-close',
MSG_CHANNEL_REQUEST: 'channel-request',
MSG_CHANNEL_SUCCESS: 'channel-success',
- MSG_CHANNEL_FAILURE: 'channel-failure'
+ MSG_CHANNEL_FAILURE: 'channel-failure',
+ MSG_USERAUTH_GSSAPI_RESPONSE: 'userauth-gssapi-response',
+ MSG_USERAUTH_GSSAPI_TOKEN: 'userauth-gssapi-token',
+ MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE: 'userauth-gssapi-exchange-complete',
+ MSG_USERAUTH_GSSAPI_ERROR: 'userauth-gssapi-error',
+ MSG_USERAUTH_GSSAPI_ERRTOK: 'userauth-gssapi-error-token',
+ MSG_USERAUTH_GSSAPI_MIC: 'userauth-gssapi-mic'
}
diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py
index 05693a1f..25eb2566 100644
--- a/paramiko/kex_group1.py
+++ b/paramiko/kex_group1.py
@@ -32,15 +32,16 @@ from paramiko.ssh_exception import SSHException
_MSG_KEXDH_INIT, _MSG_KEXDH_REPLY = range(30, 32)
c_MSG_KEXDH_INIT, c_MSG_KEXDH_REPLY = [byte_chr(c) for c in range(30, 32)]
-# draft-ietf-secsh-transport-09.txt, page 17
-P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF
-G = 2
-
-b7fffffffffffffff = byte_chr(0x7f) + max_byte * 7
-b0000000000000000 = zero_byte * 8
class KexGroup1(object):
+ # draft-ietf-secsh-transport-09.txt, page 17
+ P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF
+ G = 2
+
+ b7fffffffffffffff = byte_chr(0x7f) + max_byte * 7
+ b0000000000000000 = zero_byte * 8
+
name = 'diffie-hellman-group1-sha1'
def __init__(self, transport):
@@ -53,11 +54,11 @@ class KexGroup1(object):
self._generate_x()
if self.transport.server_mode:
# compute f = g^x mod p, but don't send it yet
- self.f = pow(G, self.x, P)
+ self.f = pow(self.G, self.x, self.P)
self.transport._expect_packet(_MSG_KEXDH_INIT)
return
# compute e = g^x mod p (where g=2), and send it
- self.e = pow(G, self.x, P)
+ self.e = pow(self.G, self.x, self.P)
m = Message()
m.add_byte(c_MSG_KEXDH_INIT)
m.add_mpint(self.e)
@@ -84,8 +85,8 @@ class KexGroup1(object):
while 1:
x_bytes = self.transport.rng.read(128)
x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:]
- if (x_bytes[:8] != b7fffffffffffffff) and \
- (x_bytes[:8] != b0000000000000000):
+ if (x_bytes[:8] != self.b7fffffffffffffff) and \
+ (x_bytes[:8] != self.b0000000000000000):
break
self.x = util.inflate_long(x_bytes)
@@ -93,10 +94,10 @@ class KexGroup1(object):
# client mode
host_key = m.get_string()
self.f = m.get_mpint()
- if (self.f < 1) or (self.f > P - 1):
+ if (self.f < 1) or (self.f > self.P - 1):
raise SSHException('Server kex "f" is out of range')
sig = m.get_binary()
- K = pow(self.f, self.x, P)
+ K = pow(self.f, self.x, self.P)
# okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
hm = Message()
hm.add(self.transport.local_version, self.transport.remote_version,
@@ -112,9 +113,9 @@ class KexGroup1(object):
def _parse_kexdh_init(self, m):
# server mode
self.e = m.get_mpint()
- if (self.e < 1) or (self.e > P - 1):
+ if (self.e < 1) or (self.e > self.P - 1):
raise SSHException('Client kex "e" is out of range')
- K = pow(self.e, self.x, P)
+ K = pow(self.e, self.x, self.P)
key = self.transport.get_server_key().asbytes()
# okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
hm = Message()
diff --git a/paramiko/kex_group14.py b/paramiko/kex_group14.py
new file mode 100644
index 00000000..a914aeaf
--- /dev/null
+++ b/paramiko/kex_group14.py
@@ -0,0 +1,33 @@
+# Copyright (C) 2013 Torsten Landschoff <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..1fb7aea6
--- /dev/null
+++ b/paramiko/kex_gss.py
@@ -0,0 +1,649 @@
+# 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:
+U{pyasn1 >= 0.1.7 <https://pypi.python.org/pypi/pyasn1>},
+U{python-gssapi >= 0.4.0 (Unix) <https://pypi.python.org/pypi/python-gssapi>},
+U{pywin32 2.1.8 (Windows) <sourceforge.net/projects/pywin32/>}.
+
+@summary: SSH2 GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange Module
+@version: 0.1
+@author: Sebastian Deiss
+@contact: U{https://github.com/SebastianDeiss/paramiko/issues}
+@organization: science + computing ag
+ (U{EMail<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)
+@see: L{ssh_gss}
+
+Created on 12.12.2013
+"""
+
+
+from Crypto.Hash import SHA
+from paramiko.common import *
+from paramiko import util
+from paramiko.message import Message
+from paramiko.ssh_exception import SSHException
+
+
+MSG_KEXGSS_INIT, MSG_KEXGSS_CONTINUE, MSG_KEXGSS_COMPLETE, MSG_KEXGSS_HOSTKEY,\
+MSG_KEXGSS_ERROR = range(30, 35)
+MSG_KEXGSS_GROUPREQ, MSG_KEXGSS_GROUP = range(40, 42)
+c_MSG_KEXGSS_INIT, c_MSG_KEXGSS_CONTINUE, c_MSG_KEXGSS_COMPLETE,\
+c_MSG_KEXGSS_HOSTKEY, c_MSG_KEXGSS_ERROR = [byte_chr(c) for c in range(30, 35)]
+c_MSG_KEXGSS_GROUPREQ, c_MSG_KEXGSS_GROUP = [byte_chr(c) for c in range(40, 42)]
+
+
+class KexGSSGroup1(object):
+ """
+ GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange
+ as defined in U{RFC 4462 Section 2 <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: U{RFC 4462 Section 2.2 <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 ptype: The type of the incomming packet
+ @type ptype: Char
+ @param m: The paket content
+ @type m: L{Message}
+ """
+ if self.transport.server_mode and (ptype == MSG_KEXGSS_INIT):
+ return self._parse_kexgss_init(m)
+ elif not self.transport.server_mode and (ptype == MSG_KEXGSS_HOSTKEY):
+ return self._parse_kexgss_hostkey(m)
+ elif self.transport.server_mode and (ptype == MSG_KEXGSS_CONTINUE):
+ return self._parse_kexgss_continue(m)
+ elif not self.transport.server_mode and (ptype == MSG_KEXGSS_COMPLETE):
+ return self._parse_kexgss_complete(m)
+ elif ptype == MSG_KEXGSS_ERROR:
+ return self._parse_kexgss_error(m)
+ raise SSHException('GSS KexGroup1 asked to handle packet type %d'
+ % ptype)
+
+ # ## internals...
+
+ def _generate_x(self):
+ """
+ generate an "x" (1 < x < q), where q is (p-1)/2.
+ p is a 128-byte (1024-bit) number, where the first 64 bits are 1.
+ therefore q can be approximated as a 2^1023. we drop the subset of
+ potential x where the first 63 bits are 1, because some of those will be
+ larger than q (but this is a tiny tiny subset of potential x).
+ """
+ while 1:
+ x_bytes = self.transport.rng.read(128)
+ x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:]
+ if (x_bytes[:8] != self.b7fffffffffffffff) and \
+ (x_bytes[:8] != self.b0000000000000000):
+ break
+ self.x = util.inflate_long(x_bytes)
+
+ def _parse_kexgss_hostkey(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_HOSTKEY message (client mode).
+
+ @param m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message
+ @type m: L{Message}
+ """
+ # client mode
+ host_key = m.get_string()
+ self.transport.host_key = host_key
+ sig = m.get_string()
+ self.transport._verify_key(host_key, sig)
+ self.transport._expect_packet(MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE)
+
+ def _parse_kexgss_continue(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_CONTINUE message.
+
+ @param m: The content of the SSH2_MSG_KEXGSS_CONTINUE message
+ @type m: L{Message}
+ """
+ if not self.transport.server_mode:
+ srv_token = m.get_string()
+ m = Message()
+ m.add_byte(c_MSG_KEXGSS_CONTINUE)
+ m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host,
+ recv_token=srv_token))
+ self.transport.send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE,
+ MSG_KEXGSS_ERROR)
+ else:
+ pass
+
+ def _parse_kexgss_complete(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_COMPLETE message (client mode).
+
+ @param m: The content of the SSH2_MSG_KEXGSS_COMPLETE message
+ @type m: L{Message}
+ """
+ # client mode
+ if self.transport.host_key is None:
+ self.transport.host_key = NullHostKey()
+ self.f = m.get_mpint()
+ if (self.f < 1) or (self.f > self.P - 1):
+ raise SSHException('Server kex "f" is out of range')
+ mic_token = m.get_string()
+ """
+ This must be TRUE, if there is a GSS-API token in this
+ message.
+ """
+ bool = m.get_boolean()
+ srv_token = None
+ if bool:
+ srv_token = m.get_string()
+ K = pow(self.f, self.x, self.P)
+ # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
+ hm = Message()
+ hm.add(self.transport.local_version, self.transport.remote_version,
+ self.transport.local_kex_init, self.transport.remote_kex_init)
+ hm.add_string(self.transport.host_key.__str__())
+ hm.add_mpint(self.e)
+ hm.add_mpint(self.f)
+ hm.add_mpint(K)
+ self.transport._set_K_H(K, SHA.new(str(hm)).digest())
+ if srv_token is not None:
+ self.kexgss.ssh_init_sec_context(target=self.gss_host,
+ recv_token=srv_token)
+ self.kexgss.ssh_check_mic(mic_token,
+ self.transport.session_id)
+ else:
+ self.kexgss.ssh_check_mic(mic_token,
+ self.transport.session_id)
+ self.transport._activate_outbound()
+
+ def _parse_kexgss_init(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_INIT message (server mode).
+
+ @param m: The content of the SSH2_MSG_KEXGSS_INIT message
+ @type m: L{Message}
+ """
+ # server mode
+ client_token = m.get_string()
+ self.e = m.get_mpint()
+ if (self.e < 1) or (self.e > self.P - 1):
+ raise SSHException('Client kex "e" is out of range')
+ K = pow(self.e, self.x, self.P)
+ self.transport.host_key = NullHostKey()
+ key = self.transport.host_key.__str__()
+ # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
+ hm = Message()
+ hm.add(self.transport.remote_version, self.transport.local_version,
+ self.transport.remote_kex_init, self.transport.local_kex_init)
+ hm.add_string(key)
+ hm.add_mpint(self.e)
+ hm.add_mpint(self.f)
+ hm.add_mpint(K)
+ H = SHA.new(hm.asbytes()).digest()
+ self.transport._set_K_H(K, H)
+ srv_token = self.kexgss.ssh_accept_sec_context(self.gss_host,
+ client_token)
+ m = Message()
+ if self.kexgss._gss_srv_ctxt_status:
+ mic_token = self.kexgss.ssh_get_mic(self.transport.session_id,
+ gss_kex=True)
+ m.add_byte(c_MSG_KEXGSS_COMPLETE)
+ m.add_mpint(self.f)
+ m.add_string(mic_token)
+ if srv_token is not None:
+ m.add_boolean(True)
+ m.add_string(srv_token)
+ else:
+ m.add_boolean(False)
+ self.transport._send_message(m)
+ self.transport._activate_outbound()
+ else:
+ m.add_byte(c_MSG_KEXGSS_CONTINUE)
+ m.add_string(srv_token)
+ self.transport._send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE,
+ MSG_KEXGSS_ERROR)
+
+ def _parse_kexgss_error(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_ERROR message (client mode).
+ The server may send a GSS-API error message. if it does, we display
+ the error by throwing an exception (client mode).
+
+ @param m: The content of the SSH2_MSG_KEXGSS_ERROR message
+ @type m: L{Message}
+ @raise SSHException: Contains GSS-API major and minor status as well as
+ the error message and the language tag of the
+ message
+ """
+ maj_status = m.get_int()
+ min_status = m.get_int()
+ err_msg = m.get_string()
+ lang_tag = m.get_string() # we don't care about the language!
+ raise SSHException("GSS-API Error:\nMajor Status: %s\nMinor Status: %s\
+ \nError Message: %s\n") % (str(maj_status),
+ str(min_status),
+ err_msg)
+
+
+class KexGSSGroup14(KexGSSGroup1):
+ """
+ GSS-API / SSPI Authenticated Diffie-Hellman Group14 Key Exchange
+ as defined in U{RFC 4462 Section 2 <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: U{RFC 4462 Section 2.2 <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 U{RFC 4462 Section 2 <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: U{RFC 4462 Section 2.2 <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 ptype: The type of the incomming packet
+ @type ptype: Char
+ @param m: The paket content
+ @type m: L{Message}
+ """
+ if ptype == MSG_KEXGSS_GROUPREQ:
+ return self._parse_kexgss_groupreq(m)
+ elif ptype == MSG_KEXGSS_GROUP:
+ return self._parse_kexgss_group(m)
+ elif ptype == MSG_KEXGSS_INIT:
+ return self._parse_kexgss_gex_init(m)
+ elif ptype == MSG_KEXGSS_HOSTKEY:
+ return self._parse_kexgss_hostkey(m)
+ elif ptype == MSG_KEXGSS_CONTINUE:
+ return self._parse_kexgss_continue(m)
+ elif ptype == MSG_KEXGSS_COMPLETE:
+ return self._parse_kexgss_complete(m)
+ elif ptype == MSG_KEXGSS_ERROR:
+ return self._parse_kexgss_error(m)
+ raise SSHException('KexGex asked to handle packet type %d' % ptype)
+
+ # ## internals...
+
+ def _generate_x(self):
+ # generate an "x" (1 < x < (p-1)/2).
+ q = (self.p - 1) // 2
+ qnorm = util.deflate_long(q, 0)
+ qhbyte = byte_ord(qnorm[0])
+ byte_count = len(qnorm)
+ qmask = 0xff
+ while not (qhbyte & 0x80):
+ qhbyte <<= 1
+ qmask >>= 1
+ while True:
+ x_bytes = self.transport.rng.read(byte_count)
+ x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:]
+ x = util.inflate_long(x_bytes, 1)
+ if (x > 1) and (x < q):
+ break
+ self.x = x
+
+ def _parse_kexgss_groupreq(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_GROUPREQ message (server mode).
+
+ @param m: The content of the SSH2_MSG_KEXGSS_GROUPREQ message
+ @type m: L{Message}
+ """
+ minbits = m.get_int()
+ preferredbits = m.get_int()
+ maxbits = m.get_int()
+ # smoosh the user's preferred size into our own limits
+ if preferredbits > self.max_bits:
+ preferredbits = self.max_bits
+ if preferredbits < self.min_bits:
+ preferredbits = self.min_bits
+ # fix min/max if they're inconsistent. technically, we could just pout
+ # and hang up, but there's no harm in giving them the benefit of the
+ # doubt and just picking a bitsize for them.
+ if minbits > preferredbits:
+ minbits = preferredbits
+ if maxbits < preferredbits:
+ maxbits = preferredbits
+ # now save a copy
+ self.min_bits = minbits
+ self.preferred_bits = preferredbits
+ self.max_bits = maxbits
+ # generate prime
+ pack = self.transport._get_modulus_pack()
+ if pack is None:
+ raise SSHException('Can\'t do server-side gex with no modulus pack')
+ self.transport._log(DEBUG, 'Picking p (%d <= %d <= %d bits)' % (minbits, preferredbits, maxbits))
+ self.g, self.p = pack.get_modulus(minbits, preferredbits, maxbits)
+ m = Message()
+ m.add_byte(c_MSG_KEXGSS_GROUP)
+ m.add_mpint(self.p)
+ m.add_mpint(self.g)
+ self.transport._send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_INIT)
+
+ def _parse_kexgss_group(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_GROUP message (client mode).
+
+ @param m: The content of the SSH2_MSG_KEXGSS_GROUP message
+ @type m: L{Message}
+ """
+ self.p = m.get_mpint()
+ self.g = m.get_mpint()
+ # reject if p's bit length < 1024 or > 8192
+ bitlen = util.bit_length(self.p)
+ if (bitlen < 1024) or (bitlen > 8192):
+ raise SSHException('Server-generated gex p (don\'t ask) is out of range (%d bits)' % bitlen)
+ self.transport._log(DEBUG, 'Got server p (%d bits)' % bitlen)
+ self._generate_x()
+ # now compute e = g^x mod p
+ self.e = pow(self.g, self.x, self.p)
+ m = Message()
+ m.add_byte(c_MSG_KEXGSS_INIT)
+ m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host))
+ m.add_mpint(self.e)
+ self.transport._send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_HOSTKEY,
+ MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE,
+ MSG_KEXGSS_ERROR)
+
+ def _parse_kexgss_gex_init(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_INIT message (server mode).
+
+ @param m: The content of the SSH2_MSG_KEXGSS_INIT message
+ @type m: L{Message}
+ """
+ client_token = m.get_string()
+ self.e = m.get_mpint()
+ if (self.e < 1) or (self.e > self.p - 1):
+ raise SSHException('Client kex "e" is out of range')
+ self._generate_x()
+ self.f = pow(self.g, self.x, self.p)
+ K = pow(self.e, self.x, self.p)
+ self.transport.host_key = NullHostKey()
+ key = self.transport.host_key.__str__()
+ # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K)
+ hm = Message()
+ hm.add(self.transport.remote_version, self.transport.local_version,
+ self.transport.remote_kex_init, self.transport.local_kex_init,
+ key)
+ hm.add_int(self.min_bits)
+ hm.add_int(self.preferred_bits)
+ hm.add_int(self.max_bits)
+ hm.add_mpint(self.p)
+ hm.add_mpint(self.g)
+ hm.add_mpint(self.e)
+ hm.add_mpint(self.f)
+ hm.add_mpint(K)
+ H = SHA.new(hm.asbytes()).digest()
+ self.transport._set_K_H(K, H)
+ srv_token = self.kexgss.ssh_accept_sec_context(self.gss_host,
+ client_token)
+ m = Message()
+ if self.kexgss._gss_srv_ctxt_status:
+ mic_token = self.kexgss.ssh_get_mic(self.transport.session_id,
+ gss_kex=True)
+ m.add_byte(c_MSG_KEXGSS_COMPLETE)
+ m.add_mpint(self.f)
+ m.add_string(mic_token)
+ if srv_token is not None:
+ m.add_boolean(True)
+ m.add_string(srv_token)
+ else:
+ m.add_boolean(False)
+ self.transport._send_message(m)
+ self.transport._activate_outbound()
+ else:
+ m.add_byte(c_MSG_KEXGSS_CONTINUE)
+ m.add_string(srv_token)
+ self.transport._send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE,
+ MSG_KEXGSS_ERROR)
+
+ def _parse_kexgss_hostkey(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_HOSTKEY message (client mode).
+
+ @param m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message
+ @type m: L{Message}
+ """
+ # client mode
+ host_key = m.get_string()
+ self.transport.host_key = host_key
+ sig = m.get_string()
+ self.transport._verify_key(host_key, sig)
+ self.transport._expect_packet(MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE)
+
+ def _parse_kexgss_continue(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_CONTINUE message.
+
+ @param m: The content of the SSH2_MSG_KEXGSS_CONTINUE message
+ @type m: L{Message}
+ """
+ if not self.transport.server_mode:
+ srv_token = m.get_string()
+ m = Message()
+ m.add_byte(c_MSG_KEXGSS_CONTINUE)
+ m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host,
+ recv_token=srv_token))
+ self.transport.send_message(m)
+ self.transport._expect_packet(MSG_KEXGSS_CONTINUE,
+ MSG_KEXGSS_COMPLETE,
+ MSG_KEXGSS_ERROR)
+ else:
+ pass
+
+ def _parse_kexgss_complete(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_COMPLETE message (client mode).
+
+ @param m: The content of the SSH2_MSG_KEXGSS_COMPLETE message
+ @type m: L{Message}
+ """
+ if self.transport.host_key is None:
+ self.transport.host_key = NullHostKey()
+ self.f = m.get_mpint()
+ mic_token = m.get_string()
+ """
+ This must be TRUE, if there is a GSS-API token in this
+ message.
+ """
+ bool = m.get_boolean()
+ srv_token = None
+ if bool:
+ srv_token = m.get_string()
+ if (self.f < 1) or (self.f > self.p - 1):
+ raise SSHException('Server kex "f" is out of range')
+ K = pow(self.f, self.x, self.p)
+ # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K)
+ hm = Message()
+ hm.add(self.transport.local_version, self.transport.remote_version,
+ self.transport.local_kex_init, self.transport.remote_kex_init,
+ self.transport.host_key.__str__())
+ if not self.old_style:
+ hm.add_int(self.min_bits)
+ hm.add_int(self.preferred_bits)
+ if not self.old_style:
+ hm.add_int(self.max_bits)
+ hm.add_mpint(self.p)
+ hm.add_mpint(self.g)
+ hm.add_mpint(self.e)
+ hm.add_mpint(self.f)
+ hm.add_mpint(K)
+ H = SHA.new(hm.asbytes()).digest()
+ self.transport._set_K_H(K, H)
+ if srv_token is not None:
+ self.kexgss.ssh_init_sec_context(target=self.gss_host,
+ recv_token=srv_token)
+ self.kexgss.ssh_check_mic(mic_token,
+ self.transport.session_id)
+ else:
+ self.kexgss.ssh_check_mic(mic_token,
+ self.transport.session_id)
+ self.transport._activate_outbound()
+
+ def _parse_kexgss_error(self, m):
+ """
+ Parse the SSH2_MSG_KEXGSS_ERROR message (client mode).
+ The server may send a GSS-API error message. if it does, we display
+ the error by throwing an exception (client mode).
+
+ @param m: The content of the SSH2_MSG_KEXGSS_ERROR message
+ @type m: L{Message}
+ @raise SSHException: Contains GSS-API major and minor status as well as
+ the error message and the language tag of the
+ message
+ """
+ maj_status = m.get_int()
+ min_status = m.get_int()
+ err_msg = m.get_string()
+ lang_tag = m.get_string() # we don't care about the language!
+ raise SSHException("GSS-API Error:\nMajor Status: %s\nMinor Status: %s\
+ \nError Message: %s\n") % (str(maj_status),
+ str(min_status),
+ err_msg)
+
+
+class NullHostKey(object):
+ """
+ This class represents the Null Host Key for GSS-API Key Exchange
+ as defined in U{RFC 4462 Section 5 <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/server.py b/paramiko/server.py
index a922201d..38decf5b 100644
--- a/paramiko/server.py
+++ b/paramiko/server.py
@@ -276,6 +276,86 @@ class ServerInterface (object):
@rtype: int or L{InteractiveQuery}
"""
return AUTH_FAILED
+
+ def check_auth_gssapi_with_mic(self, username,
+ gss_authenticated=AUTH_FAILED,
+ cc_file=None):
+ """
+ Authenticate the given user to the server if he is a valid krb5
+ principal.
+
+ @param username: The username of the authenticating client
+ @type username: String
+ @param gss_authenticated: The result of the krb5 authentication
+ @type gss_authenticated: Integer
+ @param cc_filename: The krb5 client credentials cache filename
+ @type cc_filename: String
+ @return: L{AUTH_FAILED} if the user is not authenticated otherwise
+ L{AUTH_SUCCESSFUL}
+ @rtype: Integer
+ @note: Kerberos credential delegation is not supported.
+ @see: L{ssh_gss}
+ @note: We are just checking in L{AuthHandler} that the given user is
+ a valid krb5 principal!
+ We don't check if the krb5 principal is allowed to log in on
+ the server, because there is no way to do that in python. So
+ if you develop your own SSH server with paramiko for a cetain
+ plattform like Linux, you should call C{krb5_kuserok()} in your
+ local kerberos library to make sure that the krb5_principal has
+ an account on the server and is allowed to log in as a user.
+ @see: U{http://www.unix.com/man-page/all/3/krb5_kuserok/}
+ """
+ if gss_authenticated == AUTH_SUCCESSFUL:
+ return AUTH_SUCCESSFUL
+ return AUTH_FAILED
+
+ def check_auth_gssapi_keyex(self, username,
+ gss_authenticated=AUTH_FAILED,
+ cc_file=None):
+ """
+ Authenticate the given user to the server if he is a valid krb5
+ principal and GSS-API Key Exchange was performed.
+ If GSS-API Key Exchange was not performed, this authentication method
+ won't be available.
+
+ @param username: The username of the authenticating client
+ @type username: String
+ @param gss_authenticated: The result of the krb5 authentication
+ @type gss_authenticated: Integer
+ @param cc_filename: The krb5 client credentials cache filename
+ @type cc_filename: String
+ @return: L{AUTH_FAILED} if the user is not authenticated otherwise
+ L{AUTH_SUCCESSFUL}
+ @rtype: Integer
+ @note: Kerberos credential delegation is not supported.
+ @see: L{ssh_gss}, L{kex_gss}
+ @note: We are just checking in L{AuthHandler} that the given user is
+ a valid krb5 principal!
+ We don't check if the krb5 principal is allowed to log in on
+ the server, because there is no way to do that in python. So
+ if you develop your own SSH server with paramiko for a certain
+ platform like Linux, you should call C{krb5_kuserok()} in your
+ local kerberos library to make sure that the krb5_principal has
+ an account on the server and is allowed to log in as a user.
+ @see: U{http://www.unix.com/man-page/all/3/krb5_kuserok/}
+ """
+ if gss_authenticated == AUTH_SUCCESSFUL:
+ return AUTH_SUCCESSFUL
+ return AUTH_FAILED
+
+ def enable_auth_gssapi(self):
+ """
+ Overwrite this function in your SSH server to enable GSSAPI
+ authentication.
+ The default implementation always returns false.
+
+ @return: True if GSSAPI authentication is enabled otherwise false
+ @rtype: Boolean
+ @see: ssh_gss
+ """
+ UseGSSAPI = False
+ GSSAPICleanupCredentials = False
+ return UseGSSAPI
def check_port_forward_request(self, address, port):
"""
diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py
new file mode 100644
index 00000000..72beeaae
--- /dev/null
+++ b/paramiko/ssh_gss.py
@@ -0,0 +1,619 @@
+# Copyright (C) 2013-2014 science + computing ag
+# Author: Sebastian Deiss <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:
+U{pyasn1 >= 0.1.7 <https://pypi.python.org/pypi/pyasn1>},
+U{python-gssapi >= 0.4.0 (Unix) <https://pypi.python.org/pypi/python-gssapi>},
+U{pywin32 2.1.8 (Windows) <sourceforge.net/projects/pywin32/>}.
+
+@summary: SSH2 GSS-API / SSPI authentication module
+@version: 0.1
+@author: Sebastian Deiss
+@contact: U{https://github.com/SebastianDeiss/paramiko/issues}
+@organization: science + computing ag
+ (U{EMail<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)
+@see: L{kex_gss}
+
+Created on 07.11.2013
+"""
+
+import struct
+import os
+try:
+ from pyasn1.type.univ import ObjectIdentifier
+ from pyasn1.codec.der import encoder, decoder
+except ImportError:
+ class ObjectIdentifier(object):
+ def __init__(self, *args):
+ raise NotImplementedError("Module pyasn1 not importable")
+
+ class decoder(object):
+ def decode(self):
+ raise NotImplementedError("Module pyasn1 not importable")
+
+from paramiko.common import MSG_USERAUTH_REQUEST
+from paramiko.ssh_exception import SSHException
+
+"""
+@var _API: constraint for the used API
+@type _API: String
+"""
+_API = "MIT"
+
+try:
+ import gssapi
+except ImportError:
+ try:
+ import sspicon
+ import sspi
+ _API = "SSPI"
+ except ImportError:
+ _API = None
+
+
+def GSSAuth(auth_method, gss_deleg_creds=True):
+ """
+ Provide SSH2 GSS-API / SSPI authentication for Paramiko.
+
+ @param auth_method: The name of the SSH authentication mechanism
+ (gssapi-with-mic or gss-keyex)
+ @type auth_method: String
+ @param gss_deleg_creds: Delegate client credentials or not.
+ We delegate credentials by default.
+ @type gss_deleg_creds: Boolean
+ @return: Either an L{_SSH_GSSAPI} (Unix) object or an
+ L{_SSH_SSPI} (Windows) object
+ @rtype: Object
+
+ @raise ImportError: If no GSS-API / SSPI module could be imported.
+
+ @see: U{RFC 4462 <www.ietf.org/rfc/rfc4462.txt>}
+ @note: Check for the available API and return either an L{_SSH_GSSAPI}
+ (MIT GSSAPI) object or an L{_SSH_SSPI} (MS SSPI) object. If you
+ get python-gssapi working on Windows, python-gssapi
+ will be used and a L{_SSH_GSSAPI} object will be returned.
+ If there is no supported API available,
+ C{None} will be returned.
+ """
+ if _API == "MIT":
+ return _SSH_GSSAPI(auth_method, gss_deleg_creds)
+ elif _API == "SSPI" and os.name == "nt":
+ return _SSH_SSPI(auth_method, gss_deleg_creds)
+ else:
+ raise ImportError("Unable to import a GSS-API / SSPI module!")
+
+
+class _SSH_GSSAuth(object):
+ """
+ Contains the shared variables and methods of L{_SSH_GSSAPI} and
+ L{_SSH_SSPI}.
+ """
+ def __init__(self, auth_method, gss_deleg_creds):
+ """
+ @param auth_method: The name of the SSH authentication mechanism
+ (gssapi-with-mic or gss-keyex)
+ @type auth_method: String
+ @param gss_deleg_creds: Delegate client credentials or not
+ @type gss_deleg_creds: Boolean
+ """
+ self._auth_method = auth_method
+ self._gss_deleg_creds = gss_deleg_creds
+ self._gss_host = None
+ self._username = None
+ self._session_id = None
+ self._service = "ssh-connection"
+ """
+ OpenSSH supports Kerberos V5 mechanism only for GSS-API authentication,
+ so we also support the krb5 mechanism only.
+ """
+ self._krb5_mech = "1.2.840.113554.1.2.2"
+
+ # client mode
+ self._gss_ctxt = None
+ self._gss_ctxt_status = False
+
+ # server mode
+ self._gss_srv_ctxt = None
+ self._gss_srv_ctxt_status = False
+ self.cc_file = None
+
+ def set_service(self, service):
+ """
+ This is just a setter to use a non default service.
+ I added this method, because RFC 4462 doesn't specify "ssh-connection"
+ as the only service value.
+
+ @param service: The desired SSH service
+ @type service: String
+ @rtype: Void
+ """
+ if service.find("ssh-"):
+ self._service = service
+
+ def set_username(self, username):
+ """
+ Setter for C{username}. If GSS-API Key Exchange is performed, the
+ username is not set by C{ssh_init_sec_context}.
+
+ @param username: The name of the user who attempts to login
+ @type username: String
+ @rtype: Void
+ """
+ self._username = username
+
+ def ssh_gss_oids(self, mode="client"):
+ """
+ This method returns a single OID, because we only support the
+ Kerberos V5 mechanism.
+
+ @param mode: Client for client mode and server for server mode
+ @param mode: String
+ @return: A byte sequence containing the number of supported
+ OIDs, the length of the OID and the actual OID encoded with
+ DER
+ @note: In server mode we just return the OID length and the DER encoded
+ OID.
+ @rtype: Bytes
+ """
+ OIDs = self._make_uint32(1)
+ krb5_OID = encoder.encode(ObjectIdentifier(self._krb5_mech))
+ OID_len = self._make_uint32(len(krb5_OID))
+ if mode == "server":
+ return OID_len + krb5_OID
+ return OIDs + OID_len + krb5_OID
+
+ def ssh_check_mech(self, desired_mech):
+ """
+ Check if the given OID is the Kerberos V5 OID (server mode).
+
+ @param desired_mech: The desired GSS-API mechanism of the client
+ @type desired_mech: String
+ @return: C{True} if the given OID is supported, otherwise C{False}
+ @rtype: Boolean
+ """
+ mech, __ = decoder.decode(desired_mech)
+ if mech.__str__() != self._krb5_mech:
+ return False
+ return True
+
+ # Internals
+ #--------------------------------------------------------------------------
+ def _make_uint32(self, integer):
+ """
+ Create a 32 bit unsigned integer (The byte sequence of an integer).
+
+ @param integer: The integer value to convert
+ @type integer: Integer
+ @return: The byte sequence of an 32 bit integer
+ @rtype: Bytes
+ """
+ return struct.pack("!I", integer)
+
+ def _ssh_build_mic(self, session_id, username, service, auth_method):
+ """
+ Create the SSH2 MIC filed for gssapi-with-mic.
+
+ @param session_id: The SSH session ID
+ @type session_id: String
+ @param username: The name of the user who attempts to login
+ @type username: String
+ @param service: The requested SSH service
+ @type service: String
+ @param auth_method: The requested SSH authentication mechanism
+ @type auth_method: String
+ @return: The MIC as defined in RFC 4462. The contents of the
+ MIC field are:
+ string session_identifier,
+ byte SSH_MSG_USERAUTH_REQUEST,
+ string user-name,
+ string service (ssh-connection),
+ string authentication-method
+ (gssapi-with-mic or gssapi-keyex)
+ @rtype: Bytes
+ """
+ mic = self._make_uint32(len(session_id))
+ mic += session_id
+ mic += struct.pack('B', MSG_USERAUTH_REQUEST)
+ mic += self._make_uint32(len(username))
+ mic += str.encode(username)
+ mic += self._make_uint32(len(service))
+ mic += str.encode(service)
+ mic += self._make_uint32(len(auth_method))
+ mic += str.encode(auth_method)
+ return mic
+
+
+class _SSH_GSSAPI(_SSH_GSSAuth):
+ """
+ Implementation of the GSS-API MIT Kerberos Authentication for SSH2.
+
+ @see: L{GSSAuth}
+ """
+ def __init__(self, auth_method, gss_deleg_creds):
+ """
+ @param auth_method: The name of the SSH authentication mechanism
+ (gssapi-with-mic or gss-keyex)
+ @type auth_method: String
+ @param gss_deleg_creds: Delegate client credentials or not
+ @type gss_deleg_creds: Boolean
+ """
+ _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds)
+
+ if self._gss_deleg_creds:
+ self._gss_flags = (gssapi.C_PROT_READY_FLAG,
+ gssapi.C_INTEG_FLAG,
+ gssapi.C_MUTUAL_FLAG,
+ gssapi.C_DELEG_FLAG)
+ else:
+ self._gss_flags = (gssapi.C_PROT_READY_FLAG,
+ gssapi.C_INTEG_FLAG,
+ gssapi.C_MUTUAL_FLAG)
+
+ def ssh_init_sec_context(self, target, desired_mech=None,
+ username=None, recv_token=None):
+ """
+ Initialize a GSS-API context.
+
+ @param username: The name of the user who attempts to login
+ @type username: String
+ @param target: The hostname of the target to connect to
+ @type target: String
+ @param desired_mech: The negotiated GSS-API mechanism
+ ("pseudo negotiated" mechanism, because we
+ support just the krb5 mechanism :-))
+ @type desired_mech: String
+ @param recv_token: The GSS-API token received from the Server
+ @type recv_token: String
+ @raise SSHException: Is raised if the desired mechanism of the client
+ is not supported
+ @return: A C{String} if the GSS-API has returned a token or C{None} if
+ no token was returned
+ @rtype: String or None
+ """
+ self._username = username
+ self._gss_host = target
+ targ_name = gssapi.Name("host@" + self._gss_host,
+ gssapi.C_NT_HOSTBASED_SERVICE)
+ ctx = gssapi.Context()
+ ctx.flags = self._gss_flags
+ if desired_mech is None:
+ krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech)
+ else:
+ mech, __ = decoder.decode(desired_mech)
+ if mech.__str__() != self._krb5_mech:
+ raise SSHException("Unsupported mechanism OID.")
+ else:
+ krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech)
+ token = None
+ if recv_token is None:
+ self._gss_ctxt = gssapi.InitContext(peer_name=targ_name,
+ mech_type=krb5_mech,
+ req_flags=ctx.flags)
+ token = self._gss_ctxt.step(token)
+ else:
+ token = self._gss_ctxt.step(recv_token)
+ self._gss_ctxt_status = self._gss_ctxt.established
+ return token
+
+ def ssh_get_mic(self, session_id, gss_kex=False):
+ """
+ Create the MIC token for a SSH2 message.
+
+ @param session_id: The SSH session ID
+ @type session_id: String
+ @param gss_kex: Generate the MIC for GSS-API Key Exchange or not
+ @type gss_kex: Boolean
+ @return: gssapi-with-mic:
+ Returns the MIC token from GSS-API for the message we created
+ with C{_ssh_build_mic}.
+ gssapi-keyex:
+ Returns the MIC token from GSS-API with the SSH session ID as
+ message.
+ @rtype: String
+ @see: L{_ssh_build_mic}
+ """
+ self._session_id = session_id
+ if not gss_kex:
+ mic_field = self._ssh_build_mic(self._session_id,
+ self._username,
+ self._service,
+ self._auth_method)
+ mic_token = self._gss_ctxt.get_mic(mic_field)
+ else:
+ # for key exchange with gssapi-keyex
+ mic_token = self._gss_srv_ctxt.get_mic(self._session_id)
+ return mic_token
+
+ def ssh_accept_sec_context(self, hostname, recv_token, username=None):
+ """
+ Accept a GSS-API context (server mode).
+
+ @param hostname: The servers hostname
+ @type hostname: String
+ @param username: The name of the user who attempts to login
+ @type username: String
+ @param recv_token: The GSS-API Token received from the server, if it's
+ not the initial call
+ @type recv_token: String
+ @return: A C{String} if the GSS-API has returned a token or C{None} if
+ no token was returned
+ @rtype: String or None
+ """
+ # hostname and username are not required for GSSAPI, but for SSPI
+ self._gss_host = hostname
+ self._username = username
+ if self._gss_srv_ctxt is None:
+ self._gss_srv_ctxt = gssapi.AcceptContext()
+ token = self._gss_srv_ctxt.step(recv_token)
+ self._gss_srv_ctxt_status = self._gss_srv_ctxt.established
+ return token
+
+ def ssh_check_mic(self, mic_token, session_id, username=None):
+ """
+ Verify the MIC token for a SSH2 message.
+
+ @param mic_token: The MIC token received from the client
+ @type mic_token: String
+ @param session_id: The SSH session ID
+ @type session_id: String
+ @param username: The name of the user who attempts to login
+ @type username: String
+ @return: 0 if the MIC check was successful and 1 if it fails
+ @rtype: Integer
+ """
+ self._session_id = session_id
+ self._username = username
+ if self._username is not None:
+ # server mode
+ mic_field = self._ssh_build_mic(self._session_id,
+ self._username,
+ self._service,
+ self._auth_method)
+ mic_status = self._gss_srv_ctxt.verify_mic(mic_field,
+ mic_token)
+ else:
+ # for key exchange with gssapi-keyex
+ # client mode
+ mic_status = self._gss_ctxt.verify_mic(self._session_id,
+ mic_token)
+ return mic_status
+
+ @property
+ def credentials_delegated(self):
+ """
+ Checks if credentials are delegated (server mode).
+
+ @return: C{True} if credentials are delegated, otherwise C{False}
+ @rtype: Boolean
+ """
+ if self._gss_srv_ctxt.delegated_cred is not None:
+ return True
+ return False
+
+ def save_client_creds(self, client_token):
+ """
+ Save the Client token in a file. This is used by the SSH server
+ to store the client credentials if credentials are delegated
+ (server mode).
+
+ @param client_token: The GSS-API token received form the client
+ @type client_token: String
+ @raise NotImplementedError: Credential delegation is currently not
+ supported in server mode
+ """
+ raise NotImplementedError
+
+
+class _SSH_SSPI(_SSH_GSSAuth):
+ """
+ Implementation of the Microsoft SSPI Kerberos Authentication for SSH2.
+
+ @see: L{GSSAuth}
+ """
+ def __init__(self, auth_method, gss_deleg_creds):
+ """
+ @param auth_method: The name of the SSH authentication mechanism
+ (gssapi-with-mic or gss-keyex)
+ @type auth_method: String
+ @param gss_deleg_creds: Delegate client credentials or not
+ @type gss_deleg_creds: Boolean
+ """
+ _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds)
+
+ if self._gss_deleg_creds:
+ self._gss_flags = sspicon.ISC_REQ_INTEGRITY |\
+ sspicon.ISC_REQ_MUTUAL_AUTH |\
+ sspicon.ISC_REQ_DELEGATE
+ else:
+ self._gss_flags = sspicon.ISC_REQ_INTEGRITY |\
+ sspicon.ISC_REQ_MUTUAL_AUTH
+
+ def ssh_init_sec_context(self, target, desired_mech=None,
+ username=None, recv_token=None):
+ """
+ Initialize a SSPI context.
+
+ @param username: The name of the user who attempts to login
+ @type username: String
+ @param target: The FQDN of the target to connect to
+ @type target: String
+ @param desired_mech: The negotiated SSPI mechanism
+ ("pseudo negotiated" mechanism, because we
+ support just the krb5 mechanism :-))
+ @type desired_mech: String
+ @param recv_token: The SSPI token received from the Server
+ @type recv_token: String
+ @raise SSHException: Is raised if the desired mechanism of the client
+ is not supported
+ @return: A C{String} if the SSPI has returned a token or C{None} if
+ no token was returned
+ @rtype: String or None
+ """
+ self._username = username
+ self._gss_host = target
+ targ_name = "host/" + self._gss_host
+ if desired_mech is not None:
+ mech, __ = decoder.decode(desired_mech)
+ if mech.__str__() != self._krb5_mech:
+ raise SSHException("Unsupported mechanism OID.")
+ if recv_token is None:
+ self._gss_ctxt = sspi.ClientAuth("Kerberos",
+ scflags=self._gss_flags,
+ targetspn=targ_name)
+ error, token = self._gss_ctxt.authorize(recv_token)
+ token = token[0].Buffer
+ if error == 0:
+ """
+ if the status is GSS_COMPLETE (error = 0) the context is fully
+ established an we can set _gss_ctxt_status to True.
+ """
+ self._gss_ctxt_status = True
+ token = None
+ """
+ You won't get another token if the context is fully established,
+ so i set token to None instead of ""
+ """
+ return token
+
+ def ssh_get_mic(self, session_id, gss_kex=False):
+ """
+ Create the MIC token for a SSH2 message.
+
+ @param session_id: The SSH session ID
+ @type session_id: String
+ @param gss_kex: Generate the MIC for Key Exchange with SSPI or not
+ @type gss_kex: Boolean
+ @return: gssapi-with-mic:
+ Returns the MIC token from SSPI for the message we created
+ with C{_ssh_build_mic}.
+ gssapi-keyex:
+ Returns the MIC token from SSPI with the SSH session ID as
+ message.
+ @rtype: String
+ @see: L{_ssh_build_mic}
+ """
+ self._session_id = session_id
+ if not gss_kex:
+ mic_field = self._ssh_build_mic(self._session_id,
+ self._username,
+ self._service,
+ self._auth_method)
+ mic_token = self._gss_ctxt.sign(mic_field)
+ else:
+ # for key exchange with gssapi-keyex
+ mic_token = self._gss_srv_ctxt.sign(self._session_id)
+ return mic_token
+
+ def ssh_accept_sec_context(self, hostname, username, recv_token):
+ """
+ Accept a SSPI context (server mode).
+
+ @param hostname: The servers FQDN
+ @type hostname: String
+ @param username: The name of the user who attempts to login
+ @type username: String
+ @param recv_token: The SSPI Token received from the server, if it's not
+ the initial call
+ @type recv_token: String
+ @return: A C{String} if the SSPI has returned a token or C{None} if
+ no token was returned
+ @rtype: String or None
+ """
+ self._gss_host = hostname
+ self._username = username
+ targ_name = "host/" + self._gss_host
+ self._gss_srv_ctxt = sspi.ServerAuth("Kerberos", spn=targ_name)
+ error, token = self._gss_srv_ctxt.authorize(recv_token)
+ token = token[0].Buffer
+ if error == 0:
+ self._gss_srv_ctxt_status = True
+ token = None
+ return token
+
+ def ssh_check_mic(self, mic_token, session_id, username=None):
+ """
+ Verify the MIC token for a SSH2 message.
+
+ @param mic_token: The MIC token received from the client
+ @type mic_token: String
+ @param session_id: The SSH session ID
+ @type session_id: String
+ @param username: The name of the user who attempts to login
+ @type username: String
+ @return: 0 if the MIC check was successful
+ @rtype: Integer
+ """
+ self._session_id = session_id
+ self._username = username
+ mic_status = 1
+ if username is not None:
+ # server mode
+ mic_field = self._ssh_build_mic(self._session_id,
+ self._username,
+ self._service,
+ self._auth_method)
+ mic_status = self._gss_srv_ctxt.verify(mic_field,
+ mic_token)
+ else:
+ # for key exchange with gssapi-keyex
+ # client mode
+ mic_status = self._gss_ctxt.verify(self._session_id,
+ mic_token)
+ """
+ The SSPI method C{verify} has no return value, so if no SSPI error
+ is returned, set C{mic_status} to 0.
+ """
+ mic_status = 0
+ return mic_status
+
+ @property
+ def credentials_delegated(self):
+ """
+ Checks if credentials are delegated (server mode).
+
+ @return: C{True} if credentials are delegated, otherwise C{False}
+ @rtype: Boolean
+ """
+ return (
+ self._gss_flags & sspicon.ISC_REQ_DELEGATE
+ ) and (
+ self._gss_srv_ctxt_status or (self._gss_flags)
+ )
+
+ def save_client_creds(self, client_token):
+ """
+ Save the Client token in a file. This is used by the SSH server
+ to store the client credentails if credentials are delegated
+ (server mode).
+
+ @param client_token: The SSPI token received form the client
+ @type client_token: String
+ @raise NotImplementedError: Credential delegation is currently not
+ supported in server mode
+ """
+ raise NotImplementedError
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 6ab9274c..d46a6c6f 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -32,12 +32,15 @@ import weakref
import paramiko
from paramiko import util
from paramiko.auth_handler import AuthHandler
+from paramiko.ssh_gss import GSSAuth
from paramiko.channel import Channel
from paramiko.common import *
from paramiko.compress import ZlibCompressor, ZlibDecompressor
from paramiko.dsskey import DSSKey
from paramiko.kex_gex import KexGex
from paramiko.kex_group1 import KexGroup1
+from paramiko.kex_group14 import KexGroup14
+from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14, NullHostKey
from paramiko.message import Message
from paramiko.packet import Packetizer, NeedRekeyException
from paramiko.primes import ModulusPack
@@ -205,7 +208,7 @@ class Transport (threading.Thread):
'arcfour128', 'arcfour256' )
_preferred_macs = ( 'hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96' )
_preferred_keys = ( 'ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256' )
- _preferred_kex = ( 'diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1' )
+ _preferred_kex = ( 'diffie-hellman-group1-sha1', 'diffie-hellman-group14-sha1', 'diffie-hellman-group-exchange-sha1' )
_preferred_compression = ( 'none', )
_cipher_info = {
@@ -234,7 +237,11 @@ class Transport (threading.Thread):
_kex_info = {
'diffie-hellman-group1-sha1': KexGroup1,
+ 'diffie-hellman-group14-sha1': KexGroup14,
'diffie-hellman-group-exchange-sha1': KexGex,
+ 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup1,
+ 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup14,
+ 'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGex
}
_compression_info = {
@@ -249,7 +256,7 @@ class Transport (threading.Thread):
_modulus_pack = None
- def __init__(self, sock):
+ def __init__(self, sock, gss_kex=False, gss_deleg_creds=True):
"""
Create a new SSH session over an existing socket, or socket-like
object. This only creates the Transport object; it doesn't begin the
@@ -328,6 +335,21 @@ class Transport (threading.Thread):
self.host_key_type = None
self.host_key = None
+ # GSS-API / SSPI Key Exchange
+ self.use_gss_kex = gss_kex
+ # This will be set to True if GSS-API Key Exchange was performed
+ self.gss_kex_used = False
+ self.kexgss_ctxt = None
+ self.gss_host = None
+ if gss_kex:
+ self.kexgss_ctxt = GSSAuth("gssapi-keyex", gss_deleg_creds)
+ self._preferred_kex = ('gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==',
+ 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==',
+ 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==',
+ 'diffie-hellman-group-exchange-sha1',
+ 'diffie-hellman-group1-sha1',
+ 'diffie-hellman-group14-sha1')
+
# state used during negotiation
self.kex_engine = None
self.H = None
@@ -418,6 +440,18 @@ class Transport (threading.Thread):
"""
return SecurityOptions(self)
+ def set_gss_host(self, gss_host):
+ """
+ Setter for C{gss_host} if GSS-API Key Exchange is performed.
+
+ @param gss_host: The targets name in the kerberos database
+ Default: The name of the host to connect to
+ @type gss_host: String
+ @rtype: Void
+ """
+ # We need the FQDN to get this working with SSPI
+ self.gss_host = socket.getfqdn(gss_host)
+
def start_client(self, event=None):
"""
Negotiate a new SSH2 session as a client. This is the first step after
@@ -970,7 +1004,8 @@ class Transport (threading.Thread):
self.lock.release()
return chan
- def connect(self, hostkey=None, username='', password=None, pkey=None):
+ def connect(self, hostkey=None, username='', password=None, pkey=None,
+ gss_host=None, gss_auth=False, gss_kex=False, gss_deleg_creds=True):
"""
Negotiate an SSH2 session, and optionally verify the server's host key
and authenticate using a password or private key. This is a shortcut
@@ -1000,6 +1035,14 @@ class Transport (threading.Thread):
@param pkey: a private key to use for authentication, if you want to
use private key authentication; otherwise C{None}.
@type pkey: L{PKey<pkey.PKey>}
+ @param gss_host: The host to authenticate to
+ @type gss_host: str
+ @param gss_auth: Enable or Disable GSSAPI Authentication
+ @type gss_auth: Boolean
+ @param gss_kex: Perform GSS-API Key Exchange and user authentication
+ @type gss_kex: Boolean
+ @param gss_deleg_creds: Use delegated credentails with GSSAPI
+ @type gss_deleg_cred: Boolean
@raise SSHException: if the SSH2 negotiation fails, the host key
supplied by the server is incorrect, or authentication fails.
@@ -1010,7 +1053,9 @@ class Transport (threading.Thread):
self.start_client()
# check host key if we were given one
- if (hostkey is not None):
+ # If GSS-API Key Exchange was performed, we are not required to check
+ # the host key.
+ if (hostkey is not None) and not gss_kex:
key = self.get_remote_server_key()
if (key.get_name() != hostkey.get_name()) or (key.asbytes() != hostkey.asbytes()):
self._log(DEBUG, 'Bad host key from server')
@@ -1019,13 +1064,19 @@ class Transport (threading.Thread):
raise SSHException('Bad host key from server')
self._log(DEBUG, 'Host key verified (%s)' % hostkey.get_name())
- if (pkey is not None) or (password is not None):
+ if (pkey is not None) or (password is not None) or gss_auth or gss_kex:
if password is not None:
self._log(DEBUG, 'Attempting password auth...')
self.auth_password(username, password)
- else:
+ elif pkey is not None:
self._log(DEBUG, 'Attempting public-key auth...')
self.auth_publickey(username, pkey)
+ elif gss_auth:
+ self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-with-mic)')
+ self.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds)
+ else:
+ self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-keyex)')
+ self.auth_gssapi_keyex(username)
return
@@ -1308,6 +1359,57 @@ class Transport (threading.Thread):
self.auth_handler.auth_interactive(username, handler, my_event, submethods)
return self.auth_handler.wait_for_response(my_event)
+ def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds):
+ """
+ Authenticate to the Server using GSS-API / SSPI.
+
+ @param username: The username to authenticate as
+ @type username: String
+ @param gss_host: The target host
+ @type gss_host: String
+ @param gss_deleg_creds: Delegate credentials or not
+ @type gss_deleg_creds: Boolean
+ @return: list of auth types permissible for the next stage of
+ authentication (normally empty)
+ @rtype: list
+ @raise BadAuthenticationType: if gssapi-with-mic isn't
+ allowed by the server (and no event was passed in)
+ @raise AuthenticationException: if the authentication failed (and no
+ event was passed in)
+ @raise SSHException: if there was a network error
+ """
+ if (not self.active) or (not self.initial_kex_done):
+ # we should never try to authenticate unless we're on a secure link
+ raise SSHException('No existing session')
+ my_event = threading.Event()
+ self.auth_handler = AuthHandler(self)
+ self.auth_handler.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds, my_event)
+ return self.auth_handler.wait_for_response(my_event)
+
+ def auth_gssapi_keyex(self, username):
+ """
+ Authenticate to the Server with GSS-API / SSPI if GSS-API Key Exchange
+ was the used key exchange method.
+
+ @param username: The username to authenticate as
+ @type username: String
+ @return: list of auth types permissible for the next stage of
+ authentication (normally empty)
+ @rtype: list
+ @raise BadAuthenticationType: if GSS-API Key Exchange was not performed
+ (and no event was passed in)
+ @raise AuthenticationException: if the authentication failed (and no
+ event was passed in)
+ @raise SSHException: if there was a network error
+ """
+ if (not self.active) or (not self.initial_kex_done):
+ # we should never try to authenticate unless we're on a secure link
+ raise SSHException('No existing session')
+ my_event = threading.Event()
+ self.auth_handler = AuthHandler(self)
+ self.auth_handler.auth_gssapi_keyex(username, my_event)
+ return self.auth_handler.wait_for_response(my_event)
+
def set_log_channel(self, name):
"""
Set the channel for this transport's logging. The default is
@@ -1582,7 +1684,7 @@ class Transport (threading.Thread):
if ptype not in self._expected_packet:
raise SSHException('Expecting packet from %r, got %d' % (self._expected_packet, ptype))
self._expected_packet = tuple()
- if (ptype >= 30) and (ptype <= 39):
+ if (ptype >= 30) and (ptype <= 41):
self.kex_engine.parse_next(ptype, m)
continue
diff --git a/sites/_shared_static/logo.png b/sites/_shared_static/logo.png
new file mode 100644
index 00000000..bc76697e
--- /dev/null
+++ b/sites/_shared_static/logo.png
Binary files differ
diff --git a/sites/shared_conf.py b/sites/shared_conf.py
new file mode 100644
index 00000000..86ecdfe8
--- /dev/null
+++ b/sites/shared_conf.py
@@ -0,0 +1,41 @@
+from datetime import datetime
+import os
+import sys
+
+import alabaster
+
+
+# Alabaster theme + mini-extension
+html_theme_path = [alabaster.get_path()]
+extensions = ['alabaster']
+# Paths relative to invoking conf.py - not this shared file
+html_static_path = ['../_shared_static']
+html_theme = 'alabaster'
+html_theme_options = {
+ 'description': "A Python implementation of SSHv2.",
+ 'github_user': 'paramiko',
+ 'github_repo': 'paramiko',
+ 'gittip_user': 'bitprophet',
+ 'analytics_id': 'UA-18486793-2',
+
+ 'link': '#3782BE',
+ 'link_hover': '#3782BE',
+}
+html_sidebars = {
+ '**': [
+ 'about.html',
+ 'navigation.html',
+ 'searchbox.html',
+ 'donate.html',
+ ]
+}
+
+# Regular settings
+project = u'Paramiko'
+year = datetime.now().year
+copyright = u'%d Jeff Forcier' % year
+master_doc = 'index'
+templates_path = ['_templates']
+exclude_trees = ['_build']
+source_suffix = '.rst'
+default_role = 'obj'
diff --git a/sites/www/_templates/rss.xml b/sites/www/_templates/rss.xml
new file mode 100644
index 00000000..f6f9cbd1
--- /dev/null
+++ b/sites/www/_templates/rss.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
+ <channel>
+ <atom:link href="{{ atom }}" rel="self" type="application/rss+xml" />
+ <title>{{ title }}</title>
+ <link>{{ link }}</link>
+ <description>{{ description }}</description>
+ <pubDate>{{ date }}</pubDate>
+ {% for link, title, desc, date in posts %}
+ <item>
+ <link>{{ link }}</link>
+ <guid>{{ link }}</guid>
+ <title><![CDATA[{{ title }}]]></title>
+ <description><![CDATA[{{ desc }}]]></description>
+ <pubDate>{{ date }}</pubDate>
+ </item>
+ {% endfor %}
+ </channel>
+</rss>
diff --git a/sites/www/blog.py b/sites/www/blog.py
new file mode 100644
index 00000000..3b129ebf
--- /dev/null
+++ b/sites/www/blog.py
@@ -0,0 +1,140 @@
+from collections import namedtuple
+from datetime import datetime
+import time
+import email.utils
+
+from sphinx.util.compat import Directive
+from docutils import nodes
+
+
+class BlogDateDirective(Directive):
+ """
+ Used to parse/attach date info to blog post documents.
+
+ No nodes generated, since none are needed.
+ """
+ has_content = True
+
+ def run(self):
+ # Tag parent document with parsed date value.
+ self.state.document.blog_date = datetime.strptime(
+ self.content[0], "%Y-%m-%d"
+ )
+ # Don't actually insert any nodes, we're already done.
+ return []
+
+class blog_post_list(nodes.General, nodes.Element):
+ pass
+
+class BlogPostListDirective(Directive):
+ """
+ Simply spits out a 'blog_post_list' temporary node for replacement.
+
+ Gets replaced at doctree-resolved time - only then will all blog post
+ documents be written out (& their date directives executed).
+ """
+ def run(self):
+ return [blog_post_list('')]
+
+
+Post = namedtuple('Post', 'name doc title date opener')
+
+def get_posts(app):
+ # Obtain blog posts
+ post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs)
+ posts = map(lambda x: (x, app.env.get_doctree(x)), post_names)
+ # Obtain common data used for list page & RSS
+ data = []
+ for post, doc in sorted(posts, key=lambda x: x[1].blog_date, reverse=True):
+ # Welp. No "nice" way to get post title. Thanks Sphinx.
+ title = doc[0][0][0]
+ # Date. This may or may not end up reflecting the required
+ # *input* format, but doing it here gives us flexibility.
+ date = doc.blog_date
+ # 1st paragraph as opener. TODO: allow a role or something marking
+ # where to actually pull from?
+ opener = doc.traverse(nodes.paragraph)[0]
+ data.append(Post(post, doc, title, date, opener))
+ return data
+
+def replace_blog_post_lists(app, doctree, fromdocname):
+ """
+ Replace blog_post_list nodes with ordered list-o-links to posts.
+ """
+ # Obtain blog posts
+ post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs)
+ posts = map(lambda x: (x, app.env.get_doctree(x)), post_names)
+ # Build "list" of links/etc
+ post_links = []
+ for post, doc, title, date, opener in get_posts(app):
+ # Link itself
+ uri = app.builder.get_relative_uri(fromdocname, post)
+ link = nodes.reference('', '', refdocname=post, refuri=uri)
+ # Title, bolded. TODO: use 'topic' or something maybe?
+ link.append(nodes.strong('', title))
+ date = date.strftime("%Y-%m-%d")
+ # Meh @ not having great docutils nodes which map to this.
+ html = '<div class="timestamp"><span>%s</span></div>' % date
+ timestamp = nodes.raw(text=html, format='html')
+ # NOTE: may group these within another element later if styling
+ # necessitates it
+ group = [timestamp, nodes.paragraph('', '', link), opener]
+ post_links.extend(group)
+
+ # Replace temp node(s) w/ expanded list-o-links
+ for node in doctree.traverse(blog_post_list):
+ node.replace_self(post_links)
+
+def rss_timestamp(timestamp):
+ # Use horribly inappropriate module for its magical daylight-savings-aware
+ # timezone madness. Props to Tinkerer for the idea.
+ return email.utils.formatdate(
+ time.mktime(timestamp.timetuple()),
+ localtime=True
+ )
+
+def generate_rss(app):
+ # Meh at having to run this subroutine like 3x per build. Not worth trying
+ # to be clever for now tho.
+ posts_ = get_posts(app)
+ # LOL URLs
+ root = app.config.rss_link
+ if not root.endswith('/'):
+ root += '/'
+ # Oh boy
+ posts = [
+ (
+ root + app.builder.get_target_uri(x.name),
+ x.title,
+ str(x.opener[0]), # Grab inner text element from paragraph
+ rss_timestamp(x.date),
+ )
+ for x in posts_
+ ]
+ location = 'blog/rss.xml'
+ context = {
+ 'title': app.config.project,
+ 'link': root,
+ 'atom': root + location,
+ 'description': app.config.rss_description,
+ # 'posts' is sorted by date already
+ 'date': rss_timestamp(posts_[0].date),
+ 'posts': posts,
+ }
+ yield (location, context, 'rss.xml')
+
+def setup(app):
+ # Link in RSS feed back to main website, e.g. 'http://paramiko.org'
+ app.add_config_value('rss_link', None, '')
+ # Ditto for RSS description field
+ app.add_config_value('rss_description', None, '')
+ # Interprets date metadata in blog post documents
+ app.add_directive('date', BlogDateDirective)
+ # Inserts blog post list node (in e.g. a listing page) for replacement
+ # below
+ app.add_node(blog_post_list)
+ app.add_directive('blog-posts', BlogPostListDirective)
+ # Performs abovementioned replacement
+ app.connect('doctree-resolved', replace_blog_post_lists)
+ # Generates RSS page from whole cloth at page generation step
+ app.connect('html-collect-pages', generate_rss)
diff --git a/sites/www/blog.rst b/sites/www/blog.rst
new file mode 100644
index 00000000..af9651e4
--- /dev/null
+++ b/sites/www/blog.rst
@@ -0,0 +1,16 @@
+====
+Blog
+====
+
+.. blog-posts directive gets replaced with an ordered list of blog posts.
+
+.. blog-posts::
+
+
+.. The following toctree ensures blog posts get processed.
+
+.. toctree::
+ :hidden:
+ :glob:
+
+ blog/*
diff --git a/sites/www/blog/first-post.rst b/sites/www/blog/first-post.rst
new file mode 100644
index 00000000..7b075073
--- /dev/null
+++ b/sites/www/blog/first-post.rst
@@ -0,0 +1,7 @@
+===========
+First post!
+===========
+
+A blog post.
+
+.. date:: 2013-12-04
diff --git a/sites/www/blog/second-post.rst b/sites/www/blog/second-post.rst
new file mode 100644
index 00000000..c4463f33
--- /dev/null
+++ b/sites/www/blog/second-post.rst
@@ -0,0 +1,7 @@
+===========
+Another one
+===========
+
+.. date:: 2013-12-05
+
+Indeed!
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
new file mode 100644
index 00000000..2680086e
--- /dev/null
+++ b/sites/www/changelog.rst
@@ -0,0 +1,114 @@
+=========
+Changelog
+=========
+
+* :feature:`250` GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and user authentication.
+* :bug:`193` (and its attentant PRs :issue:`230` & :issue:`253`): Fix SSH agent
+ problems present on Windows. Thanks to David Hobbs for initial report and to
+ Aarni Koskela & Olle Lundberg for the patches.
+* :release:`1.12.1 <2014-01-08>`
+* :release:`1.11.3 <2014-01-08>` 176
+* :release:`1.10.5 <2014-01-08>` 176
+* :bug:`225` Note ecdsa requirement in README. Thanks to Amaury Rodriguez for
+ the catch.
+* :bug:`176` Fix AttributeError bugs in known_hosts file (re)loading. Thanks
+ to Nathan Scowcroft for the patch & Martin Blumenstingl for the initial test
+ case.
+* :release:`1.12.0 <2013-09-27>`
+* :release:`1.11.2 <2013-09-27>`
+* :release:`1.10.4 <2013-09-27>` 199, 200, 179
+* :feature:`152` Add tentative support for ECDSA keys. *This adds the ecdsa
+ module as a new dependency of Paramiko.* The module is available at
+ [warner/python-ecdsa on Github](https://github.com/warner/python-ecdsa) and
+ [ecdsa on PyPI](https://pypi.python.org/pypi/ecdsa).
+
+ * Note that you might still run into problems with key negotiation --
+ Paramiko picks the first key that the server offers, which might not be
+ what you have in your known_hosts file.
+ * Mega thanks to Ethan Glasser-Camp for the patch.
+
+* :feature:`136` Add server-side support for the SSH protocol's 'env' command.
+ Thanks to Benjamin Pollack for the patch.
+* :bug:`156` Fix potential deadlock condition when using Channel objects as
+ sockets (e.g. when using SSH gatewaying). Thanks to Steven Noonan and Frank
+ Arnold for catch & patch.
+* :bug:`179` Fix a missing variable causing errors when an ssh_config file has
+ a non-default AddressFamily set. Thanks to Ed Marshall & Tomaz Muraus for
+ catch & patch.
+* :bug:`200` Fix an exception-causing typo in ``demo_simple.py``. Thanks to Alex
+ Buchanan for catch & Dave Foster for patch.
+* :bug:`199` Typo fix in the license header cross-project. Thanks to Armin
+ Ronacher for catch & patch.
+* :release:`1.11.1 <2013-09-20>`
+* :release:`1.10.3 <2013-09-20>`
+* :bug:`162` Clean up HMAC module import to avoid deadlocks in certain uses of
+ SSHClient. Thanks to Gernot Hillier for the catch & suggested fix.
+* :bug:`36` Fix the port-forwarding demo to avoid file descriptor errors.
+ Thanks to Jonathan Halcrow for catch & patch.
+* :bug:`168` Update config handling to properly handle multiple 'localforward'
+ and 'remoteforward' keys. Thanks to Emre Yılmaz for the patch.
+* :release:`1.11.0 <2013-07-26>`
+* :release:`1.10.2 <2013-07-26>`
+* :bug:`98 major` On Windows, when interacting with the PuTTY PAgeant, Paramiko
+ now creates the shared memory map with explicit Security Attributes of the
+ user, which is the same technique employed by the canonical PuTTY library to
+ avoid permissions issues when Paramiko is running under a different UAC
+ context than the PuTTY Ageant process. Thanks to Jason R. Coombs for the
+ patch.
+* :support:`100` Remove use of PyWin32 in ``win_pageant`` module. Module was
+ already dependent on ctypes for constructing appropriate structures and had
+ ctypes implementations of all functionality. Thanks to Jason R. Coombs for
+ the patch.
+* :bug:`87 major` Ensure updates to ``known_hosts`` files account for any
+ updates to said files after Paramiko initially read them. (Includes related
+ fix to guard against duplicate entries during subsequent ``known_hosts``
+ loads.) Thanks to ``@sunweaver`` for the contribution.
+* :bug:`153` (also :issue:`67`) Warn on parse failure when reading known_hosts
+ file. Thanks to ``@glasserc`` for patch.
+* :bug:`146` Indentation fixes for readability. Thanks to Abhinav Upadhyay for
+ catch & patch.
+* :release:`1.10.1 <2013-04-05>`
+* :bug:`142` (`Fabric #811 <https://github.com/fabric/fabric/issues/811>`_)
+ SFTP put of empty file will still return the attributes of the put file.
+ Thanks to Jason R. Coombs for the patch.
+* :bug:`154` (`Fabric #876 <https://github.com/fabric/fabric/issues/876>`_)
+ Forwarded SSH agent connections left stale local pipes lying around, which
+ could cause local (and sometimes remote or network) resource starvation when
+ running many agent-using remote commands. Thanks to Kevin Tegtmeier for catch
+ & patch.
+* :release:`1.10.0 <2013-03-01>`
+* :feature:`66` Batch SFTP writes to help speed up file transfers. Thanks to
+ Olle Lundberg for the patch.
+* :bug:`133 major` Fix handling of window-change events to be on-spec and not
+ attempt to wait for a response from the remote sshd; this fixes problems with
+ less common targets such as some Cisco devices. Thanks to Phillip Heller for
+ catch & patch.
+* :feature:`93` Overhaul SSH config parsing to be in line with ``man
+ ssh_config`` (& the behavior of ``ssh`` itself), including addition of parameter
+ expansion within config values. Thanks to Olle Lundberg for the patch.
+* :feature:`110` Honor SSH config ``AddressFamily`` setting when looking up
+ local host's FQDN. Thanks to John Hensley for the patch.
+* :feature:`128` Defer FQDN resolution until needed, when parsing SSH config
+ files. Thanks to Parantapa Bhattacharya for catch & patch.
+* :bug:`102 major` Forego random padding for packets when running under
+ ``*-ctr`` ciphers. This corrects some slowdowns on platforms where random
+ byte generation is inefficient (e.g. Windows). Thanks to ``@warthog618`` for
+ catch & patch, and Michael van der Kolff for code/technique review.
+* :feature:`127` Turn ``SFTPFile`` into a context manager. Thanks to Michael
+ Williamson for the patch.
+* :feature:`116` Limit ``Message.get_bytes`` to an upper bound of 1MB to protect
+ against potential DoS vectors. Thanks to ``@mvschaik`` for catch & patch.
+* :feature:`115` Add convenience ``get_pty`` kwarg to ``Client.exec_command`` so
+ users not manually controlling a channel object can still toggle PTY
+ creation. Thanks to Michael van der Kolff for the patch.
+* :feature:`71` Add ``SFTPClient.putfo`` and ``.getfo`` methods to allow direct
+ uploading/downloading of file-like objects. Thanks to Eric Buehl for the
+ patch.
+* :feature:`113` Add ``timeout`` parameter to ``SSHClient.exec_command`` for
+ easier setting of the command's internal channel object's timeout. Thanks to
+ Cernov Vladimir for the patch.
+* :support:`94` Remove duplication of SSH port constant. Thanks to Olle
+ Lundberg for the catch.
+* :feature:`80` Expose the internal "is closed" property of the file transfer
+ class ``BufferedFile`` as ``.closed``, better conforming to Python's file
+ interface. Thanks to ``@smunaut`` and James Hiscock for catch & patch.
diff --git a/sites/www/conf.py b/sites/www/conf.py
new file mode 100644
index 00000000..481acdff
--- /dev/null
+++ b/sites/www/conf.py
@@ -0,0 +1,35 @@
+# Obtain shared config values
+import sys
+import os
+from os.path import abspath, join, dirname
+
+sys.path.append(abspath(join(dirname(__file__), '..')))
+from shared_conf import *
+
+# Local blog extension
+sys.path.append(abspath('.'))
+extensions.append('blog')
+rss_link = 'http://paramiko.org'
+rss_description = 'Paramiko project news'
+
+# Releases changelog extension
+extensions.append('releases')
+releases_release_uri = "https://github.com/paramiko/paramiko/tree/%s"
+releases_issue_uri = "https://github.com/paramiko/paramiko/issues/%s"
+
+# Intersphinx for referencing API/usage docs
+extensions.append('sphinx.ext.intersphinx')
+# Default is 'local' building, but reference the public docs site when building
+# under RTD.
+target = join(dirname(__file__), '..', 'docs', '_build')
+if os.environ.get('READTHEDOCS') == 'True':
+ # TODO: switch to docs.paramiko.org post go-live of sphinx API docs
+ target = 'http://paramiko-docs.readthedocs.org/en/latest/'
+#intersphinx_mapping = {
+# 'docs': (target, None),
+#}
+
+# Sister-site links to API docs
+html_theme_options['extra_nav_links'] = {
+ "API Docs": 'http://docs.paramiko.org',
+}
diff --git a/sites/www/contact.rst b/sites/www/contact.rst
new file mode 100644
index 00000000..2b6583f5
--- /dev/null
+++ b/sites/www/contact.rst
@@ -0,0 +1,11 @@
+=======
+Contact
+=======
+
+You can get in touch with the developer & user community in any of the
+following ways:
+
+* IRC: ``#paramiko`` on Freenode
+* Mailing list: ``paramiko@librelist.com`` (see `the LibreList homepage
+ <http://librelist.com>`_ for usage details).
+* This website - a blog section is forthcoming.
diff --git a/sites/www/contributing.rst b/sites/www/contributing.rst
new file mode 100644
index 00000000..2b752cc5
--- /dev/null
+++ b/sites/www/contributing.rst
@@ -0,0 +1,19 @@
+============
+Contributing
+============
+
+How to get the code
+===================
+
+Our primary Git repository is on Github at `paramiko/paramiko
+<https://github.com/paramiko/paramiko>`_; please follow their instructions for
+cloning to your local system. (If you intend to submit patches/pull requests,
+we recommend forking first, then cloning your fork. Github has excellent
+documentation for all this.)
+
+
+How to submit bug reports or new code
+=====================================
+
+Please see `this project-agnostic contribution guide
+<http://contribution-guide.org>`_ - we follow it explicitly.
diff --git a/sites/www/index.rst b/sites/www/index.rst
new file mode 100644
index 00000000..7fefedd2
--- /dev/null
+++ b/sites/www/index.rst
@@ -0,0 +1,38 @@
+Welcome to Paramiko!
+====================
+
+Paramiko is a Python (2.5+) implementation of the SSHv2 protocol [#]_,
+providing both client and server functionality. While it leverages a Python C
+extension for low level cryptography (`PyCrypto <http://pycrypto.org>`_),
+Paramiko itself is a pure Python interface around SSH networking concepts.
+
+This website covers project information for Paramiko such as the changelog,
+contribution guidelines, development roadmap, news/blog, and so forth. Detailed
+usage and API documentation can be found at our code documentation site,
+`docs.paramiko.org <http://docs.paramiko.org>`_.
+
+.. toctree::
+ changelog
+ installing
+ contributing
+ contact
+
+.. Hide blog in hidden toctree for now (to avoid warnings.)
+
+.. toctree::
+ :hidden:
+
+ blog
+
+
+.. rubric:: Footnotes
+
+.. [#]
+ SSH is defined in RFCs
+ `4251 <http://www.rfc-editor.org/rfc/rfc4251.txt>`_,
+ `4252 <http://www.rfc-editor.org/rfc/rfc4252.txt>`_,
+ `4253 <http://www.rfc-editor.org/rfc/rfc4253.txt>`_, and
+ `4254 <http://www.rfc-editor.org/rfc/rfc4254.txt>`_;
+ the primary working implementation of the protocol is the `OpenSSH project
+ <http://openssh.org>`_. Paramiko implements a large portion of the SSH
+ feature set, but there are occasional gaps.
diff --git a/sites/www/installing.rst b/sites/www/installing.rst
new file mode 100644
index 00000000..0d4dc1ac
--- /dev/null
+++ b/sites/www/installing.rst
@@ -0,0 +1,105 @@
+==========
+Installing
+==========
+
+Paramiko itself
+===============
+
+The recommended way to get Invoke is to **install the latest stable release**
+via `pip <http://pip-installer.org>`_::
+
+ $ pip install paramiko
+
+.. note::
+ Users who want the bleeding edge can install the development version via
+ ``pip install paramiko==dev``.
+
+We currently support **Python 2.5/2.6/2.7**, with support for Python 3 coming
+soon. Users on Python 2.4 or older are urged to upgrade. Paramiko *may* work on
+Python 2.4 still, but there is no longer any support guarantee.
+
+Paramiko has two dependencies: the pure-Python ECDSA module `ecdsa`, and the
+PyCrypto C extension. `ecdsa` is easily installable from wherever you
+obtained Paramiko's package; PyCrypto may require more work. Read on for
+details.
+
+PyCrypto
+========
+
+`PyCrypto <https://www.dlitz.net/software/pycrypto/>`_ provides the low-level
+(C-based) encryption algorithms we need to implement the SSH protocol. There
+are a couple gotchas associated with installing PyCrypto: its compatibility
+with Python's package tools, and the fact that it is a C-based extension.
+
+.. _pycrypto-and-pip:
+
+Possible gotcha on older Python and/or pip versions
+---------------------------------------------------
+
+We strongly recommend using ``pip`` to as it is newer and generally better than
+``easy_install``. However, a combination of bugs in specific (now rather old)
+versions of Python, ``pip`` and PyCrypto can prevent installation of PyCrypto.
+Specifically:
+
+* Python = 2.5.x
+* PyCrypto >= 2.1 (required for most modern versions of Paramiko)
+* ``pip`` < 0.8.1
+
+When all three criteria are met, you may encounter ``No such file or
+directory`` IOErrors when trying to ``pip install paramiko`` or ``pip install
+PyCrypto``.
+
+The fix is to make sure at least one of the above criteria is not met, by doing
+the following (in order of preference):
+
+* Upgrade to ``pip`` 0.8.1 or above, e.g. by running ``pip install -U pip``.
+* Upgrade to Python 2.6 or above.
+* Downgrade to Paramiko 1.7.6 or 1.7.7, which do not require PyCrypto >= 2.1,
+ and install PyCrypto 2.0.1 (the oldest version on PyPI which works with
+ Paramiko 1.7.6/1.7.7)
+
+
+C extension
+-----------
+
+Unless you are installing from a precompiled source such as a Debian apt
+repository or RedHat RPM, or using :ref:`pypm <pypm>`, you will also need the
+ability to build Python C-based modules from source in order to install
+PyCrypto. Users on **Unix-based platforms** such as Ubuntu or Mac OS X will
+need the traditional C build toolchain installed (e.g. Developer Tools / XCode
+Tools on the Mac, or the ``build-essential`` package on Ubuntu or Debian Linux
+-- basically, anything with ``gcc``, ``make`` and so forth) as well as the
+Python development libraries, often named ``python-dev`` or similar.
+
+For **Windows** users we recommend using :ref:`pypm`, installing a C
+development environment such as `Cygwin <http://cygwin.com>`_ or obtaining a
+precompiled Win32 PyCrypto package from `voidspace's Python modules page
+<http://www.voidspace.org.uk/python/modules.shtml#pycrypto>`_.
+
+.. note::
+ Some Windows users whose Python is 64-bit have found that the PyCrypto
+ dependency ``winrandom`` may not install properly, leading to ImportErrors.
+ In this scenario, you'll probably need to compile ``winrandom`` yourself
+ via e.g. MS Visual Studio. See `Fabric #194
+ <https://github.com/fabric/fabric/issues/194>`_ for info.
+
+
+.. _pypm:
+
+ActivePython and PyPM
+=====================
+
+Windows users who already have ActiveState's `ActivePython
+<http://www.activestate.com/activepython/downloads>`_ distribution installed
+may find Paramiko is best installed with `its package manager, PyPM
+<http://code.activestate.com/pypm/>`_. Below is example output from an
+installation of Paramiko via ``pypm``::
+
+ C:\> pypm install paramiko
+ The following packages will be installed into "%APPDATA%\Python" (2.7):
+ paramiko-1.7.8 pycrypto-2.4
+ Get: [pypm-free.activestate.com] paramiko 1.7.8
+ Get: [pypm-free.activestate.com] pycrypto 2.4
+ Installing paramiko-1.7.8
+ Installing pycrypto-2.4
+ C:\>
diff --git a/test.py b/test.py
index bd966d1e..a954ef27 100755
--- a/test.py
+++ b/test.py
@@ -44,6 +44,22 @@ from tests.test_packetizer import PacketizerTest
from tests.test_auth import AuthTest
from tests.test_transport import TransportTest
from tests.test_client import SSHClientTest
+from test_message import MessageTest
+from test_file import BufferedFileTest
+from test_buffered_pipe import BufferedPipeTest
+from test_util import UtilTest
+from test_hostkeys import HostKeysTest
+from test_pkey import KeyTest
+from test_kex import KexTest
+from test_packetizer import PacketizerTest
+from test_auth import AuthTest
+from test_transport import TransportTest
+from test_sftp import SFTPTest
+from test_sftp_big import BigSFTPTest
+from test_client import SSHClientTest
+from test_gssapi import GSSAPITest
+from test_ssh_gss import GSSAuthTest
+from test_kex_gss import GSSKexTest
default_host = 'localhost'
default_user = os.environ.get('USER', 'nobody')
@@ -85,6 +101,17 @@ def main():
help='skip SFTP client/server tests, which can be slow')
parser.add_option('--no-big-file', action='store_false', dest='use_big_file', default=True,
help='skip big file SFTP tests, which are slow as molasses')
+ parser.add_option('--gssapi-test', action='store_true', dest='gssapi_test', default=False,
+ help='Test the used APIs for GSS-API / SSPI authentication')
+ parser.add_option('--test-gssauth', action='store_true', dest='test_gssauth', default=False,
+ help='Test GSS-API / SSPI authentication for SSHv2. To test this, you need kerberos a infrastructure.\
+ Note: Paramiko needs access to your krb5.keytab file. Make it readable for Paramiko or\
+ copy the used key to another file and set the environment variable KRB5_KTNAME to this file.')
+ parser.add_option('--test-gssapi-keyex', action='store_true', dest='test_gsskex', default=False,
+ help='Test GSS-API / SSPI authenticated iffie-Hellman Key Exchange and user\
+ authentication. To test this, you need kerberos a infrastructure.\
+ Note: Paramiko needs access to your krb5.keytab file. Make it readable for Paramiko or\
+ copy the used key to another file and set the environment variable KRB5_KTNAME to this file.')
parser.add_option('-R', action='store_false', dest='use_loopback_sftp', default=True,
help='perform SFTP tests against a remote server (by default, SFTP tests ' +
'are done through a loopback socket)')
@@ -101,6 +128,16 @@ def main():
parser.add_option('-P', '--sftp-passwd', dest='password', type='string', default=default_passwd,
metavar='<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 +173,15 @@ def main():
suite.addTest(unittest.makeSuite(SFTPTest))
if options.use_big_file:
suite.addTest(unittest.makeSuite(BigSFTPTest))
+ if options.gssapi_test:
+ GSSAPITest.init(options.targ_name, options.server_mode)
+ suite.addTest(unittest.makeSuite(GSSAPITest))
+ if options.test_gssauth:
+ GSSAuthTest.init(options.krb5_principal, options.targ_name)
+ suite.addTest(unittest.makeSuite(GSSAuthTest))
+ if options.test_gsskex:
+ GSSKexTest.init(options.krb5_principal, options.targ_name)
+ suite.addTest(unittest.makeSuite(GSSKexTest))
verbosity = 1
if options.verbose:
verbosity = 2
diff --git a/tests/test_gssapi.py b/tests/test_gssapi.py
new file mode 100644
index 00000000..e9ef99a9
--- /dev/null
+++ b/tests/test_gssapi.py
@@ -0,0 +1,167 @@
+# Copyright (C) 2013-2014 science + computing ag
+# Author: Sebastian Deiss <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()