diff --git a/Makefile b/Makefile index c4d3873..593dcde 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,9 @@ RELEASE=charmander release: python ./setup.py sdist --formats=zip -docs: +docs: always epydoc -o docs/ paramiko +always: # places where the version number is stored: # diff --git a/demo_server.py b/demo_server.py index 7fd25ad..65b45cf 100755 --- a/demo_server.py +++ b/demo_server.py @@ -1,6 +1,6 @@ #!/usr/bin/python -import sys, os, socket, threading, logging, traceback +import sys, os, socket, threading, logging, traceback, base64 import paramiko # setup logging @@ -18,10 +18,14 @@ if len(l.handlers) == 0: host_key = paramiko.DSSKey() host_key.read_private_key_file('demo_dss_key') -print 'Read key: ' + paramiko.hexify(host_key.get_fingerprint()) +print 'Read key: ' + paramiko.util.hexify(host_key.get_fingerprint()) class ServerTransport(paramiko.Transport): + # 'data' is the output of base64.encodestring(str(key)) + data = 'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hpfAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMCKDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iTUWT10hcuO4Ks8=' + good_pub_key = paramiko.RSAKey(data=base64.decodestring(data)) + def check_channel_request(self, kind, chanid): if kind == 'session': return ServerChannel(chanid) @@ -32,6 +36,11 @@ class ServerTransport(paramiko.Transport): return self.AUTH_SUCCESSFUL return self.AUTH_FAILED + def check_auth_publickey(self, username, key): + if (username == 'robey') and (key == self.good_pub_key): + return self.AUTH_SUCCESSFUL + return self.AUTH_FAILED + class ServerChannel(paramiko.Channel): "Channel descendant that pretends to understand pty and shell requests" @@ -79,11 +88,13 @@ try: t.add_server_key(host_key) t.ultra_debug = 0 t.start_server(event) - # print repr(t) - event.wait(10) - if not t.is_active(): - print '*** SSH negotiation failed.' - sys.exit(1) + while 1: + event.wait(0.1) + if not t.is_active(): + print '*** SSH negotiation failed.' + sys.exit(1) + if event.isSet(): + break # print repr(t) # wait for auth diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 81d41ed..7653161 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -41,5 +41,5 @@ class DSSKey (dsskey.DSSKey): __all__ = [ 'Transport', 'Channel', 'RSAKey', 'DSSKey', 'transport', - 'auth_transport', 'channel', 'rsakey', 'ddskey', 'util', + 'auth_transport', 'channel', 'rsakey', 'dsskey', 'util', 'SSHException' ] diff --git a/paramiko/auth_transport.py b/paramiko/auth_transport.py index 34f1174..5c4516f 100644 --- a/paramiko/auth_transport.py +++ b/paramiko/auth_transport.py @@ -13,7 +13,10 @@ _DISCONNECT_SERVICE_NOT_AVAILABLE, _DISCONNECT_AUTH_CANCELLED_BY_USER, \ class Transport (BaseTransport): - "BaseTransport with the auth framework hooked up" + """ + Subclass of L{BaseTransport} that handles authentication. This separation + keeps either class file from being too unwieldy. + """ AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED = range(3) @@ -53,15 +56,23 @@ class Transport (BaseTransport): """ return self.authenticated and self.active - def _request_auth(self): - m = Message() - m.add_byte(chr(_MSG_SERVICE_REQUEST)) - m.add_string('ssh-userauth') - self._send_message(m) - def auth_key(self, username, key, event): + """ + Authenticate to the server using a private key. The key is used to + sign data from the server, so it must include the private part. The + given L{event} is triggered on success or failure. On success, + L{is_authenticated} will return C{True}. + + @param username: the username to authenticate as. + @type username: string + @param key: the private key to authenticate with. + @type key: L{PKey } + @param event: an event to trigger when the authentication attempt is + complete (whether it was successful or not) + @type event: threading.Event + """ if (not self.active) or (not self.initial_kex_done): - # we should never try to send the password unless we're on a secure link + # we should never try to authenticate unless we're on a secure link raise SSHException('No existing session') try: self.lock.acquire() @@ -74,7 +85,20 @@ class Transport (BaseTransport): self.lock.release() def auth_password(self, username, password, event): - 'authenticate using a password; event is triggered on success or fail' + """ + Authenticate to the server using a password. The username and password + are sent over an encrypted link, and the given L{event} is triggered on + success or failure. On success, L{is_authenticated} will return + C{True}. + + @param username: the username to authenticate as. + @type username: string + @param password: the password to authenticate with. + @type password: string + @param event: an event to trigger when the authentication attempt is + complete (whether it was successful or not) + @type event: threading.Event + """ if (not self.active) or (not self.initial_kex_done): # we should never try to send the password unless we're on a secure link raise SSHException('No existing session') @@ -88,7 +112,58 @@ class Transport (BaseTransport): finally: self.lock.release() - def disconnect_service_not_available(self): + def get_allowed_auths(self, username): + "override me!" + return 'password' + + def check_auth_none(self, username): + "override me! return int ==> auth status" + return self.AUTH_FAILED + + def check_auth_password(self, username, password): + "override me! return int ==> auth status" + return self.AUTH_FAILED + + def check_auth_publickey(self, username, key): + """ + I{(subclass override)} + Determine if a given key supplied by the client is acceptable for use + in authentication. You should override this method in server mode to + check the username and key and decide if you would accept a signature + made using this key. + + Return C{AUTH_FAILED} if the key is not accepted, C{AUTH_SUCCESSFUL} + if the key is accepted and completes the authentication, or + C{AUTH_PARTIALLY_SUCCESSFUL} if your authentication is stateful, and + this key is accepted for authentication, but more authentication is + required. (In this latter case, L{get_allowed_auths} will be called + to report to the client what options it has for continuing the + authentication.) + + The default implementation always returns C{AUTH_FAILED}. + + @param username: the username of the authenticating client. + @type username: string + @param key: the key object provided by the client. + @type key: L{PKey } + @return: C{AUTH_FAILED} if the client can't authenticate with this key; + C{AUTH_SUCCESSFUL} if it can; C{AUTH_PARTIALLY_SUCCESSFUL} if it can + authenticate with this key but must continue with authentication. + @rtype: int + """ + return self.AUTH_FAILED + + + ### internals... + + + def _request_auth(self): + m = Message() + m.add_byte(chr(_MSG_SERVICE_REQUEST)) + m.add_string('ssh-userauth') + self._send_message(m) + + def _disconnect_service_not_available(self): m = Message() m.add_byte(chr(_MSG_DISCONNECT)) m.add_int(_DISCONNECT_SERVICE_NOT_AVAILABLE) @@ -97,7 +172,7 @@ class Transport (BaseTransport): self._send_message(m) self.close() - def disconnect_no_more_auth(self): + def _disconnect_no_more_auth(self): m = Message() m.add_byte(chr(_MSG_DISCONNECT)) m.add_int(_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) @@ -106,7 +181,19 @@ class Transport (BaseTransport): self._send_message(m) self.close() - def parse_service_request(self, m): + def _get_session_blob(self, key, service, username): + m = Message() + m.add_string(self.session_id) + m.add_byte(chr(_MSG_USERAUTH_REQUEST)) + m.add_string(username) + m.add_string(service) + m.add_string('publickey') + m.add_boolean(1) + m.add_string(key.get_name()) + m.add_string(str(key)) + return str(m) + + def _parse_service_request(self, m): service = m.get_string() if self.server_mode and (service == 'ssh-userauth'): # accepted @@ -116,9 +203,9 @@ class Transport (BaseTransport): self._send_message(m) return # dunno this one - self.disconnect_service_not_available() + self._disconnect_service_not_available() - def parse_service_accept(self, m): + def _parse_service_accept(self, m): service = m.get_string() if service == 'ssh-userauth': self._log(DEBUG, 'userauth is OK') @@ -134,30 +221,16 @@ class Transport (BaseTransport): m.add_boolean(1) m.add_string(self.private_key.get_name()) m.add_string(str(self.private_key)) - m.add_string(self.private_key.sign_ssh_session(self.randpool, self.H, self.username)) + blob = self._get_session_blob(self.private_key, 'ssh-connection', self.username) + sig = self.private_key.sign_ssh_data(self.randpool, blob) + m.add_string(str(sig)) else: raise SSHException('Unknown auth method "%s"' % self.auth_method) self._send_message(m) else: self._log(DEBUG, 'Service request "%s" accepted (?)' % service) - def get_allowed_auths(self, username): - "override me!" - return 'password' - - def check_auth_none(self, username): - "override me! return int ==> auth status" - return self.AUTH_FAILED - - def check_auth_password(self, username, password): - "override me! return int ==> auth status" - return self.AUTH_FAILED - - def check_auth_publickey(self, username, key): - "override me! return int ==> auth status" - return self.AUTH_FAILED - - def parse_userauth_request(self, m): + def _parse_userauth_request(self, m): if not self.server_mode: # er, uh... what? m = Message() @@ -174,11 +247,11 @@ class Transport (BaseTransport): method = m.get_string() self._log(DEBUG, 'Auth request (type=%s) service=%s, username=%s' % (method, service, username)) if service != 'ssh-connection': - self.disconnect_service_not_available() + self._disconnect_service_not_available() return if (self.auth_username is not None) and (self.auth_username != username): self._log(DEBUG, 'Auth rejected because the client attempted to change username in mid-flight') - self.disconnect_no_more_auth() + self._disconnect_no_more_auth() return if method == 'none': result = self.check_auth_none(username) @@ -194,8 +267,32 @@ class Transport (BaseTransport): else: result = self.check_auth_password(username, password) elif method == 'publickey': - # FIXME - result = self.check_auth_none(username) + sig_attached = m.get_boolean() + keytype = m.get_string() + keyblob = m.get_string() + key = self._key_from_blob(keytype, keyblob) + if (key is None) or (not key.valid): + self._log(DEBUG, 'Auth rejected: unsupported or mangled public key') + self._disconnect_no_more_auth() + return + # first check if this key is okay... if not, we can skip the verify + result = self.check_auth_publickey(username, key) + if result != self.AUTH_FAILED: + # key is okay, verify it + if not sig_attached: + # client wants to know if this key is acceptable, before it + # signs anything... send special "ok" message + m = Message() + m.add_byte(chr(_MSG_USERAUTH_PK_OK)) + m.add_string(keytype) + m.add_string(keyblob) + self._send_message(m) + return + sig = Message(m.get_string()) + blob = self._get_session_blob(key, service, username) + if not key.verify_ssh_sig(blob, sig): + self._log(DEBUG, 'Auth rejected: invalid signature') + result = self.AUTH_FAILED else: result = self.check_auth_none(username) # okay, send result @@ -215,15 +312,15 @@ class Transport (BaseTransport): self.auth_fail_count += 1 self._send_message(m) if self.auth_fail_count >= 10: - self.disconnect_no_more_auth() + self._disconnect_no_more_auth() - def parse_userauth_success(self, m): + def _parse_userauth_success(self, m): self._log(INFO, 'Authentication successful!') self.authenticated = True if self.auth_event != None: self.auth_event.set() - def parse_userauth_failure(self, m): + def _parse_userauth_failure(self, m): authlist = m.get_list() partial = m.get_boolean() if partial: @@ -237,7 +334,7 @@ class Transport (BaseTransport): if self.auth_event != None: self.auth_event.set() - def parse_userauth_banner(self, m): + def _parse_userauth_banner(self, m): banner = m.get_string() lang = m.get_string() self._log(INFO, 'Auth banner: ' + banner) @@ -245,11 +342,11 @@ class Transport (BaseTransport): _handler_table = BaseTransport._handler_table.copy() _handler_table.update({ - _MSG_SERVICE_REQUEST: parse_service_request, - _MSG_SERVICE_ACCEPT: parse_service_accept, - _MSG_USERAUTH_REQUEST: parse_userauth_request, - _MSG_USERAUTH_SUCCESS: parse_userauth_success, - _MSG_USERAUTH_FAILURE: parse_userauth_failure, - _MSG_USERAUTH_BANNER: parse_userauth_banner, + _MSG_SERVICE_REQUEST: _parse_service_request, + _MSG_SERVICE_ACCEPT: _parse_service_accept, + _MSG_USERAUTH_REQUEST: _parse_userauth_request, + _MSG_USERAUTH_SUCCESS: _parse_userauth_success, + _MSG_USERAUTH_FAILURE: _parse_userauth_failure, + _MSG_USERAUTH_BANNER: _parse_userauth_banner, }) diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index 3eca858..e0006e5 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -3,7 +3,6 @@ import base64 from ssh_exception import SSHException from message import Message -from transport import _MSG_USERAUTH_REQUEST from util import inflate_long, deflate_long from Crypto.PublicKey import DSA from Crypto.Hash import SHA @@ -14,9 +13,11 @@ from util import format_binary class DSSKey (PKey): - def __init__(self, msg=None): + def __init__(self, msg=None, data=None): self.valid = 0 - if (msg == None) or (msg.get_string() != 'ssh-dss'): + if (msg is None) and (data is not None): + msg = Message(data) + if (msg is None) or (msg.get_string() != 'ssh-dss'): return self.p = msg.get_mpint() self.q = msg.get_mpint() @@ -36,9 +37,33 @@ class DSSKey (PKey): m.add_mpint(self.y) return str(m) + def __hash__(self): + h = hash(self.get_name()) + h = h * 37 + hash(self.p) + h = h * 37 + hash(self.q) + h = h * 37 + hash(self.g) + h = h * 37 + hash(self.y) + # h might be a long by now... + return hash(h) + def get_name(self): return 'ssh-dss' + def sign_ssh_data(self, randpool, data): + hash = SHA.new(data).digest() + dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x))) + # generate a suitable k + qsize = len(deflate_long(self.q, 0)) + while 1: + k = inflate_long(randpool.get_bytes(qsize), 1) + if (k > 2) and (k < self.q): + break + r, s = dss.sign(inflate_long(hash, 1), k) + m = Message() + m.add_string('ssh-dss') + m.add_string(deflate_long(r, 0) + deflate_long(s, 0)) + return m + def verify_ssh_sig(self, data, msg): if not self.valid: return 0 @@ -59,21 +84,6 @@ class DSSKey (PKey): dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q))) return dss.verify(sigM, (sigR, sigS)) - def sign_ssh_data(self, randpool, data): - hash = SHA.new(data).digest() - dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x))) - # generate a suitable k - qsize = len(deflate_long(self.q, 0)) - while 1: - k = inflate_long(randpool.get_bytes(qsize), 1) - if (k > 2) and (k < self.q): - break - r, s = dss.sign(inflate_long(hash, 1), k) - m = Message() - m.add_string('ssh-dss') - m.add_string(deflate_long(r, 0) + deflate_long(s, 0)) - return str(m) - def read_private_key_file(self, filename): # private key file contains: # DSAPrivateKey = { version = 0, p, q, g, y, x } @@ -94,15 +104,3 @@ class DSSKey (PKey): self.x = keylist[5] self.size = len(deflate_long(self.p, 0)) self.valid = 1 - - def sign_ssh_session(self, randpool, sid, username): - m = Message() - m.add_string(sid) - m.add_byte(chr(_MSG_USERAUTH_REQUEST)) - m.add_string(username) - m.add_string('ssh-connection') - m.add_string('publickey') - m.add_boolean(1) - m.add_string('ssh-dss') - m.add_string(str(self)) - return self.sign_ssh_data(randpool, str(m)) diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index f14c786..3fcb8f2 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -145,7 +145,7 @@ class KexGex(object): m.add_byte(chr(_MSG_KEXDH_GEX_REPLY)) m.add_string(key) m.add_mpint(self.f) - m.add_string(sig) + m.add_string(str(sig)) self.transport._send_message(m) self.transport._activate_outbound() diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 3112326..1358513 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -97,6 +97,6 @@ class KexGroup1(object): m.add_byte(chr(_MSG_KEXDH_REPLY)) m.add_string(key) m.add_mpint(self.f) - m.add_string(sig) + m.add_string(str(sig)) self.transport._send_message(m) self.transport._activate_outbound() diff --git a/paramiko/pkey.py b/paramiko/pkey.py index c4c7599..6ad2845 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -7,28 +7,50 @@ class PKey (object): Base class for public keys. """ - def __init__(self, msg=None): + def __init__(self, msg=None, data=None): """ - Create a new instance of this public key type. If C{msg} is not - C{None}, the key's public part(s) will be filled in from the - message. + Create a new instance of this public key type. If C{msg} is given, + the key's public part(s) will be filled in from the message. If + C{data} is given, the key's public part(s) will be filled in from + the string. @param msg: an optional SSH L{Message} containing a public key of this type. @type msg: L{Message} + @param data: an optional string containing a public key of this type + @type data: string """ pass def __str__(self): """ Return a string of an SSH L{Message} made up of the public part(s) of - this key. + this key. This string is suitable for passing to L{__init__} to + re-create the key object later. @return: string representation of an SSH key message. @rtype: string """ return '' + def __cmp__(self, other): + """ + Compare this key to another. Returns 0 if this key is equivalent to + the given key, or non-0 if they are different. Only the public parts + of the key are compared, so a public key will compare equal to its + corresponding private key. + + @param other: key to compare to. + @type other: L{PKey} + @return: 0 if the two keys are equivalent, non-0 otherwise. + @rtype: int + """ + hs = hash(self) + ho = hash(other) + if hs != ho: + return cmp(hs, ho) + return cmp(str(self), str(other)) + def get_name(self): """ Return the name of this private key implementation. @@ -50,6 +72,20 @@ class PKey (object): """ return MD5.new(str(self)).digest() + def sign_ssh_data(self, randpool, data): + """ + Sign a blob of data with this private key, and return a L{Message} + representing an SSH signature message. + + @param randpool: a secure random number generator. + @type randpool: L{Crypto.Util.randpool.RandomPool} + @param data: the data to sign. + @type data: string + @return: an SSH signature message. + @rtype: L{Message} + """ + return '' + def verify_ssh_sig(self, data, msg): """ Given a blob of data, and an SSH message representing a signature of @@ -65,23 +101,6 @@ class PKey (object): """ return False - def sign_ssh_data(self, randpool, data): - """ - Sign a blob of data with this private key, and return a string - representing an SSH signature message. - - @bug: It would be cleaner for this method to return a L{Message} - object, so it would be complementary to L{verify_ssh_sig}. FIXME. - - @param randpool: a secure random number generator. - @type randpool: L{Crypto.Util.randpool.RandomPool} - @param data: the data to sign. - @type data: string - @return: string representation of an SSH signature message. - @rtype: string - """ - return '' - def read_private_key_file(self, filename): """ Read private key contents from a file into this object. @@ -94,19 +113,3 @@ class PKey (object): @raise binascii.Error: on base64 decoding error """ pass - - def sign_ssh_session(self, randpool, sid, username): - """ - Sign an SSH authentication request. - - @bug: Same as L{sign_ssh_data} - - @param randpool: a secure random number generator. - @type randpool: L{Crypto.Util.randpool.RandomPool} - @param sid: the session ID given by the server - @type sid: string - @param username: the username to use in the authentication request - @type username: string - @return: string representation of an SSH signature message. - @rtype: string - """ diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index e0935d5..b797c6b 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -1,7 +1,6 @@ #!/usr/bin/python from message import Message -from transport import _MSG_USERAUTH_REQUEST from Crypto.PublicKey import RSA from Crypto.Hash import SHA from ber import BER @@ -11,9 +10,11 @@ import base64 class RSAKey (PKey): - def __init__(self, msg=None): + def __init__(self, msg=None, data=''): self.valid = 0 - if (msg == None) or (msg.get_string() != 'ssh-rsa'): + if (msg is None) and (data is not None): + msg = Message(data) + if (msg is None) or (msg.get_string() != 'ssh-rsa'): return self.e = msg.get_mpint() self.n = msg.get_mpint() @@ -29,6 +30,12 @@ class RSAKey (PKey): m.add_mpint(self.n) return str(m) + def __hash__(self): + h = hash(self.get_name()) + h = h * 37 + hash(self.e) + h = h * 37 + hash(self.n) + return hash(h) + def get_name(self): return 'ssh-rsa' @@ -41,6 +48,15 @@ class RSAKey (PKey): filler = '\xff' * (self.size - len(SHA1_DIGESTINFO) - len(data) - 3) return '\x00\x01' + filler + '\x00' + SHA1_DIGESTINFO + data + def sign_ssh_data(self, randpool, data): + hash = SHA.new(data).digest() + rsa = RSA.construct((long(self.n), long(self.e), long(self.d))) + sig = deflate_long(rsa.sign(self._pkcs1imify(hash), '')[0], 0) + m = Message() + m.add_string('ssh-rsa') + m.add_string(sig) + return m + def verify_ssh_sig(self, data, msg): if (not self.valid) or (msg.get_string() != 'ssh-rsa'): return False @@ -52,15 +68,6 @@ class RSAKey (PKey): rsa = RSA.construct((long(self.n), long(self.e))) return rsa.verify(hash, (sig,)) - def sign_ssh_data(self, randpool, data): - hash = SHA.new(data).digest() - rsa = RSA.construct((long(self.n), long(self.e), long(self.d))) - sig = deflate_long(rsa.sign(self._pkcs1imify(hash), '')[0], 0) - m = Message() - m.add_string('ssh-rsa') - m.add_string(sig) - return str(m) - def read_private_key_file(self, filename): # private key file contains: # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } @@ -82,16 +89,3 @@ class RSAKey (PKey): self.q = keylist[5] self.size = len(deflate_long(self.n, 0)) self.valid = 1 - - def sign_ssh_session(self, randpool, sid, username): - m = Message() - m.add_string(sid) - m.add_byte(chr(_MSG_USERAUTH_REQUEST)) - m.add_string(username) - m.add_string('ssh-connection') - m.add_string('publickey') - m.add_boolean(1) - m.add_string('ssh-rsa') - m.add_string(str(self)) - return self.sign_ssh_data(randpool, str(m)) - diff --git a/paramiko/transport.py b/paramiko/transport.py index 1a42fb7..cf914f7 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -155,6 +155,26 @@ class BaseTransport (threading.Thread): self.server_accepts = [ ] self.server_accept_cv = threading.Condition(self.lock) + def __repr__(self): + """ + Returns a string representation of this object, for debugging. + + @rtype: string + """ + if not self.active: + return '' + out = '} + """ try: return self.server_key_dict[self.host_key_type] except KeyError: @@ -228,37 +261,6 @@ class BaseTransport (threading.Thread): return False load_server_moduli = staticmethod(load_server_moduli) - def _get_modulus_pack(self): - "used by KexGex to find primes for group exchange" - return self._modulus_pack - - def __repr__(self): - """ - Returns a string representation of this object, for debugging. - - @rtype: string - """ - if not self.active: - return '' - out = '