diff --git a/MANIFEST b/MANIFEST index e53d8e7..cf55ca7 100644 --- a/MANIFEST +++ b/MANIFEST @@ -6,8 +6,8 @@ dsskey.py kex_gex.py kex_group1.py message.py +paramiko.py rsakey.py -secsh.py setup.py transport.py util.py diff --git a/Makefile b/Makefile index beb5526..a2b28ae 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,11 @@ # releases: # aerodactyl (13sep03) # bulbasaur -# charmander +# charmander (10nov03) -RELEASE=bulbasaur +RELEASE=charmander release: - mkdir ../secsh-$(RELEASE) - cp README ../secsh-$(RELEASE) - cp *.py ../secsh-$(RELEASE) - cd .. && zip -r secsh-$(RELEASE).zip secsh-$(RELEASE) - echo rm -rf ../secsh-$(RELEASE) - -py: python ./setup.py sdist --formats=zip # places where the version number is stored: diff --git a/NOTES b/NOTES index de3d2fc..dc92e61 100644 --- a/NOTES +++ b/NOTES @@ -1,6 +1,6 @@ +-------------------+ +-----------------+ -(Socket)InputStream ---> | secsh transport | <===> | secsh channel | +(Socket)InputStream ---> | ssh2 transport | <===> | ssh2 channel | (Socket)OutputStream --> | (auth, pipe) | N | (buffer) | +-------------------+ +-----------------+ @ feeder thread | | @@ -8,8 +8,8 @@ - feed into channel +---> OutputStream buffers -SIS <-- @ --> (parse, find chan) --> secsh chan: buffer <-- SSHInputStream -SSHOutputStream --> secsh chan --> secsh transport --> SOS [no thread] +SIS <-- @ --> (parse, find chan) --> ssh2 chan: buffer <-- SSHInputStream +SSHOutputStream --> ssh2 chan --> ssh2 transport --> SOS [no thread] diff --git a/README b/README index 0f33fe9..693d5b0 100644 --- a/README +++ b/README @@ -1,14 +1,15 @@ -secsh 0.1 +paramiko 0.1 "charmander" release, 10 nov 2003 (c) 2003 Robey Pointer -http://www.lag.net/~robey/secsh/ +http://www.lag.net/~robey/paramiko/ *** WHAT -secsh is a module for python 2.3 that implements the SSH2 protocol for secure +"paramiko" is a combination of the esperanto words for "paranoid" and "friend". +it's a module for python 2.3 that implements the SSH2 protocol for secure (encrypted and authenticated) connections to remote machines. unlike SSL (aka TLS), SSH2 protocol does not require heirarchical certificates signed by a powerful central authority. you may know SSH2 as the protocol that replaced @@ -22,7 +23,7 @@ key), and opening flow-controled "channels" to the server, which are returned as socket-like objects. you are responsible for verifying that the server's host key is the one you expected to see, and you have control over which kinds of encryption or hashing you prefer (if you care), but all of the heavy lifting -is done by the secsh module. +is done by the paramiko module. it is written entirely in python (no C or platform-dependent code) and is released under the GNU LGPL (lesser GPL). @@ -55,7 +56,7 @@ know these screwy steps. i just don't understand windows enough.]) *** DEMO the demo client (demo.py) is a raw implementation of the normal 'ssh' CLI tool. -while the secsh library should work on all platforms, the demo app will only +while the paramiko library should work on all platforms, the demo app will only run on posix, because it uses select. you can run demo.py with no arguments, or you can give a hostname (or @@ -65,9 +66,9 @@ the host keys from there, though it's easily confused. you can choose to authenticate with a password, or with an RSA or DSS key, but it can only read your private key file(s) if they're not password-protected. -the demo app leaves a logfile called "demo.log" so you can see what secsh +the demo app leaves a logfile called "demo.log" so you can see what paramiko logs as it works. but the most interesting part is probably the code itself, -which hopefully demonstrates how you can use the secsh library. +which hopefully demonstrates how you can use the paramiko library. there's also now a demo server (demo_server.py) which listens on port 2200 and accepts a login (robey/foo) and pretends to be a BBS, just to demonstrate diff --git a/auth_transport.py b/auth_transport.py index 13b4a23..854b358 100644 --- a/auth_transport.py +++ b/auth_transport.py @@ -4,7 +4,7 @@ from transport import BaseTransport from transport import MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT, MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, \ MSG_USERAUTH_SUCCESS, MSG_USERAUTH_BANNER from message import Message -from secsh import SecshException +from paramiko import SSHException from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \ @@ -34,7 +34,7 @@ class Transport(BaseTransport): def auth_key(self, username, key, 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 SecshException('No existing session') + raise SSHException('No existing session') try: self.lock.acquire() self.auth_event = event @@ -49,7 +49,7 @@ class Transport(BaseTransport): 'authenticate using a password; event is triggered on success or fail' 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 SecshException('No existing session') + raise SSHException('No existing session') try: self.lock.acquire() self.auth_event = event @@ -108,7 +108,7 @@ class Transport(BaseTransport): m.add_string(str(self.private_key)) m.add_string(self.private_key.sign_ssh_session(self.randpool, self.H, self.username)) else: - raise SecshException('Unknown auth method "%s"' % self.auth_method) + raise SSHException('Unknown auth method "%s"' % self.auth_method) self.send_message(m) else: self.log(DEBUG, 'Service request "%s" accepted (?)' % service) diff --git a/channel.py b/channel.py index 8bbe22a..de1e31e 100644 --- a/channel.py +++ b/channel.py @@ -1,5 +1,5 @@ from message import Message -from secsh import SecshException +from paramiko import SSHException from transport import MSG_CHANNEL_REQUEST, MSG_CHANNEL_CLOSE, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, \ MSG_CHANNEL_EOF, MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE @@ -15,7 +15,7 @@ def set_nonblocking(fd): class Channel(object): """ - Abstraction for a secsh channel. + Abstraction for an SSH2 channel. """ def __init__(self, chanid): @@ -31,11 +31,11 @@ class Channel(object): self.in_buffer_cv = threading.Condition(self.lock) self.out_buffer_cv = threading.Condition(self.lock) self.name = str(chanid) - self.logger = logging.getLogger('secsh.chan.' + str(chanid)) + self.logger = logging.getLogger('paramiko.chan.' + str(chanid)) self.pipe_rfd = self.pipe_wfd = None def __repr__(self): - out = '' + return '' def __iter__(self): return self diff --git a/demo.py b/demo.py index 50ae3f9..e34ad42 100755 --- a/demo.py +++ b/demo.py @@ -1,7 +1,7 @@ #!/usr/bin/python import sys, os, socket, threading, getpass, logging, time, base64, select, termios, tty, traceback -import secsh +import paramiko ##### utility functions @@ -31,7 +31,7 @@ def load_host_keys(): ##### main demo # setup logging -l = logging.getLogger("secsh") +l = logging.getLogger("paramiko") l.setLevel(logging.DEBUG) if len(l.handlers) == 0: f = open('demo.log', 'w') @@ -65,7 +65,7 @@ except Exception, e: try: event = threading.Event() - t = secsh.Transport(sock) + t = paramiko.Transport(sock) t.ultra_debug = 0 t.start_client(event) # print repr(t) @@ -103,7 +103,7 @@ try: auth = default_auth if auth == 'r': - key = secsh.RSAKey() + key = paramiko.RSAKey() default_path = os.environ['HOME'] + '/.ssh/id_rsa' path = raw_input('RSA key [%s]: ' % default_path) if len(path) == 0: @@ -111,7 +111,7 @@ try: key.read_private_key_file(path) t.auth_key(username, key, event) elif auth == 'd': - key = secsh.DSSKey() + key = paramiko.DSSKey() default_path = os.environ['HOME'] + '/.ssh/id_dsa' path = raw_input('DSS key [%s]: ' % default_path) if len(path) == 0: diff --git a/demo_server.py b/demo_server.py index 273bb79..0c7ec51 100755 --- a/demo_server.py +++ b/demo_server.py @@ -1,10 +1,10 @@ #!/usr/bin/python import sys, os, socket, threading, logging, traceback -import secsh +import paramiko # setup logging -l = logging.getLogger("secsh") +l = logging.getLogger("paramiko") l.setLevel(logging.DEBUG) if len(l.handlers) == 0: f = open('demo_server.log', 'w') @@ -12,11 +12,11 @@ if len(l.handlers) == 0: lh.setFormatter(logging.Formatter('%(levelname)-.3s [%(asctime)s] %(name)s: %(message)s', '%Y%m%d:%H%M%S')) l.addHandler(lh) -host_key = secsh.RSAKey() +host_key = paramiko.RSAKey() host_key.read_private_key_file('demo_host_key') -class ServerTransport(secsh.Transport): +class ServerTransport(paramiko.Transport): def check_channel_request(self, kind, chanid): if kind == 'session': return ServerChannel(chanid) @@ -27,11 +27,11 @@ class ServerTransport(secsh.Transport): return self.AUTH_SUCCESSFUL return self.AUTH_FAILED -class ServerChannel(secsh.Channel): +class ServerChannel(paramiko.Channel): "Channel descendant that pretends to understand pty and shell requests" def __init__(self, chanid): - secsh.Channel.__init__(self, chanid) + paramiko.Channel.__init__(self, chanid) self.event = threading.Event() def check_pty_request(self, term, width, height, pixelwidth, pixelheight, modes): diff --git a/kex_gex.py b/kex_gex.py index ef1bd9f..5fd6796 100644 --- a/kex_gex.py +++ b/kex_gex.py @@ -6,7 +6,7 @@ from message import Message from util import inflate_long, deflate_long, generate_prime, bit_length -from secsh import SecshException +from paramiko import SSHException from transport import MSG_NEWKEYS from Crypto.Hash import SHA from Crypto.Util import number @@ -49,7 +49,7 @@ class KexGex(object): return self.parse_kexdh_gex_init(m) elif ptype == MSG_KEXDH_GEX_REPLY: return self.parse_kexdh_gex_reply(m) - raise SecshException('KexGex asked to handle packet type %d' % ptype) + raise SSHException('KexGex asked to handle packet type %d' % ptype) def generate_x(self): # generate an "x" (1 < x < (p-1)/2). @@ -108,7 +108,7 @@ class KexGex(object): # reject if p's bit length < 1024 or > 8192 bitlen = bit_length(self.p) if (bitlen < 1024) or (bitlen > 8192): - raise SecshException('Server-generated gex p (don\'t ask) is out of range (%d bits)' % bitlen) + 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 @@ -122,7 +122,7 @@ class KexGex(object): def parse_kexdh_gex_init(self, m): self.e = m.get_mpint() if (self.e < 1) or (self.e > self.p - 1): - raise SecshException('Client kex "e" is out of range') + raise SSHException('Client kex "e" is out of range') self.generate_x() K = pow(self.e, self.x, P) key = str(self.transport.get_server_key()) @@ -154,7 +154,7 @@ class KexGex(object): self.f = m.get_mpint() sig = m.get_string() if (self.f < 1) or (self.f > self.p - 1): - raise SecshException('Server kex "f" is out of range') + 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().add(self.transport.local_version).add(self.transport.remote_version) diff --git a/kex_group1.py b/kex_group1.py index ab16ee4..b507d88 100644 --- a/kex_group1.py +++ b/kex_group1.py @@ -5,7 +5,7 @@ # "g" generator. from message import Message, inflate_long -from secsh import SecshException +from paramiko import SSHException from transport import MSG_NEWKEYS from Crypto.Hash import SHA from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL @@ -59,14 +59,14 @@ class KexGroup1(object): return self.parse_kexdh_init(m) elif not self.transport.server_mode and (ptype == MSG_KEXDH_REPLY): return self.parse_kexdh_reply(m) - raise SecshException('KexGroup1 asked to handle packet type %d' % ptype) + raise SSHException('KexGroup1 asked to handle packet type %d' % ptype) def parse_kexdh_reply(self, m): # client mode host_key = m.get_string() self.f = m.get_mpint() if (self.f < 1) or (self.f > P - 1): - raise SecshException('Server kex "f" is out of range') + raise SSHException('Server kex "f" is out of range') sig = m.get_string() K = pow(self.f, self.x, P) # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) @@ -82,7 +82,7 @@ class KexGroup1(object): # server mode self.e = m.get_mpint() if (self.e < 1) or (self.e > P - 1): - raise SecshException('Client kex "e" is out of range') + raise SSHException('Client kex "e" is out of range') K = pow(self.e, self.x, P) key = str(self.transport.get_server_key()) # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) diff --git a/message.py b/message.py index aeccd9f..0660fe6 100644 --- a/message.py +++ b/message.py @@ -1,11 +1,11 @@ -# implementation of a secsh "message" +# implementation of an SSH2 "message" import string, types, struct from util import inflate_long, deflate_long class Message(object): - "represents the encoding of a secsh message" + "represents the encoding of an SSH2 message" def __init__(self, content=''): self.packet = content diff --git a/secsh.py b/paramiko.py similarity index 93% rename from secsh.py rename to paramiko.py index 29342e8..2b18981 100644 --- a/secsh.py +++ b/paramiko.py @@ -5,7 +5,7 @@ import sys if (sys.version_info[0] < 2) or ((sys.version_info[0] == 2) and (sys.version_info[1] < 3)): raise RuntimeError('You need python 2.3 for this module.') -class SecshException(Exception): +class SSHException(Exception): pass diff --git a/setup.py b/setup.py index 9eb1c9d..1d49611 100644 --- a/setup.py +++ b/setup.py @@ -6,20 +6,22 @@ Emphasis is on using SSH2 as an alternative to SSL for making secure connections between pyton scripts. All major ciphers and hash methods are supported. +(Previous name: secsh) + Required packages: pyCrypto ''' -setup(name = "secsh", +setup(name = "paramiko", version = "0.1-charmander", description = "SSH2 protocol library", author = "Robey Pointer", author_email = "robey@lag.net", - url = "http://www.lag.net/~robey/secsh/", - py_modules = [ 'secsh', 'transport', 'auth_transport', 'channel', + url = "http://www.lag.net/~robey/paramiko/", + py_modules = [ 'paramiko', 'transport', 'auth_transport', 'channel', 'message', 'util', 'ber', 'kex_group1', 'kex_gex', 'rsakey', 'dsskey' ], - download_url = 'http://www.lag.net/~robey/secsh/secsh-0.1-charmander.zip', + download_url = 'http://www.lag.net/~robey/paramiko/paramiko-0.1-charmander.zip', license = 'LGPL', platforms = 'Posix; MacOS X; Windows', classifiers = [ 'Development Status :: 3 - Alpha', diff --git a/transport.py b/transport.py index 611e577..521dbcb 100644 --- a/transport.py +++ b/transport.py @@ -14,7 +14,7 @@ MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \ import sys, os, string, threading, socket, logging, struct from message import Message from channel import Channel -from secsh import SecshException +from paramiko import SSHException from util import format_binary, safe_string, inflate_long, deflate_long, tb_strings from rsakey import RSAKey from dsskey import DSSKey @@ -131,7 +131,7 @@ class BaseTransport(threading.Thread): self.channels = { } # (id -> Channel) self.channel_events = { } # (id -> Event) self.channel_counter = 1 - self.logger = logging.getLogger('secsh.transport') + self.logger = logging.getLogger('paramiko.transport') self.window_size = 65536 self.max_packet_size = 2048 self.ultra_debug = 0 @@ -167,7 +167,7 @@ class BaseTransport(threading.Thread): def __repr__(self): if not self.active: - return '' + return '' out = 'II', self.sequence_number_in, packet_size) + packet my_mac = HMAC.HMAC(self.mac_key_in, mac_payload, self.remote_mac_engine).digest()[:self.remote_mac_len] if my_mac != mac: - raise SecshException('Mismatched MAC') + raise SSHException('Mismatched MAC') padding = ord(packet[0]) payload = packet[1:packet_size - padding + 1] randpool.add_event(packet[packet_size - padding + 1]) @@ -360,7 +360,7 @@ class BaseTransport(threading.Thread): # comply, then just drop the connection self.received_packets_overflow += 1 if self.received_packets_overflow >= 20: - raise SecshException('Remote transport is ignoring rekey requests') + raise SSHException('Remote transport is ignoring rekey requests') return ord(payload[0]), msg @@ -379,9 +379,9 @@ class BaseTransport(threading.Thread): else: key = None if (key == None) or not key.valid: - raise SecshException('Unknown host key type') + raise SSHException('Unknown host key type') if not key.verify_ssh_sig(self.H, Message(sig)): - raise SecshException('Signature verification (%s) failed. Boo. Robey should debug this.' % self.host_key_type) + raise SSHException('Signature verification (%s) failed. Boo. Robey should debug this.' % self.host_key_type) self.host_key = host_key def compute_key(self, id, nbytes): @@ -404,7 +404,7 @@ class BaseTransport(threading.Thread): def get_cipher(self, name, key, iv): if not self.cipher_info.has_key(name): - raise SecshException('Unknown client cipher ' + name) + raise SSHException('Unknown client cipher ' + name) return self.cipher_info[name]['class'].new(key, self.cipher_info[name]['mode'], iv) def run(self): @@ -429,7 +429,7 @@ class BaseTransport(threading.Thread): continue if self.expected_packet != 0: if ptype != self.expected_packet: - raise SecshException('Expecting packet %d, got %d' % (self.expected_packet, ptype)) + raise SSHException('Expecting packet %d, got %d' % (self.expected_packet, ptype)) self.expected_packet = 0 if (ptype >= 30) and (ptype <= 39): self.kex_engine.parse_next(ptype, m) @@ -447,7 +447,7 @@ class BaseTransport(threading.Thread): msg.add_byte(chr(MSG_UNIMPLEMENTED)) msg.add_int(m.seqno) self.send_message(msg) - except SecshException, e: + except SSHException, e: self.log(DEBUG, 'Exception: ' + str(e)) self.log(DEBUG, tb_strings()) except EOFError, e: @@ -479,7 +479,7 @@ class BaseTransport(threading.Thread): return 1 def negotiate_keys(self, m): - # throws SecshException on anything unusual + # throws SSHException on anything unusual if self.local_kex_init == None: # remote side wants to renegotiate self.send_kex_init() @@ -499,7 +499,7 @@ class BaseTransport(threading.Thread): break self.log(DEBUG, 'Banner: ' + buffer) if buffer[:4] != 'SSH-': - raise SecshException('Indecipherable protocol version "' + buffer + '"') + raise SSHException('Indecipherable protocol version "' + buffer + '"') # save this server version string for later self.remote_version = buffer # pull off any attached comment @@ -511,11 +511,11 @@ class BaseTransport(threading.Thread): # parse out version string and make sure it matches segs = buffer.split('-', 2) if len(segs) < 3: - raise SecshException('Invalid SSH banner') + raise SSHException('Invalid SSH banner') version = segs[1] client = segs[2] if version != '1.99' and version != '2.0': - raise SecshException('Incompatible version (%s instead of 2.0)' % (version,)) + raise SSHException('Incompatible version (%s instead of 2.0)' % (version,)) self.log(INFO, 'Connected (version %s, client %s)' % (version, client)) def send_kex_init(self): @@ -566,7 +566,7 @@ class BaseTransport(threading.Thread): # no compression support (yet?) if (not('none' in client_compress_algo_list) or not('none' in server_compress_algo_list)): - raise SecshException('Incompatible ssh peer.') + raise SSHException('Incompatible ssh peer.') # as a server, we pick the first item in the client's list that we support. # as a client, we pick the first item in our list that the server supports. @@ -575,7 +575,7 @@ class BaseTransport(threading.Thread): else: agreed_kex = filter(kex_algo_list.__contains__, self.preferred_kex) if len(agreed_kex) == 0: - raise SecshException('Incompatible ssh peer (no acceptable kex algorithm)') + raise SSHException('Incompatible ssh peer (no acceptable kex algorithm)') self.kex_engine = self.kex_info[agreed_kex[0]](self) if self.server_mode: @@ -583,10 +583,10 @@ class BaseTransport(threading.Thread): else: agreed_keys = filter(server_key_algo_list.__contains__, self.preferred_keys) if len(agreed_keys) == 0: - raise SecshException('Incompatible ssh peer (no acceptable host key)') + raise SSHException('Incompatible ssh peer (no acceptable host key)') self.host_key_type = agreed_keys[0] if self.server_mode and (self.get_server_key() is None): - raise SecshException('Incompatible ssh peer (can\'t match requested host key type)') + raise SSHException('Incompatible ssh peer (can\'t match requested host key type)') if self.server_mode: agreed_local_ciphers = filter(self.preferred_ciphers.__contains__, @@ -599,7 +599,7 @@ class BaseTransport(threading.Thread): agreed_remote_ciphers = filter(server_encrypt_algo_list.__contains__, self.preferred_ciphers) if (len(agreed_local_ciphers) == 0) or (len(agreed_remote_ciphers) == 0): - raise SecshException('Incompatible ssh server (no acceptable ciphers)') + raise SSHException('Incompatible ssh server (no acceptable ciphers)') self.local_cipher = agreed_local_ciphers[0] self.remote_cipher = agreed_remote_ciphers[0] self.log(DEBUG, 'Ciphers agreed: local=%s, remote=%s' % (self.local_cipher, self.remote_cipher)) @@ -611,7 +611,7 @@ class BaseTransport(threading.Thread): agreed_local_macs = filter(client_mac_algo_list.__contains__, self.preferred_macs) agreed_remote_macs = filter(server_mac_algo_list.__contains__, self.preferred_macs) if (len(agreed_local_macs) == 0) or (len(agreed_remote_macs) == 0): - raise SecshException('Incompatible ssh server (no acceptable macs)') + raise SSHException('Incompatible ssh server (no acceptable macs)') self.local_mac = agreed_local_macs[0] self.remote_mac = agreed_remote_macs[0]