[project @ Arch-1:robey@lag.net--2003-public%secsh--dev--1.0--patch-7]
cleaned up server code, renamed some files & classes renamed demo-server.py and demo-host-key to demo_server.py and demo_host_key, just to be consistent. renamed SSHException -> SecshException. generalized the mechanism where Channel decides whether to allow different channel requests: 4 of the main ones (pty, window-change, shell, and subsystem) go through easily override-able methods now. you could probably make an actual ssh shell server. gave ChannelFile a repr(). turned off ultra debugging in the demos. demo_server creates a subclass of Channel to allow pty/shell and sets an event when the shell request is made, so that it knows when it can start sending the fake bbs. renamed to charmander and updated some of the distutils files.
This commit is contained in:
parent
0e1ef2c65c
commit
aad7b859f1
3
MANIFEST
3
MANIFEST
|
@ -1,4 +1,5 @@
|
||||||
README
|
README
|
||||||
|
auth_transport.py
|
||||||
ber.py
|
ber.py
|
||||||
channel.py
|
channel.py
|
||||||
dsskey.py
|
dsskey.py
|
||||||
|
@ -11,3 +12,5 @@ setup.py
|
||||||
transport.py
|
transport.py
|
||||||
util.py
|
util.py
|
||||||
demo.py
|
demo.py
|
||||||
|
demo_server.py
|
||||||
|
demo_host_key
|
||||||
|
|
3
NOTES
3
NOTES
|
@ -58,6 +58,9 @@ from Channel:
|
||||||
exec_command
|
exec_command
|
||||||
invoke_subsystem
|
invoke_subsystem
|
||||||
resize_pty
|
resize_pty
|
||||||
|
[server:]
|
||||||
|
check_pty_request
|
||||||
|
check_shell_request
|
||||||
|
|
||||||
from ChannelFile:
|
from ChannelFile:
|
||||||
next
|
next
|
||||||
|
|
|
@ -4,7 +4,7 @@ from transport import BaseTransport
|
||||||
from transport import MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT, MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, \
|
from transport import MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT, MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, \
|
||||||
MSG_USERAUTH_SUCCESS, MSG_USERAUTH_BANNER
|
MSG_USERAUTH_SUCCESS, MSG_USERAUTH_BANNER
|
||||||
from message import Message
|
from message import Message
|
||||||
from secsh import SSHException
|
from secsh import SecshException
|
||||||
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
|
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
|
|
||||||
DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \
|
DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \
|
||||||
|
@ -34,7 +34,7 @@ class Transport(BaseTransport):
|
||||||
def auth_key(self, username, key, event):
|
def auth_key(self, username, key, event):
|
||||||
if (not self.active) or (not self.initial_kex_done):
|
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 send the password unless we're on a secure link
|
||||||
raise SSHException('No existing session')
|
raise SecshException('No existing session')
|
||||||
try:
|
try:
|
||||||
self.lock.acquire()
|
self.lock.acquire()
|
||||||
self.auth_event = event
|
self.auth_event = event
|
||||||
|
@ -49,7 +49,7 @@ class Transport(BaseTransport):
|
||||||
'authenticate using a password; event is triggered on success or fail'
|
'authenticate using a password; event is triggered on success or fail'
|
||||||
if (not self.active) or (not self.initial_kex_done):
|
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 send the password unless we're on a secure link
|
||||||
raise SSHException('No existing session')
|
raise SecshException('No existing session')
|
||||||
try:
|
try:
|
||||||
self.lock.acquire()
|
self.lock.acquire()
|
||||||
self.auth_event = event
|
self.auth_event = event
|
||||||
|
@ -108,7 +108,7 @@ class Transport(BaseTransport):
|
||||||
m.add_string(str(self.private_key))
|
m.add_string(str(self.private_key))
|
||||||
m.add_string(self.private_key.sign_ssh_session(self.randpool, self.H, self.username))
|
m.add_string(self.private_key.sign_ssh_session(self.randpool, self.H, self.username))
|
||||||
else:
|
else:
|
||||||
raise SSHException('Unknown auth method "%s"' % self.auth_method)
|
raise SecshException('Unknown auth method "%s"' % self.auth_method)
|
||||||
self.send_message(m)
|
self.send_message(m)
|
||||||
else:
|
else:
|
||||||
self.log(DEBUG, 'Service request "%s" accepted (?)' % service)
|
self.log(DEBUG, 'Service request "%s" accepted (?)' % service)
|
||||||
|
|
57
channel.py
57
channel.py
|
@ -1,5 +1,5 @@
|
||||||
from message import Message
|
from message import Message
|
||||||
from secsh import SSHException
|
from secsh import SecshException
|
||||||
from transport import MSG_CHANNEL_REQUEST, MSG_CHANNEL_CLOSE, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, \
|
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
|
MSG_CHANNEL_EOF, MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE
|
||||||
|
|
||||||
|
@ -100,6 +100,22 @@ class Channel(object):
|
||||||
finally:
|
finally:
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
|
||||||
|
def check_pty_request(self, term, width, height, pixelwidth, pixelheight, modes):
|
||||||
|
"override me! return True if a pty of the given dimensions (for shell access, usually) can be provided"
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_shell_request(self):
|
||||||
|
"override me! return True if shell access will be provided"
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_subsystem_request(self, name):
|
||||||
|
"override me! return True if the given subsystem can be provided"
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_window_change_request(self, width, height, pixelwidth, pixelheight):
|
||||||
|
"override me! return True if the pty was resized"
|
||||||
|
return False
|
||||||
|
|
||||||
def handle_request(self, m):
|
def handle_request(self, m):
|
||||||
key = m.get_string()
|
key = m.get_string()
|
||||||
want_reply = m.get_boolean()
|
want_reply = m.get_boolean()
|
||||||
|
@ -110,10 +126,25 @@ class Channel(object):
|
||||||
elif key == 'xon-xoff':
|
elif key == 'xon-xoff':
|
||||||
# ignore
|
# ignore
|
||||||
ok = True
|
ok = True
|
||||||
elif (key == 'pty-req') or (key == 'shell'):
|
elif key == 'pty-req':
|
||||||
if self.transport.server_mode:
|
term = m.get_string()
|
||||||
# humor them
|
width = m.get_int()
|
||||||
ok = True
|
height = m.get_int()
|
||||||
|
pixelwidth = m.get_int()
|
||||||
|
pixelheight = m.get_int()
|
||||||
|
modes = m.get_string()
|
||||||
|
ok = self.check_pty_request(term, width, height, pixelwidth, pixelheight, modes)
|
||||||
|
elif key == 'shell':
|
||||||
|
ok = self.check_shell_request()
|
||||||
|
elif key == 'subsystem':
|
||||||
|
name = m.get_string()
|
||||||
|
ok = self.check_subsystem_request(name)
|
||||||
|
elif key == 'window-change':
|
||||||
|
width = m.get_int()
|
||||||
|
height = m.get_int()
|
||||||
|
pixelwidth = m.get_int()
|
||||||
|
pixelheight = m.get_int()
|
||||||
|
ok = self.check_window_change_request(width, height, pixelwidth, pixelheight)
|
||||||
else:
|
else:
|
||||||
self.log(DEBUG, 'Unhandled channel request "%s"' % key)
|
self.log(DEBUG, 'Unhandled channel request "%s"' % key)
|
||||||
ok = False
|
ok = False
|
||||||
|
@ -155,7 +186,7 @@ class Channel(object):
|
||||||
|
|
||||||
def get_pty(self, term='vt100', width=80, height=24):
|
def get_pty(self, term='vt100', width=80, height=24):
|
||||||
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
||||||
raise SSHException('Channel is not open')
|
raise SecshException('Channel is not open')
|
||||||
m = Message()
|
m = Message()
|
||||||
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
||||||
m.add_int(self.remote_chanid)
|
m.add_int(self.remote_chanid)
|
||||||
|
@ -171,7 +202,7 @@ class Channel(object):
|
||||||
|
|
||||||
def invoke_shell(self):
|
def invoke_shell(self):
|
||||||
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
||||||
raise SSHException('Channel is not open')
|
raise SecshException('Channel is not open')
|
||||||
m = Message()
|
m = Message()
|
||||||
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
||||||
m.add_int(self.remote_chanid)
|
m.add_int(self.remote_chanid)
|
||||||
|
@ -181,7 +212,7 @@ class Channel(object):
|
||||||
|
|
||||||
def exec_command(self, command):
|
def exec_command(self, command):
|
||||||
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
||||||
raise SSHException('Channel is not open')
|
raise SecshException('Channel is not open')
|
||||||
m = Message()
|
m = Message()
|
||||||
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
||||||
m.add_int(self.remote_chanid)
|
m.add_int(self.remote_chanid)
|
||||||
|
@ -192,7 +223,7 @@ class Channel(object):
|
||||||
|
|
||||||
def invoke_subsystem(self, subsystem):
|
def invoke_subsystem(self, subsystem):
|
||||||
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
||||||
raise SSHException('Channel is not open')
|
raise SecshException('Channel is not open')
|
||||||
m = Message()
|
m = Message()
|
||||||
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
||||||
m.add_int(self.remote_chanid)
|
m.add_int(self.remote_chanid)
|
||||||
|
@ -203,7 +234,7 @@ class Channel(object):
|
||||||
|
|
||||||
def resize_pty(self, width=80, height=24):
|
def resize_pty(self, width=80, height=24):
|
||||||
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
if self.closed or self.eof_received or self.eof_sent or not self.active:
|
||||||
raise SSHException('Channel is not open')
|
raise SecshException('Channel is not open')
|
||||||
m = Message()
|
m = Message()
|
||||||
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
m.add_byte(chr(MSG_CHANNEL_REQUEST))
|
||||||
m.add_int(self.remote_chanid)
|
m.add_int(self.remote_chanid)
|
||||||
|
@ -500,9 +531,6 @@ class ChannelFile(object):
|
||||||
XXX Todo: the channel and its file-wrappers should be able to be closed or
|
XXX Todo: the channel and its file-wrappers should be able to be closed or
|
||||||
garbage-collected independently, for compatibility with real sockets and
|
garbage-collected independently, for compatibility with real sockets and
|
||||||
their file-wrappers. Currently, closing does nothing but flush the buffer.
|
their file-wrappers. Currently, closing does nothing but flush the buffer.
|
||||||
XXX Todo: translation of the various forms of newline is not implemented,
|
|
||||||
let alone the universal newline. Line buffering (for writing) is
|
|
||||||
implemented, though, which makes little sense without text mode support.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, channel, mode = "r", buf_size = -1):
|
def __init__(self, channel, mode = "r", buf_size = -1):
|
||||||
|
@ -528,6 +556,9 @@ class ChannelFile(object):
|
||||||
self.newlines = None
|
self.newlines = None
|
||||||
self.softspace = False
|
self.softspace = False
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<secsh.ChannelFile from ' + repr(self.channel) + '>'
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
2
demo.py
2
demo.py
|
@ -66,7 +66,7 @@ except Exception, e:
|
||||||
try:
|
try:
|
||||||
event = threading.Event()
|
event = threading.Event()
|
||||||
t = secsh.Transport(sock)
|
t = secsh.Transport(sock)
|
||||||
t.ultra_debug = 1
|
t.ultra_debug = 0
|
||||||
t.start_client(event)
|
t.start_client(event)
|
||||||
# print repr(t)
|
# print repr(t)
|
||||||
event.wait(10)
|
event.wait(10)
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
|
|
||||||
import sys, os, socket, threading, logging, traceback, time
|
import sys, os, socket, threading, logging, traceback
|
||||||
import secsh
|
import secsh
|
||||||
|
|
||||||
# setup logging
|
# setup logging
|
||||||
l = logging.getLogger("secsh")
|
l = logging.getLogger("secsh")
|
||||||
l.setLevel(logging.DEBUG)
|
l.setLevel(logging.DEBUG)
|
||||||
if len(l.handlers) == 0:
|
if len(l.handlers) == 0:
|
||||||
f = open('demo-server.log', 'w')
|
f = open('demo_server.log', 'w')
|
||||||
lh = logging.StreamHandler(f)
|
lh = logging.StreamHandler(f)
|
||||||
lh.setFormatter(logging.Formatter('%(levelname)-.3s [%(asctime)s] %(name)s: %(message)s', '%Y%m%d:%H%M%S'))
|
lh.setFormatter(logging.Formatter('%(levelname)-.3s [%(asctime)s] %(name)s: %(message)s', '%Y%m%d:%H%M%S'))
|
||||||
l.addHandler(lh)
|
l.addHandler(lh)
|
||||||
|
|
||||||
host_key = secsh.RSAKey()
|
host_key = secsh.RSAKey()
|
||||||
host_key.read_private_key_file('demo-host-key')
|
host_key.read_private_key_file('demo_host_key')
|
||||||
|
|
||||||
|
|
||||||
class ServerTransport(secsh.Transport):
|
class ServerTransport(secsh.Transport):
|
||||||
def check_channel_request(self, kind, chanid):
|
def check_channel_request(self, kind, chanid):
|
||||||
if kind == 'session':
|
if kind == 'session':
|
||||||
return secsh.Channel(chanid)
|
return ServerChannel(chanid)
|
||||||
return self.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
|
return self.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
|
||||||
|
|
||||||
def check_auth_password(self, username, password):
|
def check_auth_password(self, username, password):
|
||||||
|
@ -27,6 +27,20 @@ class ServerTransport(secsh.Transport):
|
||||||
return self.AUTH_SUCCESSFUL
|
return self.AUTH_SUCCESSFUL
|
||||||
return self.AUTH_FAILED
|
return self.AUTH_FAILED
|
||||||
|
|
||||||
|
class ServerChannel(secsh.Channel):
|
||||||
|
"Channel descendant that pretends to understand pty and shell requests"
|
||||||
|
|
||||||
|
def __init__(self, chanid):
|
||||||
|
secsh.Channel.__init__(self, chanid)
|
||||||
|
self.event = threading.Event()
|
||||||
|
|
||||||
|
def check_pty_request(self, term, width, height, pixelwidth, pixelheight, modes):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_shell_request(self):
|
||||||
|
self.event.set()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# now connect
|
# now connect
|
||||||
try:
|
try:
|
||||||
|
@ -50,7 +64,7 @@ try:
|
||||||
event = threading.Event()
|
event = threading.Event()
|
||||||
t = ServerTransport(client)
|
t = ServerTransport(client)
|
||||||
t.add_server_key(host_key)
|
t.add_server_key(host_key)
|
||||||
t.ultra_debug = 1
|
t.ultra_debug = 0
|
||||||
t.start_server(event)
|
t.start_server(event)
|
||||||
# print repr(t)
|
# print repr(t)
|
||||||
event.wait(10)
|
event.wait(10)
|
||||||
|
@ -60,7 +74,11 @@ try:
|
||||||
# print repr(t)
|
# print repr(t)
|
||||||
|
|
||||||
chan = t.accept()
|
chan = t.accept()
|
||||||
time.sleep(2)
|
chan.event.wait(10)
|
||||||
|
if not chan.event.isSet():
|
||||||
|
print '*** Client never asked for a shell.'
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
chan.send('\r\n\r\nWelcome to my dorky little BBS!\r\n\r\n')
|
chan.send('\r\n\r\nWelcome to my dorky little BBS!\r\n\r\n')
|
||||||
chan.send('We are on fire all the time! Hooray! Candy corn for everyone!\r\n')
|
chan.send('We are on fire all the time! Hooray! Candy corn for everyone!\r\n')
|
||||||
chan.send('Happy birthday to Robot Dave!\r\n\r\n')
|
chan.send('Happy birthday to Robot Dave!\r\n\r\n')
|
24
kex_gex.py
24
kex_gex.py
|
@ -5,8 +5,8 @@
|
||||||
# LOT more on the server side).
|
# LOT more on the server side).
|
||||||
|
|
||||||
from message import Message
|
from message import Message
|
||||||
from util import inflate_long, deflate_long, generate_prime
|
from util import inflate_long, deflate_long, generate_prime, bit_length
|
||||||
from secsh import SSHException
|
from secsh import SecshException
|
||||||
from transport import MSG_NEWKEYS
|
from transport import MSG_NEWKEYS
|
||||||
from Crypto.Hash import SHA
|
from Crypto.Hash import SHA
|
||||||
from Crypto.Util import number
|
from Crypto.Util import number
|
||||||
|
@ -49,17 +49,7 @@ class KexGex(object):
|
||||||
return self.parse_kexdh_gex_init(m)
|
return self.parse_kexdh_gex_init(m)
|
||||||
elif ptype == MSG_KEXDH_GEX_REPLY:
|
elif ptype == MSG_KEXDH_GEX_REPLY:
|
||||||
return self.parse_kexdh_gex_reply(m)
|
return self.parse_kexdh_gex_reply(m)
|
||||||
raise SSHException('KexGex asked to handle packet type %d' % ptype)
|
raise SecshException('KexGex asked to handle packet type %d' % ptype)
|
||||||
|
|
||||||
def bit_length(n):
|
|
||||||
norm = deflate_long(n, 0)
|
|
||||||
hbyte = ord(norm[0])
|
|
||||||
bitlen = len(norm) * 8
|
|
||||||
while not (hbyte & 0x80):
|
|
||||||
hbyte <<= 1
|
|
||||||
bitlen -= 1
|
|
||||||
return bitlen
|
|
||||||
bit_length = staticmethod(bit_length)
|
|
||||||
|
|
||||||
def generate_x(self):
|
def generate_x(self):
|
||||||
# generate an "x" (1 < x < (p-1)/2).
|
# generate an "x" (1 < x < (p-1)/2).
|
||||||
|
@ -116,9 +106,9 @@ class KexGex(object):
|
||||||
self.p = m.get_mpint()
|
self.p = m.get_mpint()
|
||||||
self.g = m.get_mpint()
|
self.g = m.get_mpint()
|
||||||
# reject if p's bit length < 1024 or > 8192
|
# reject if p's bit length < 1024 or > 8192
|
||||||
bitlen = self.bit_length(self.p)
|
bitlen = bit_length(self.p)
|
||||||
if (bitlen < 1024) or (bitlen > 8192):
|
if (bitlen < 1024) or (bitlen > 8192):
|
||||||
raise SSHException('Server-generated gex p (don\'t ask) is out of range (%d bits)' % bitlen)
|
raise SecshException('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.transport.log(DEBUG, 'Got server p (%d bits)' % bitlen)
|
||||||
self.generate_x()
|
self.generate_x()
|
||||||
# now compute e = g^x mod p
|
# now compute e = g^x mod p
|
||||||
|
@ -132,7 +122,7 @@ class KexGex(object):
|
||||||
def parse_kexdh_gex_init(self, m):
|
def parse_kexdh_gex_init(self, m):
|
||||||
self.e = m.get_mpint()
|
self.e = m.get_mpint()
|
||||||
if (self.e < 1) or (self.e > self.p - 1):
|
if (self.e < 1) or (self.e > self.p - 1):
|
||||||
raise SSHException('Client kex "e" is out of range')
|
raise SecshException('Client kex "e" is out of range')
|
||||||
self.generate_x()
|
self.generate_x()
|
||||||
K = pow(self.e, self.x, P)
|
K = pow(self.e, self.x, P)
|
||||||
key = str(self.transport.get_server_key())
|
key = str(self.transport.get_server_key())
|
||||||
|
@ -164,7 +154,7 @@ class KexGex(object):
|
||||||
self.f = m.get_mpint()
|
self.f = m.get_mpint()
|
||||||
sig = m.get_string()
|
sig = m.get_string()
|
||||||
if (self.f < 1) or (self.f > self.p - 1):
|
if (self.f < 1) or (self.f > self.p - 1):
|
||||||
raise SSHException('Server kex "f" is out of range')
|
raise SecshException('Server kex "f" is out of range')
|
||||||
K = pow(self.f, self.x, self.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 || min || n || max || p || g || e || f || K)
|
# 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)
|
hm = Message().add(self.transport.local_version).add(self.transport.remote_version)
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
# "g" generator.
|
# "g" generator.
|
||||||
|
|
||||||
from message import Message, inflate_long
|
from message import Message, inflate_long
|
||||||
from secsh import SSHException
|
from secsh import SecshException
|
||||||
from transport import MSG_NEWKEYS
|
from transport import MSG_NEWKEYS
|
||||||
from Crypto.Hash import SHA
|
from Crypto.Hash import SHA
|
||||||
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
|
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
|
@ -59,14 +59,14 @@ class KexGroup1(object):
|
||||||
return self.parse_kexdh_init(m)
|
return self.parse_kexdh_init(m)
|
||||||
elif not self.transport.server_mode and (ptype == MSG_KEXDH_REPLY):
|
elif not self.transport.server_mode and (ptype == MSG_KEXDH_REPLY):
|
||||||
return self.parse_kexdh_reply(m)
|
return self.parse_kexdh_reply(m)
|
||||||
raise SSHException('KexGroup1 asked to handle packet type %d' % ptype)
|
raise SecshException('KexGroup1 asked to handle packet type %d' % ptype)
|
||||||
|
|
||||||
def parse_kexdh_reply(self, m):
|
def parse_kexdh_reply(self, m):
|
||||||
# client mode
|
# client mode
|
||||||
host_key = m.get_string()
|
host_key = m.get_string()
|
||||||
self.f = m.get_mpint()
|
self.f = m.get_mpint()
|
||||||
if (self.f < 1) or (self.f > P - 1):
|
if (self.f < 1) or (self.f > P - 1):
|
||||||
raise SSHException('Server kex "f" is out of range')
|
raise SecshException('Server kex "f" is out of range')
|
||||||
sig = m.get_string()
|
sig = m.get_string()
|
||||||
K = pow(self.f, self.x, P)
|
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)
|
# 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
|
# server mode
|
||||||
self.e = m.get_mpint()
|
self.e = m.get_mpint()
|
||||||
if (self.e < 1) or (self.e > P - 1):
|
if (self.e < 1) or (self.e > P - 1):
|
||||||
raise SSHException('Client kex "e" is out of range')
|
raise SecshException('Client kex "e" is out of range')
|
||||||
K = pow(self.e, self.x, P)
|
K = pow(self.e, self.x, P)
|
||||||
key = str(self.transport.get_server_key())
|
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)
|
# okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K)
|
||||||
|
|
7
secsh.py
7
secsh.py
|
@ -5,8 +5,7 @@ import sys
|
||||||
if (sys.version_info[0] < 2) or ((sys.version_info[0] == 2) and (sys.version_info[1] < 3)):
|
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.')
|
raise RuntimeError('You need python 2.3 for this module.')
|
||||||
|
|
||||||
# FIXME rename
|
class SecshException(Exception):
|
||||||
class SSHException(Exception):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,7 +16,7 @@ from dsskey import DSSKey
|
||||||
|
|
||||||
|
|
||||||
__author__ = "Robey Pointer <robey@lag.net>"
|
__author__ = "Robey Pointer <robey@lag.net>"
|
||||||
__date__ = "18 Sep 2003"
|
__date__ = "9 Nov 2003"
|
||||||
__version__ = "0.1-bulbasaur"
|
__version__ = "0.1-charmander"
|
||||||
__credits__ = "Huzzah!"
|
__credits__ = "Huzzah!"
|
||||||
|
|
||||||
|
|
16
setup.py
16
setup.py
|
@ -1,23 +1,25 @@
|
||||||
from distutils.core import setup
|
from distutils.core import setup
|
||||||
|
|
||||||
longdesc = '''
|
longdesc = '''
|
||||||
This is a library for making client-side SSH2 connections (server-side is
|
This is a library for making SSH2 connections (client or server).
|
||||||
coming soon). All major ciphers and hash methods are supported.
|
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.
|
||||||
|
|
||||||
Required packages:
|
Required packages:
|
||||||
pyCrypto
|
pyCrypto
|
||||||
'''
|
'''
|
||||||
|
|
||||||
setup(name = "secsh",
|
setup(name = "secsh",
|
||||||
version = "0.1-bulbasaur",
|
version = "0.1-charmander",
|
||||||
description = "SSH2 protocol library",
|
description = "SSH2 protocol library",
|
||||||
author = "Robey Pointer",
|
author = "Robey Pointer",
|
||||||
author_email = "robey@lag.net",
|
author_email = "robey@lag.net",
|
||||||
url = "http://www.lag.net/~robey/secsh/",
|
url = "http://www.lag.net/~robey/secsh/",
|
||||||
py_modules = [ 'secsh', 'transport', 'channel', 'message', 'util', 'ber',
|
py_modules = [ 'secsh', 'transport', 'auth_transport', 'channel',
|
||||||
'kex_group1', 'kex_gex', 'rsakey', 'dsskey' ],
|
'message', 'util', 'ber', 'kex_group1', 'kex_gex',
|
||||||
scripts = [ 'demo.py' ],
|
'rsakey', 'dsskey' ],
|
||||||
download_url = 'http://www.lag.net/~robey/secsh/secsh-0.1-bulbasaur.zip',
|
download_url = 'http://www.lag.net/~robey/secsh/secsh-0.1-charmander.zip',
|
||||||
license = 'LGPL',
|
license = 'LGPL',
|
||||||
platforms = 'Posix; MacOS X; Windows',
|
platforms = 'Posix; MacOS X; Windows',
|
||||||
classifiers = [ 'Development Status :: 3 - Alpha',
|
classifiers = [ 'Development Status :: 3 - Alpha',
|
||||||
|
|
47
transport.py
47
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
|
import sys, os, string, threading, socket, logging, struct
|
||||||
from message import Message
|
from message import Message
|
||||||
from channel import Channel
|
from channel import Channel
|
||||||
from secsh import SSHException
|
from secsh import SecshException
|
||||||
from util import format_binary, safe_string, inflate_long, deflate_long, tb_strings
|
from util import format_binary, safe_string, inflate_long, deflate_long, tb_strings
|
||||||
from rsakey import RSAKey
|
from rsakey import RSAKey
|
||||||
from dsskey import DSSKey
|
from dsskey import DSSKey
|
||||||
|
@ -79,8 +79,7 @@ class BaseTransport(threading.Thread):
|
||||||
preferred_ciphers = [ 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc' ]
|
preferred_ciphers = [ 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc' ]
|
||||||
preferred_macs = [ 'hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96' ]
|
preferred_macs = [ 'hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96' ]
|
||||||
preferred_keys = [ 'ssh-rsa', 'ssh-dss' ]
|
preferred_keys = [ 'ssh-rsa', 'ssh-dss' ]
|
||||||
preferred_kex = [ 'diffie-hellman-group1-sha1' ]
|
preferred_kex = [ 'diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1' ]
|
||||||
# preferred_kex = [ 'diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1' ]
|
|
||||||
|
|
||||||
cipher_info = {
|
cipher_info = {
|
||||||
'blowfish-cbc': { 'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16 },
|
'blowfish-cbc': { 'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16 },
|
||||||
|
@ -203,7 +202,7 @@ class BaseTransport(threading.Thread):
|
||||||
def get_remote_server_key(self):
|
def get_remote_server_key(self):
|
||||||
'returns (type, key) where type is like "ssh-rsa" and key is an opaque string'
|
'returns (type, key) where type is like "ssh-rsa" and key is an opaque string'
|
||||||
if (not self.active) or (not self.initial_kex_done):
|
if (not self.active) or (not self.initial_kex_done):
|
||||||
raise SSHException('No existing session')
|
raise SecshException('No existing session')
|
||||||
key_msg = Message(self.host_key)
|
key_msg = Message(self.host_key)
|
||||||
key_type = key_msg.get_string()
|
key_type = key_msg.get_string()
|
||||||
return key_type, self.host_key
|
return key_type, self.host_key
|
||||||
|
@ -324,7 +323,7 @@ class BaseTransport(threading.Thread):
|
||||||
# leftover contains decrypted bytes from the first block (after the length field)
|
# leftover contains decrypted bytes from the first block (after the length field)
|
||||||
leftover = header[4:]
|
leftover = header[4:]
|
||||||
if (packet_size - len(leftover)) % self.block_size_in != 0:
|
if (packet_size - len(leftover)) % self.block_size_in != 0:
|
||||||
raise SSHException('Invalid packet blocking')
|
raise SecshException('Invalid packet blocking')
|
||||||
buffer = self.read_all(packet_size + self.remote_mac_len - len(leftover))
|
buffer = self.read_all(packet_size + self.remote_mac_len - len(leftover))
|
||||||
packet = buffer[:packet_size - len(leftover)]
|
packet = buffer[:packet_size - len(leftover)]
|
||||||
post_packet = buffer[packet_size - len(leftover):]
|
post_packet = buffer[packet_size - len(leftover):]
|
||||||
|
@ -338,7 +337,7 @@ class BaseTransport(threading.Thread):
|
||||||
mac_payload = struct.pack('>II', self.sequence_number_in, packet_size) + packet
|
mac_payload = struct.pack('>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]
|
my_mac = HMAC.HMAC(self.mac_key_in, mac_payload, self.remote_mac_engine).digest()[:self.remote_mac_len]
|
||||||
if my_mac != mac:
|
if my_mac != mac:
|
||||||
raise SSHException('Mismatched MAC')
|
raise SecshException('Mismatched MAC')
|
||||||
padding = ord(packet[0])
|
padding = ord(packet[0])
|
||||||
payload = packet[1:packet_size - padding + 1]
|
payload = packet[1:packet_size - padding + 1]
|
||||||
randpool.add_event(packet[packet_size - padding + 1])
|
randpool.add_event(packet[packet_size - padding + 1])
|
||||||
|
@ -361,7 +360,7 @@ class BaseTransport(threading.Thread):
|
||||||
# comply, then just drop the connection
|
# comply, then just drop the connection
|
||||||
self.received_packets_overflow += 1
|
self.received_packets_overflow += 1
|
||||||
if self.received_packets_overflow >= 20:
|
if self.received_packets_overflow >= 20:
|
||||||
raise SSHException('Remote transport is ignoring rekey requests')
|
raise SecshException('Remote transport is ignoring rekey requests')
|
||||||
|
|
||||||
return ord(payload[0]), msg
|
return ord(payload[0]), msg
|
||||||
|
|
||||||
|
@ -380,9 +379,9 @@ class BaseTransport(threading.Thread):
|
||||||
else:
|
else:
|
||||||
key = None
|
key = None
|
||||||
if (key == None) or not key.valid:
|
if (key == None) or not key.valid:
|
||||||
raise SSHException('Unknown host key type')
|
raise SecshException('Unknown host key type')
|
||||||
if not key.verify_ssh_sig(self.H, Message(sig)):
|
if not key.verify_ssh_sig(self.H, Message(sig)):
|
||||||
raise SSHException('Signature verification (%s) failed. Boo. Robey should debug this.' % self.host_key_type)
|
raise SecshException('Signature verification (%s) failed. Boo. Robey should debug this.' % self.host_key_type)
|
||||||
self.host_key = host_key
|
self.host_key = host_key
|
||||||
|
|
||||||
def compute_key(self, id, nbytes):
|
def compute_key(self, id, nbytes):
|
||||||
|
@ -405,7 +404,7 @@ class BaseTransport(threading.Thread):
|
||||||
|
|
||||||
def get_cipher(self, name, key, iv):
|
def get_cipher(self, name, key, iv):
|
||||||
if not self.cipher_info.has_key(name):
|
if not self.cipher_info.has_key(name):
|
||||||
raise SSHException('Unknown client cipher ' + name)
|
raise SecshException('Unknown client cipher ' + name)
|
||||||
return self.cipher_info[name]['class'].new(key, self.cipher_info[name]['mode'], iv)
|
return self.cipher_info[name]['class'].new(key, self.cipher_info[name]['mode'], iv)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
@ -430,7 +429,7 @@ class BaseTransport(threading.Thread):
|
||||||
continue
|
continue
|
||||||
if self.expected_packet != 0:
|
if self.expected_packet != 0:
|
||||||
if ptype != self.expected_packet:
|
if ptype != self.expected_packet:
|
||||||
raise SSHException('Expecting packet %d, got %d' % (self.expected_packet, ptype))
|
raise SecshException('Expecting packet %d, got %d' % (self.expected_packet, ptype))
|
||||||
self.expected_packet = 0
|
self.expected_packet = 0
|
||||||
if (ptype >= 30) and (ptype <= 39):
|
if (ptype >= 30) and (ptype <= 39):
|
||||||
self.kex_engine.parse_next(ptype, m)
|
self.kex_engine.parse_next(ptype, m)
|
||||||
|
@ -448,7 +447,7 @@ class BaseTransport(threading.Thread):
|
||||||
msg.add_byte(chr(MSG_UNIMPLEMENTED))
|
msg.add_byte(chr(MSG_UNIMPLEMENTED))
|
||||||
msg.add_int(m.seqno)
|
msg.add_int(m.seqno)
|
||||||
self.send_message(msg)
|
self.send_message(msg)
|
||||||
except SSHException, e:
|
except SecshException, e:
|
||||||
self.log(DEBUG, 'Exception: ' + str(e))
|
self.log(DEBUG, 'Exception: ' + str(e))
|
||||||
self.log(DEBUG, tb_strings())
|
self.log(DEBUG, tb_strings())
|
||||||
except EOFError, e:
|
except EOFError, e:
|
||||||
|
@ -480,7 +479,7 @@ class BaseTransport(threading.Thread):
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def negotiate_keys(self, m):
|
def negotiate_keys(self, m):
|
||||||
# throws SSHException on anything unusual
|
# throws SecshException on anything unusual
|
||||||
if self.local_kex_init == None:
|
if self.local_kex_init == None:
|
||||||
# remote side wants to renegotiate
|
# remote side wants to renegotiate
|
||||||
self.send_kex_init()
|
self.send_kex_init()
|
||||||
|
@ -500,7 +499,7 @@ class BaseTransport(threading.Thread):
|
||||||
break
|
break
|
||||||
self.log(DEBUG, 'Banner: ' + buffer)
|
self.log(DEBUG, 'Banner: ' + buffer)
|
||||||
if buffer[:4] != 'SSH-':
|
if buffer[:4] != 'SSH-':
|
||||||
raise SSHException('Indecipherable protocol version "' + buffer + '"')
|
raise SecshException('Indecipherable protocol version "' + buffer + '"')
|
||||||
# save this server version string for later
|
# save this server version string for later
|
||||||
self.remote_version = buffer
|
self.remote_version = buffer
|
||||||
# pull off any attached comment
|
# pull off any attached comment
|
||||||
|
@ -512,15 +511,19 @@ class BaseTransport(threading.Thread):
|
||||||
# parse out version string and make sure it matches
|
# parse out version string and make sure it matches
|
||||||
segs = buffer.split('-', 2)
|
segs = buffer.split('-', 2)
|
||||||
if len(segs) < 3:
|
if len(segs) < 3:
|
||||||
raise SSHException('Invalid SSH banner')
|
raise SecshException('Invalid SSH banner')
|
||||||
version = segs[1]
|
version = segs[1]
|
||||||
client = segs[2]
|
client = segs[2]
|
||||||
if version != '1.99' and version != '2.0':
|
if version != '1.99' and version != '2.0':
|
||||||
raise SSHException('Incompatible version (%s instead of 2.0)' % (version,))
|
raise SecshException('Incompatible version (%s instead of 2.0)' % (version,))
|
||||||
self.log(INFO, 'Connected (version %s, client %s)' % (version, client))
|
self.log(INFO, 'Connected (version %s, client %s)' % (version, client))
|
||||||
|
|
||||||
def send_kex_init(self):
|
def send_kex_init(self):
|
||||||
# send a really wimpy kex-init packet that says we're a bare-bones ssh client
|
# send a really wimpy kex-init packet that says we're a bare-bones ssh client
|
||||||
|
if self.server_mode:
|
||||||
|
# FIXME: can't do group-exchange (gex) yet -- too slow
|
||||||
|
if 'diffie-hellman-group-exchange-sha1' in self.preferred_kex:
|
||||||
|
self.preferred_kex.remove('diffie-hellman-group-exchange-sha1')
|
||||||
m = Message()
|
m = Message()
|
||||||
m.add_byte(chr(MSG_KEXINIT))
|
m.add_byte(chr(MSG_KEXINIT))
|
||||||
m.add_bytes(randpool.get_bytes(16))
|
m.add_bytes(randpool.get_bytes(16))
|
||||||
|
@ -563,7 +566,7 @@ class BaseTransport(threading.Thread):
|
||||||
# no compression support (yet?)
|
# no compression support (yet?)
|
||||||
if (not('none' in client_compress_algo_list) or
|
if (not('none' in client_compress_algo_list) or
|
||||||
not('none' in server_compress_algo_list)):
|
not('none' in server_compress_algo_list)):
|
||||||
raise SSHException('Incompatible ssh peer.')
|
raise SecshException('Incompatible ssh peer.')
|
||||||
|
|
||||||
# as a server, we pick the first item in the client's list that we support.
|
# 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.
|
# as a client, we pick the first item in our list that the server supports.
|
||||||
|
@ -572,7 +575,7 @@ class BaseTransport(threading.Thread):
|
||||||
else:
|
else:
|
||||||
agreed_kex = filter(kex_algo_list.__contains__, self.preferred_kex)
|
agreed_kex = filter(kex_algo_list.__contains__, self.preferred_kex)
|
||||||
if len(agreed_kex) == 0:
|
if len(agreed_kex) == 0:
|
||||||
raise SSHException('Incompatible ssh peer (no acceptable kex algorithm)')
|
raise SecshException('Incompatible ssh peer (no acceptable kex algorithm)')
|
||||||
self.kex_engine = self.kex_info[agreed_kex[0]](self)
|
self.kex_engine = self.kex_info[agreed_kex[0]](self)
|
||||||
|
|
||||||
if self.server_mode:
|
if self.server_mode:
|
||||||
|
@ -580,10 +583,10 @@ class BaseTransport(threading.Thread):
|
||||||
else:
|
else:
|
||||||
agreed_keys = filter(server_key_algo_list.__contains__, self.preferred_keys)
|
agreed_keys = filter(server_key_algo_list.__contains__, self.preferred_keys)
|
||||||
if len(agreed_keys) == 0:
|
if len(agreed_keys) == 0:
|
||||||
raise SSHException('Incompatible ssh peer (no acceptable host key)')
|
raise SecshException('Incompatible ssh peer (no acceptable host key)')
|
||||||
self.host_key_type = agreed_keys[0]
|
self.host_key_type = agreed_keys[0]
|
||||||
if self.server_mode and (self.get_server_key() is None):
|
if self.server_mode and (self.get_server_key() is None):
|
||||||
raise SSHException('Incompatible ssh peer (can\'t match requested host key type)')
|
raise SecshException('Incompatible ssh peer (can\'t match requested host key type)')
|
||||||
|
|
||||||
if self.server_mode:
|
if self.server_mode:
|
||||||
agreed_local_ciphers = filter(self.preferred_ciphers.__contains__,
|
agreed_local_ciphers = filter(self.preferred_ciphers.__contains__,
|
||||||
|
@ -596,7 +599,7 @@ class BaseTransport(threading.Thread):
|
||||||
agreed_remote_ciphers = filter(server_encrypt_algo_list.__contains__,
|
agreed_remote_ciphers = filter(server_encrypt_algo_list.__contains__,
|
||||||
self.preferred_ciphers)
|
self.preferred_ciphers)
|
||||||
if (len(agreed_local_ciphers) == 0) or (len(agreed_remote_ciphers) == 0):
|
if (len(agreed_local_ciphers) == 0) or (len(agreed_remote_ciphers) == 0):
|
||||||
raise SSHException('Incompatible ssh server (no acceptable ciphers)')
|
raise SecshException('Incompatible ssh server (no acceptable ciphers)')
|
||||||
self.local_cipher = agreed_local_ciphers[0]
|
self.local_cipher = agreed_local_ciphers[0]
|
||||||
self.remote_cipher = agreed_remote_ciphers[0]
|
self.remote_cipher = agreed_remote_ciphers[0]
|
||||||
self.log(DEBUG, 'Ciphers agreed: local=%s, remote=%s' % (self.local_cipher, self.remote_cipher))
|
self.log(DEBUG, 'Ciphers agreed: local=%s, remote=%s' % (self.local_cipher, self.remote_cipher))
|
||||||
|
@ -608,7 +611,7 @@ class BaseTransport(threading.Thread):
|
||||||
agreed_local_macs = filter(client_mac_algo_list.__contains__, self.preferred_macs)
|
agreed_local_macs = filter(client_mac_algo_list.__contains__, self.preferred_macs)
|
||||||
agreed_remote_macs = filter(server_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):
|
if (len(agreed_local_macs) == 0) or (len(agreed_remote_macs) == 0):
|
||||||
raise SSHException('Incompatible ssh server (no acceptable macs)')
|
raise SecshException('Incompatible ssh server (no acceptable macs)')
|
||||||
self.local_mac = agreed_local_macs[0]
|
self.local_mac = agreed_local_macs[0]
|
||||||
self.remote_mac = agreed_remote_macs[0]
|
self.remote_mac = agreed_remote_macs[0]
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue