[project @ Arch-1:robey@lag.net--2003-public%secsh--dev--1.0--patch-20]

more docs, and password-protected key files can now be read
lots more documentation, some of it moved out of the README file, which is
now much smaller and less rambling.

repr(Transport) now reports the number of bits used in the cipher.

cleaned up BER to use util functions, and throw a proper exception (the new
BERException) on error.  it doesn't ever have to be a full BER decoder, but
it can at least comb its hair and tuck in its shirt.

lots of stuff added to PKey.read_private_key_file so it can try to decode
password-protected key files.  right now it only understands "DES-EDE3-CBC"
format, but this is the only format i've seen openssh make so far.  if the
key is password-protected, but no password was given, a new exception
(PasswordRequiredException) is raised so an outer layer can ask for a password
and try again.
This commit is contained in:
Robey Pointer 2004-01-04 09:29:13 +00:00
parent 3a8887a420
commit 988c6abda0
14 changed files with 451 additions and 209 deletions

92
README
View File

@ -17,22 +17,20 @@ telnet and rsh for secure access to remote shells, but the protocol also
includes the ability to open arbitrary channels to remote services across the includes the ability to open arbitrary channels to remote services across the
encrypted tunnel (this is how sftp works, for example). encrypted tunnel (this is how sftp works, for example).
the module works by taking a socket-like object that you pass in, negotiating
with the remote server, authenticating (using a password or a given private
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 paramiko module.
it is written entirely in python (no C or platform-dependent code) and is it is written entirely in python (no C or platform-dependent code) and is
released under the GNU LGPL (lesser GPL). released under the GNU LGPL (lesser GPL).
the package and its API is fairly well documented in the "doc/" folder that
should have come with this archive.
*** REQUIREMENTS *** REQUIREMENTS
python 2.3 <http://www.python.org/> python 2.3 <http://www.python.org/>
pyCrypto <http://www.amk.ca/python/code/crypto.html> pyCrypt <http://www.amk.ca/python/code/crypto.html>
PyCrypt compiled for Win32 can be downloaded from the HashTar homepage:
http://nitace.bsd.uchicago.edu:8080/hashtar
*** PORTABILITY *** PORTABILITY
@ -45,12 +43,8 @@ run into Windows problems, send me a patch: portability is important to me.
the Channel object supports a "fileno()" call so that it can be passed into the Channel object supports a "fileno()" call so that it can be passed into
select or poll, for polling on posix. once you call "fileno()" on a Channel, select or poll, for polling on posix. once you call "fileno()" on a Channel,
it changes behavior in some fundamental ways, and these ways require posix. it changes behavior in some fundamental ways, and these ways require posix.
so don't call "fileno()" on a Channel on Windows. (the problem is that pipes so don't call "fileno()" on a Channel on Windows. this is detailed in the
are used to simulate an open socket, so that the ssh "socket" has an OS-level documentation for the "fileno" method.
file descriptor. i haven't figured out how to make pipes on Windows go into
non-blocking mode yet. [if you don't understand this last sentence, don't
be afraid. the point is to make the API simple enough that you don't HAVE to
know these screwy steps. i just don't understand windows enough.])
*** DEMO *** DEMO
@ -63,13 +57,15 @@ you can run demo.py with no arguments, or you can give a hostname (or
username@hostname) on the command line. if you don't, it'll prompt you for username@hostname) on the command line. if you don't, it'll prompt you for
a hostname and username. if you have an ".ssh/" folder, it will try to read a hostname and username. if you have an ".ssh/" folder, it will try to read
the host keys from there, though it's easily confused. you can choose to 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 authenticate with a password, or with an RSA or DSS key.
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 paramiko 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, logs as it works. but the most interesting part is probably the code itself,
which hopefully demonstrates how you can use the paramiko library. which hopefully demonstrates how you can use the paramiko library.
a simpler example is in demo_simple.py, which is a copy of the demo client
that uses the simpler "connect" method call (new with 0.9-doduo).
there's also now a demo server (demo_server.py) which listens on port 2200 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 and accepts a login (robey/foo) and pretends to be a BBS, just to demonstrate
how to perform the server side of things. how to perform the server side of things.
@ -77,63 +73,17 @@ how to perform the server side of things.
*** USE *** USE
(this section could probably be improved a lot.) the demo clients (demo.py & demo_simple.py) and the demo server
(demo_server.py) are probably the best example of how to use this package.
first, create a Transport by passing in an existing socket (connected to the there is also a lot of documentation, generated with epydoc, in the doc/
desired server). call "start_client(event)", passing in an event which will folder. point your browser there. seriously, do it. mad props to epydoc,
be triggered when the negotiation is finished (either successfully or not). which actually motivated me to write more documentation than i ever would
the event is required because each new Transport creates a new worker thread have before.
to handle incoming data asynchronously.
after the event triggers, use "is_active()" to determine if the Transport was
successfully connected. if so, you should check the server's host key to make
sure it's what you expected. don't worry, i don't mean "check" in any crypto
sense: i mean compare the key, byte for byte, with what you saw last time, to
make sure it's the same key. Transport will handle verifying that the server's
key works.
next, authenticate, using either "auth_key" or "auth_password". in the future,
this API may change to accomodate servers that require both forms of auth.
pass another event in so you can determine when the authentication dance is
over. if it was successful, "is_authenticated()" will return true.
once authentication is successful, the Transport is ready to use. call
"open_channel" or "open_session" to create new channels over the Transport
(SSH2 supports many different channels over the same connection). these calls
block until they succeed or fail, and return a Channel object on success, or
None on failure. Channel objects can be treated as "socket-like objects": they
implement:
recv(nbytes)
send(data)
settimeout(timeout_in_seconds)
close()
fileno() [* see note below]
because SSH2 has a windowing kind of flow control, if you stop reading data
from a Channel and its buffer fills up, the server will be unable to send you
any more data until you read some of it. (this won't affect other channels on
the Transport, though.)
* NOTE that if you use "fileno()", the behavior of the Channel will change
slightly, underneath. this shouldn't be noticable outside the library, but
this alternate implementation will not work on non-posix systems. so don't
try calling "fileno()" on Windows! this has the side effect that you can't
pass a Channel to "select" or "poll" on Windows (which should be fine, since
those calls don't exist on Windows). calling "fileno()" creates an OS-level
pipe and generates a real file descriptor which can be used for polling, BUT
should not be used for reading data from the channel: use "recv" instead.
because each Transport has a worker thread running in the background, you
must call "close()" on the Transport to kill this thread. on many platforms,
the python interpreter will refuse to exit cleanly if any of these threads
are still running (and you'll have to kill -9 from another shell window).
[fixme: add info about server mode]
*** MISSING LINKS *** MISSING LINKS
* ctr forms of ciphers are missing (blowfish-ctr, aes128-ctr, aes256-ctr) * ctr forms of ciphers are missing (blowfish-ctr, aes128-ctr, aes256-ctr)
* can't handle password-protected private key files
* multi-part auth not supported (ie, need username AND pk) * multi-part auth not supported (ie, need username AND pk)
* server mode needs better doc * server mode needs better documentation
* sftp?

View File

@ -39,6 +39,7 @@ if len(l.handlers) == 0:
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)
username = '' username = ''
if len(sys.argv) > 1: if len(sys.argv) > 1:
hostname = sys.argv[1] hostname = sys.argv[1]
@ -107,7 +108,11 @@ try:
path = raw_input('RSA key [%s]: ' % default_path) path = raw_input('RSA key [%s]: ' % default_path)
if len(path) == 0: if len(path) == 0:
path = default_path path = default_path
try:
key.read_private_key_file(path) key.read_private_key_file(path)
except paramiko.PasswordRequiredException:
password = getpass.getpass('RSA key password: ')
key.read_private_key_file(path, password)
t.auth_publickey(username, key, event) t.auth_publickey(username, key, event)
elif auth == 'd': elif auth == 'd':
key = paramiko.DSSKey() key = paramiko.DSSKey()
@ -115,7 +120,11 @@ try:
path = raw_input('DSS key [%s]: ' % default_path) path = raw_input('DSS key [%s]: ' % default_path)
if len(path) == 0: if len(path) == 0:
path = default_path path = default_path
try:
key.read_private_key_file(path) key.read_private_key_file(path)
except paramiko.PasswordRequiredException:
password = getpass.getpass('DSS key password: ')
key.read_private_key_file(path, password)
t.auth_key(username, key, event) t.auth_key(username, key, event)
else: else:
pw = getpass.getpass('Password for %s@%s: ' % (username, hostname)) pw = getpass.getpass('Password for %s@%s: ' % (username, hostname))

View File

@ -37,6 +37,7 @@ class ServerTransport(paramiko.Transport):
return self.AUTH_FAILED return self.AUTH_FAILED
def check_auth_publickey(self, username, key): def check_auth_publickey(self, username, key):
print 'Auth attempt with key: ' + paramiko.util.hexify(key.get_fingerprint())
if (username == 'robey') and (key == self.good_pub_key): if (username == 'robey') and (key == self.good_pub_key):
return self.AUTH_SUCCESSFUL return self.AUTH_SUCCESSFUL
return self.AUTH_FAILED return self.AUTH_FAILED
@ -66,6 +67,7 @@ try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', 2200)) sock.bind(('', 2200))
except Exception, e: except Exception, e:
print '*** Bind failed: ' + str(e) print '*** Bind failed: ' + str(e)
traceback.print_exc() traceback.print_exc()
sys.exit(1) sys.exit(1)

View File

@ -1,3 +1,40 @@
#!/usr/bin/python
"""
I{Paramiko} (a combination of the esperanto words for "paranoid" and "friend")
is a module for python 2.3 or greater that implements the SSH2 protocol for
secure (encrypted and authenticated) connections to remote machines. Unlike
SSL (aka TLS), the SSH2 protocol does not require heirarchical certificates
signed by a powerful central authority. You may know SSH2 as the protocol that
replaced C{telnet} and C{rsh} for secure access to remote shells, but the
protocol also includes the ability to open arbitrary channels to remote
services across an encrypted tunnel. (This is how C{sftp} works, for example.)
To use this package, pass a socket (or socket-like object) to a L{Transport},
and use L{start_server <paramiko.transport.BaseTransport.start_server>} or
L{start_client <paramiko.transport.BaseTransport.start_client>} to negoatite
with the remote host as either a server or client. As a client, you are
responsible for authenticating using a password or private key, and checking
the server's host key. I{(Key signature and verification is done by paramiko,
but you will need to provide private keys and check that the content of a
public key matches what you expected to see.)} As a server, you are
responsible for deciding which users, passwords, and keys to allow, and what
kind of channels to allow.
Once you have finished, either side may request flow-controlled L{Channel}s to
the other side, which are python objects that act like sockets, but send and
receive data over the encrypted session.
Paramiko is written entirely in python (no C or platform-dependent code) and is
released under the GNU Lesser General Public License (LGPL).
Website: U{http://www.lag.net/~robey/paramiko/}
@version: 0.9 (doduo)
@author: Robey Pointer
@contact: robey@lag.net
@license: GNU Lesser General Public License (LGPL)
"""
import sys import sys
@ -8,50 +45,31 @@ if (sys.version_info[0] < 2) or ((sys.version_info[0] == 2) and (sys.version_inf
__author__ = "Robey Pointer <robey@lag.net>" __author__ = "Robey Pointer <robey@lag.net>"
__date__ = "10 Nov 2003" __date__ = "10 Nov 2003"
__version__ = "0.1-charmander" __version__ = "0.1-charmander"
__credits__ = "Huzzah!" #__credits__ = "Huzzah!"
__license__ = "Lesser GNU Public License (LGPL)" __license__ = "GNU Lesser General Public License (LGPL)"
import ssh_exception, transport, auth_transport, channel, rsakey, dsskey import transport, auth_transport, channel, rsakey, dsskey, ssh_exception
class SSHException (ssh_exception.SSHException): Transport = auth_transport.Transport
""" Channel = channel.Channel
Exception thrown by failures in SSH2 protocol negotiation or logic errors. RSAKey = rsakey.RSAKey
""" DSSKey = dsskey.DSSKey
pass SSHException = ssh_exception.SSHException
PasswordRequiredException = ssh_exception.PasswordRequiredException
class Transport (auth_transport.Transport):
"""
An SSH Transport attaches to a stream (usually a socket), negotiates an
encrypted session, authenticates, and then creates stream tunnels, called
L{Channel}s, across the session. Multiple channels can be multiplexed
across a single session (and often are, in the case of port forwardings).
"""
pass
class Channel (channel.Channel):
"""
A secure tunnel across an SSH L{Transport}. A Channel is meant to behave
like a socket, and has an API that should be indistinguishable from the
python socket API.
"""
pass
class RSAKey (rsakey.RSAKey):
"""
Representation of an RSA key which can be used to sign and verify SSH2
data.
"""
pass
class DSSKey (dsskey.DSSKey):
"""
Representation of a DSS key which can be used to sign an verify SSH2
data.
"""
pass
__all__ = [ 'Transport', 'Channel', 'RSAKey', 'DSSKey', 'transport', __all__ = [ 'Transport',
'auth_transport', 'channel', 'rsakey', 'dsskey', 'util', 'Channel',
'SSHException' ] 'RSAKey',
'DSSKey',
'SSHException',
'PasswordRequiredException',
'transport',
'auth_transport',
'channel',
'rsakey',
'dsskey',
'pkey',
'ssh_exception',
'util' ]

View File

@ -1,5 +1,10 @@
#!/usr/bin/python #!/usr/bin/python
"""
L{Transport} is a subclass of L{BaseTransport} that handles authentication.
This separation keeps either class file from being too unwieldy.
"""
from transport import BaseTransport 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
@ -11,11 +16,18 @@ _DISCONNECT_SERVICE_NOT_AVAILABLE, _DISCONNECT_AUTH_CANCELLED_BY_USER, \
_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14 _DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14
class Transport (BaseTransport): class Transport (BaseTransport):
""" """
Subclass of L{BaseTransport} that handles authentication. This separation An SSH Transport attaches to a stream (usually a socket), negotiates an
keeps either class file from being too unwieldy. encrypted session, authenticates, and then creates stream tunnels, called
L{Channel}s, across the session. Multiple channels can be multiplexed
across a single session (and often are, in the case of port forwardings).
@note: Because each Transport has a worker thread running in the
background, you must call L{close} on the Transport to kill this thread.
On many platforms, the python interpreter will refuse to exit cleanly if
any of these threads are still running (and you'll have to C{kill -9} from
another shell window).
""" """
AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED = range(3) AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED = range(3)
@ -34,7 +46,7 @@ class Transport (BaseTransport):
return '<paramiko.Transport (unconnected)>' return '<paramiko.Transport (unconnected)>'
out = '<paramiko.Transport' out = '<paramiko.Transport'
if self.local_cipher != '': if self.local_cipher != '':
out += ' (cipher %s)' % self.local_cipher out += ' (cipher %s, %d bits)' % (self.local_cipher, self._cipher_info[self.local_cipher]['key-size'] * 8)
if self.authenticated: if self.authenticated:
if len(self.channels) == 1: if len(self.channels) == 1:
out += ' (active; 1 open channel)' out += ' (active; 1 open channel)'

View File

@ -1,45 +1,15 @@
#!/usr/bin/python #!/usr/bin/python
import struct import struct, util
def inflate_long(s, always_positive=0):
"turns a normalized byte string into a long-int (adapted from Crypto.Util.number)"
out = 0L
if len(s) % 4:
filler = '\x00'
if not always_positive and (ord(s[0]) >= 0x80):
# negative
filler = '\xff'
s = filler * (4 - len(s) % 4) + s
# FIXME: this doesn't actually handle negative.
# luckily ssh never uses negative bignums.
for i in range(0, len(s), 4):
out = (out << 32) + struct.unpack('>I', s[i:i+4])[0]
return out
def deflate_long(n, add_sign_padding=1):
"turns a long-int into a normalized byte string (adapted from Crypto.Util.number)"
# after much testing, this algorithm was deemed to be the fastest
s = ''
n = long(n)
while n > 0:
s = struct.pack('>I', n & 0xffffffffL) + s
n = n >> 32
# strip off leading zeros
for i in enumerate(s):
if i[1] != '\000':
break
else:
# only happens when n == 0
s = '\000'
i = (0,)
s = s[i[0]:]
if (ord(s[0]) >= 0x80) and add_sign_padding:
s = '\x00' + s
return s
class BERException (Exception):
pass
class BER(object): class BER(object):
"""
Robey's tiny little attempt at a BER decoder.
"""
def __init__(self, content=''): def __init__(self, content=''):
self.content = content self.content = content
@ -95,10 +65,10 @@ class BER(object):
return self.decode_sequence(data) return self.decode_sequence(data)
elif id == 2: elif id == 2:
# int # int
return inflate_long(data) return util.inflate_long(data)
else: else:
# 1: boolean (00 false, otherwise true) # 1: boolean (00 false, otherwise true)
raise Exception('Unknown ber encoding type %d (robey is lazy)' % id) raise BERException('Unknown ber encoding type %d (robey is lazy)' % id)
def decode_sequence(data): def decode_sequence(data):
out = [] out = []

View File

@ -1,3 +1,9 @@
#!/usr/bin/python
"""
Abstraction for an SSH2 channel.
"""
from message import Message from message import Message
from ssh_exception import SSHException from ssh_exception import SSHException
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, \
@ -13,12 +19,32 @@ def _set_nonblocking(fd):
fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK) fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
class Channel(object): class Channel (object):
""" """
Abstraction for an SSH2 channel. A secure tunnel across an SSH L{Transport}. A Channel is meant to behave
like a socket, and has an API that should be indistinguishable from the
python socket API.
Because SSH2 has a windowing kind of flow control, if you stop reading data
from a Channel and its buffer fills up, the server will be unable to send
you any more data until you read some of it. (This won't affect other
channels on the same transport -- all channels on a single transport are
flow-controlled independently.) Similarly, if the server isn't reading
data you send, calls to L{send} may block, unless you set a timeout. This
is exactly like a normal network socket, so it shouldn't be too surprising.
""" """
def __init__(self, chanid): def __init__(self, chanid):
"""
Create a new channel. The channel is not associated with any
particular session or L{Transport} until the Transport attaches it.
Normally you would only call this method from the constructor of a
subclass of L{Channel}.
@param chanid: the ID of this channel, as passed by an existing
L{Transport}.
@type chanid: int
"""
self.chanid = chanid self.chanid = chanid
self.transport = None self.transport = None
self.active = 0 self.active = 0
@ -84,6 +110,11 @@ class Channel(object):
self.transport._send_message(m) self.transport._send_message(m)
def invoke_shell(self): def invoke_shell(self):
"""
Request an interactive shell session on this channel. If the server
allows it, the channel will then be directly connected to the stdin
and stdout of the shell.
"""
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 SSHException('Channel is not open')
m = Message() m = Message()
@ -94,6 +125,14 @@ class Channel(object):
self.transport._send_message(m) self.transport._send_message(m)
def exec_command(self, command): def exec_command(self, command):
"""
Execute a command on the server. If the server allows it, the channel
will then be directly connected to the stdin and stdout of the command
being executed.
@param command: a shell command to execute.
@type command: string
"""
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 SSHException('Channel is not open')
m = Message() m = Message()
@ -105,6 +144,14 @@ class Channel(object):
self.transport._send_message(m) self.transport._send_message(m)
def invoke_subsystem(self, subsystem): def invoke_subsystem(self, subsystem):
"""
Request a subsystem on the server (for example, C{sftp}). If the
server allows it, the channel will then be directly connected to the
requested subsystem.
@param subsystem: name of the subsystem being requested.
@type subsystem: string
"""
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 SSHException('Channel is not open')
m = Message() m = Message()
@ -457,19 +504,81 @@ class Channel(object):
def check_pty_request(self, term, width, height, pixelwidth, pixelheight, modes): 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" """
I{(subclass override)}
Determine if a pseudo-terminal of the given dimensions (usually
requested for shell access) can be provided.
The default implementation always returns C{False}.
@param term: type of terminal requested (for example, C{"vt100"}).
@type term: string
@param width: width of screen in characters.
@type width: int
@param height: height of screen in characters.
@type height: int
@param pixelwidth: width of screen in pixels, if known (may be C{0} if
unknown).
@type pixelwidth: int
@param pixelheight: height of screen in pixels, if known (may be C{0}
if unknown).
@type pixelheight: int
@return: C{True} if the psuedo-terminal has been allocated; C{False}
otherwise.
@rtype: boolean
"""
return False return False
def check_shell_request(self): def check_shell_request(self):
"override me! return True if shell access will be provided" """
I{(subclass override)}
Determine if a shell will be provided to the client. If this method
returns C{True}, this channel should be connected to the stdin/stdout
of a shell.
The default implementation always returns C{False}.
@return: C{True} if this channel is now hooked up to a shell; C{False}
if a shell can't or won't be provided.
@rtype: boolean
"""
return False return False
def check_subsystem_request(self, name): def check_subsystem_request(self, name):
"override me! return True if the given subsystem can be provided" """
I{(subclass override)}
Determine if a requested subsystem will be provided to the client. If
this method returns C{True}, all future I/O through this channel will
be assumed to be connected to the requested subsystem. An example of
a subsystem is C{sftp}.
The default implementation always returns C{False}.
@return: C{True} if this channel is now hooked up to the requested
subsystem; C{False} if that subsystem can't or won't be provided.
@rtype: boolean
"""
return False return False
def check_window_change_request(self, width, height, pixelwidth, pixelheight): def check_window_change_request(self, width, height, pixelwidth, pixelheight):
"override me! return True if the pty was resized" """
I{(subclass override)}
Determine if the pseudo-terminal can be resized.
The default implementation always returns C{False}.
@param width: width of screen in characters.
@type width: int
@param height: height of screen in characters.
@type height: int
@param pixelwidth: width of screen in pixels, if known (may be C{0} if
unknown).
@type pixelwidth: int
@param pixelheight: height of screen in pixels, if known (may be C{0}
if unknown).
@type pixelheight: int
@return: C{True} if the terminal was resized; C{False} if not.
"""
return False return False
@ -707,13 +816,16 @@ class Channel(object):
self.in_window_sofar = 0 self.in_window_sofar = 0
class ChannelFile(object): class ChannelFile (object):
""" """
A file-like wrapper around Channel. A file-like wrapper around L{Channel}. A ChannelFile is created by calling
Doesn't have the non-portable side effect of Channel.fileno(). L{Channel.makefile} and doesn't have the non-portable side effect of
XXX Todo: the channel and its file-wrappers should be able to be closed or L{Channel.fileno}.
garbage-collected independently, for compatibility with real sockets and
their file-wrappers. Currently, closing does nothing but flush the buffer. @bug: To correctly emulate the file object created from a socket's
C{makefile} method, a L{Channel} and its C{ChannelFile} should be able to
be closed or garbage-collected independently. Currently, closing the
C{ChannelFile} does nothing but flush the buffer.
""" """
def __init__(self, channel, mode = "r", buf_size = -1): def __init__(self, channel, mode = "r", buf_size = -1):
@ -740,6 +852,11 @@ class ChannelFile(object):
self.softspace = False self.softspace = False
def __repr__(self): def __repr__(self):
"""
Returns a string representation of this object, for debugging.
@rtype: string
"""
return '<paramiko.ChannelFile from ' + repr(self.channel) + '>' return '<paramiko.ChannelFile from ' + repr(self.channel) + '>'
def __iter__(self): def __iter__(self):

View File

@ -1,17 +1,23 @@
#!/usr/bin/python #!/usr/bin/python
import base64 """
L{DSSKey}
"""
from ssh_exception import SSHException from ssh_exception import SSHException
from message import Message from message import Message
from util import inflate_long, deflate_long from util import inflate_long, deflate_long
from Crypto.PublicKey import DSA from Crypto.PublicKey import DSA
from Crypto.Hash import SHA from Crypto.Hash import SHA
from ber import BER from ber import BER, BERException
from pkey import PKey from pkey import PKey
from ssh_exception import SSHException
from util import format_binary
class DSSKey (PKey): class DSSKey (PKey):
"""
Representation of a DSS key which can be used to sign an verify SSH2
data.
"""
def __init__(self, msg=None, data=None): def __init__(self, msg=None, data=None):
self.valid = 0 self.valid = 0
@ -84,17 +90,15 @@ class DSSKey (PKey):
dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q))) dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q)))
return dss.verify(sigM, (sigR, sigS)) return dss.verify(sigM, (sigR, sigS))
def read_private_key_file(self, filename): def read_private_key_file(self, filename, password=None):
# private key file contains: # private key file contains:
# DSAPrivateKey = { version = 0, p, q, g, y, x } # DSAPrivateKey = { version = 0, p, q, g, y, x }
self.valid = 0 self.valid = 0
f = open(filename, 'r') data = self._read_private_key_file('DSA', filename, password)
lines = f.readlines() try:
f.close()
if lines[0].strip() != '-----BEGIN DSA PRIVATE KEY-----':
raise SSHException('not a valid DSA private key file')
data = base64.decodestring(''.join(lines[1:-1]))
keylist = BER(data).decode() keylist = BER(data).decode()
except BERException:
raise SSHException('Unable to parse key file')
if (type(keylist) != type([])) or (len(keylist) < 6) or (keylist[0] != 0): if (type(keylist) != type([])) or (len(keylist) < 6) or (keylist[0] != 0):
raise SSHException('not a valid DSA private key file (bad ber encoding)') raise SSHException('not a valid DSA private key file (bad ber encoding)')
self.p = keylist[1] self.p = keylist[1]

View File

@ -1,12 +1,27 @@
#!/usr/bin/python
"""
Common API for all public keys.
"""
from Crypto.Hash import MD5 from Crypto.Hash import MD5
from Crypto.Cipher import DES3
from message import Message from message import Message
from ssh_exception import SSHException, PasswordRequiredException
import util
import base64
class PKey (object): class PKey (object):
""" """
Base class for public keys. Base class for public keys.
""" """
# known encryption types for private key files:
_CIPHER_TABLE = {
'DES-EDE3-CBC': { 'cipher': DES3, 'keysize': 24, 'mode': DES3.MODE_CBC }
}
def __init__(self, msg=None, data=None): def __init__(self, msg=None, data=None):
""" """
Create a new instance of this public key type. If C{msg} is given, Create a new instance of this public key type. If C{msg} is given,
@ -101,15 +116,93 @@ class PKey (object):
""" """
return False return False
def read_private_key_file(self, filename): def read_private_key_file(self, filename, password=None):
""" """
Read private key contents from a file into this object. Read private key contents from a file into this object. If the private
key is encrypted and C{password} is not C{None}, the given password
will be used to decrypt the key (otherwise L{PasswordRequiredException}
is thrown).
@param filename: name of the file to read. @param filename: name of the file to read.
@type filename: string @type filename: string
@param password: an optional password to use to decrypt the key file,
if it's encrypted.
@type password: string
@raise IOError: if there was an error reading the file. @raise IOError: if there was an error reading the file.
@raise PasswordRequiredException: if the private key file is
encrypted, and C{password} is C{None}.
@raise SSHException: if the key file is invalid @raise SSHException: if the key file is invalid
@raise binascii.Error: on base64 decoding error @raise binascii.Error: on base64 decoding error
""" """
pass pass
def _read_private_key_file(self, tag, filename, password=None):
"""
Read an SSH2-format private key file, looking for a string of the type
C{"BEGIN xxx PRIVATE KEY"} for some C{xxx}, base64-decode the text we
find, and return it as a string. If the private key is encrypted and
C{password} is not C{None}, the given password will be used to decrypt
the key (otherwise L{PasswordRequiredException} is thrown).
@param tag: C{"RSA"} or C{"DSA"}, the tag used to mark the data block.
@type tag: string
@param filename: name of the file to read.
@type filename: string
@param password: an optional password to use to decrypt the key file,
if it's encrypted.
@type password: string
@return: data blob that makes up the private key.
@rtype: string
@raise IOError: if there was an error reading the file.
@raise PasswordRequiredException: if the private key file is
encrypted, and C{password} is C{None}.
@raise SSHException: if the key file is invalid.
@raise binascii.Error: on base64 decoding error.
"""
f = open(filename, 'r')
lines = f.readlines()
f.close()
start = 0
while (lines[start].strip() != '-----BEGIN ' + tag + ' PRIVATE KEY-----') and (start < len(lines)):
start += 1
if start >= len(lines):
raise SSHException('not a valid ' + tag + ' private key file')
# parse any headers first
headers = {}
start += 1
while start < len(lines):
l = lines[start].split(': ')
if len(l) == 1:
break
headers[l[0].lower()] = l[1].strip()
start += 1
# find end
end = start
while (lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----') and (end < len(lines)):
end += 1
# if we trudged to the end of the file, just try to cope.
data = base64.decodestring(''.join(lines[start:end]))
if not headers.has_key('proc-type'):
# unencryped: done
return data
# encrypted keyfile: will need a password
if headers['proc-type'] != '4,ENCRYPTED':
raise SSHException('Unknown private key structure "%s"' % headers['proc-type'])
try:
encryption_type, saltstr = headers['dek-info'].split(',')
except:
raise SSHException('Can\'t parse DEK-info in private key file')
if not self._CIPHER_TABLE.has_key(encryption_type):
raise SSHException('Unknown private key cipher "%s"' % encryption_type)
# if no password was passed in, raise an exception pointing out that we need one
if password is None:
raise PasswordRequiredException('Private key file is encrypted')
cipher = self._CIPHER_TABLE[encryption_type]['cipher']
keysize = self._CIPHER_TABLE[encryption_type]['keysize']
mode = self._CIPHER_TABLE[encryption_type]['mode']
# this confusing line turns something like '2F91' into '/\x91' (sorry, was feeling clever)
salt = ''.join([chr(int(saltstr[i:i+2], 16)) for i in range(0, len(saltstr), 2)])
key = util.generate_key_bytes(MD5, salt, password, keysize)
return cipher.new(key, mode, salt).decrypt(data)

View File

@ -1,28 +1,32 @@
#!/usr/bin/python
# utility functions for dealing with primes """
Utility functions for dealing with primes.
"""
from Crypto.Util import number from Crypto.Util import number
from util import bit_length, inflate_long import util
def generate_prime(bits, randpool): def _generate_prime(bits, randpool):
"primtive attempt at prime generation"
hbyte_mask = pow(2, bits % 8) - 1 hbyte_mask = pow(2, bits % 8) - 1
while 1: while 1:
# loop catches the case where we increment n into a higher bit-range # loop catches the case where we increment n into a higher bit-range
x = randpool.get_bytes((bits+7) // 8) x = randpool.get_bytes((bits+7) // 8)
if hbyte_mask > 0: if hbyte_mask > 0:
x = chr(ord(x[0]) & hbyte_mask) + x[1:] x = chr(ord(x[0]) & hbyte_mask) + x[1:]
n = inflate_long(x, 1) n = util.inflate_long(x, 1)
n |= 1 n |= 1
n |= (1 << (bits - 1)) n |= (1 << (bits - 1))
while not number.isPrime(n): while not number.isPrime(n):
n += 2 n += 2
if bit_length(n) == bits: if util.bit_length(n) == bits:
return n return n
def roll_random(randpool, n): def _roll_random(randpool, n):
"returns a random # from 0 to N-1" "returns a random # from 0 to N-1"
bits = bit_length(n-1) bits = util.bit_length(n-1)
bytes = (bits + 7) // 8 bytes = (bits + 7) // 8
hbyte_mask = pow(2, bits % 8) - 1 hbyte_mask = pow(2, bits % 8) - 1
@ -36,7 +40,7 @@ def roll_random(randpool, n):
x = randpool.get_bytes(bytes) x = randpool.get_bytes(bytes)
if hbyte_mask > 0: if hbyte_mask > 0:
x = chr(ord(x[0]) & hbyte_mask) + x[1:] x = chr(ord(x[0]) & hbyte_mask) + x[1:]
num = inflate_long(x, 1) num = util.inflate_long(x, 1)
if num < n: if num < n:
return num return num
@ -75,7 +79,7 @@ class ModulusPack (object):
# there's a bug in the ssh "moduli" file (yeah, i know: shock! dismay! # there's a bug in the ssh "moduli" file (yeah, i know: shock! dismay!
# call cnn!) where it understates the bit lengths of these primes by 1. # call cnn!) where it understates the bit lengths of these primes by 1.
# this is okay. # this is okay.
bl = bit_length(modulus) bl = util.bit_length(modulus)
if (bl != size) and (bl != size + 1): if (bl != size) and (bl != size + 1):
self.discarded.append((modulus, 'incorrectly reported bit length %d' % size)) self.discarded.append((modulus, 'incorrectly reported bit length %d' % size))
return return
@ -123,6 +127,6 @@ class ModulusPack (object):
if min > good: if min > good:
good = bitsizes[-1] good = bitsizes[-1]
# now pick a random modulus of this bitsize # now pick a random modulus of this bitsize
n = roll_random(self.randpool, len(self.pack[good])) n = _roll_random(self.randpool, len(self.pack[good]))
return self.pack[good][n] return self.pack[good][n]

View File

@ -1,14 +1,23 @@
#!/usr/bin/python #!/usr/bin/python
"""
L{RSAKey}
"""
from message import Message from message import Message
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto.Hash import SHA from Crypto.Hash import SHA, MD5
from ber import BER from Crypto.Cipher import DES3
from ber import BER, BERException
from util import format_binary, inflate_long, deflate_long from util import format_binary, inflate_long, deflate_long
from pkey import PKey from pkey import PKey
import base64 from ssh_exception import SSHException
class RSAKey (PKey): class RSAKey (PKey):
"""
Representation of an RSA key which can be used to sign and verify SSH2
data.
"""
def __init__(self, msg=None, data=''): def __init__(self, msg=None, data=''):
self.valid = 0 self.valid = 0
@ -68,19 +77,17 @@ class RSAKey (PKey):
rsa = RSA.construct((long(self.n), long(self.e))) rsa = RSA.construct((long(self.n), long(self.e)))
return rsa.verify(hash, (sig,)) return rsa.verify(hash, (sig,))
def read_private_key_file(self, filename): def read_private_key_file(self, filename, password=None):
# private key file contains: # private key file contains:
# RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p }
self.valid = 0 self.valid = 0
f = open(filename, 'r') data = self._read_private_key_file('RSA', filename, password)
lines = f.readlines() try:
f.close()
if lines[0].strip() != '-----BEGIN RSA PRIVATE KEY-----':
raise SSHException('not a valid RSA private key file')
data = base64.decodestring(''.join(lines[1:-1]))
keylist = BER(data).decode() keylist = BER(data).decode()
except BERException:
raise SSHException('Unable to parse key file')
if (type(keylist) != type([])) or (len(keylist) < 4) or (keylist[0] != 0): if (type(keylist) != type([])) or (len(keylist) < 4) or (keylist[0] != 0):
raise SSHException('not a valid RSA private key file (bad ber encoding)') raise SSHException('Not a valid RSA private key file (bad ber encoding)')
self.n = keylist[1] self.n = keylist[1]
self.e = keylist[2] self.e = keylist[2]
self.d = keylist[3] self.d = keylist[3]

View File

@ -1,4 +1,18 @@
#!/usr/bin/python
class SSHException(Exception): """
Exceptions defined by paramiko.
"""
class SSHException (Exception):
"""
Exception thrown by failures in SSH2 protocol negotiation or logic errors.
"""
pass pass
class PasswordRequiredException (SSHException):
"""
Exception thrown when a password is needed to unlock a private key file.
"""
pass

View File

@ -1,5 +1,9 @@
#!/usr/bin/python #!/usr/bin/python
"""
L{BaseTransport} handles the core SSH2 protocol.
"""
_MSG_DISCONNECT, _MSG_IGNORE, _MSG_UNIMPLEMENTED, _MSG_DEBUG, _MSG_SERVICE_REQUEST, \ _MSG_DISCONNECT, _MSG_IGNORE, _MSG_UNIMPLEMENTED, _MSG_DEBUG, _MSG_SERVICE_REQUEST, \
_MSG_SERVICE_ACCEPT = range(1, 7) _MSG_SERVICE_ACCEPT = range(1, 7)
_MSG_KEXINIT, _MSG_NEWKEYS = range(20, 22) _MSG_KEXINIT, _MSG_NEWKEYS = range(20, 22)
@ -164,10 +168,8 @@ class BaseTransport (threading.Thread):
if not self.active: if not self.active:
return '<paramiko.BaseTransport (unconnected)>' return '<paramiko.BaseTransport (unconnected)>'
out = '<paramiko.BaseTransport' out = '<paramiko.BaseTransport'
#if self.remote_version != '':
# out += ' (server version "%s")' % self.remote_version
if self.local_cipher != '': if self.local_cipher != '':
out += ' (cipher %s)' % self.local_cipher out += ' (cipher %s, %d bits)' % (self.local_cipher, self._cipher_info[self.local_cipher]['key-size'] * 8)
if len(self.channels) == 1: if len(self.channels) == 1:
out += ' (active; 1 open channel)' out += ' (active; 1 open channel)'
else: else:
@ -512,6 +514,8 @@ class BaseTransport (threading.Thread):
@raise SSHException: if the SSH2 negotiation fails, the host key @raise SSHException: if the SSH2 negotiation fails, the host key
supplied by the server is incorrect, or authentication fails. supplied by the server is incorrect, or authentication fails.
@since: doduo
""" """
if hostkeytype is not None: if hostkeytype is not None:
self.preferred_keys = [ hostkeytype ] self.preferred_keys = [ hostkeytype ]

View File

@ -1,5 +1,9 @@
#!/usr/bin/python #!/usr/bin/python
"""
Useful functions used by the rest of paramiko.
"""
import sys, struct, traceback import sys, struct, traceback
def inflate_long(s, always_positive=0): def inflate_long(s, always_positive=0):
@ -99,3 +103,37 @@ def bit_length(n):
def tb_strings(): def tb_strings():
return ''.join(traceback.format_exception(*sys.exc_info())).split('\n') return ''.join(traceback.format_exception(*sys.exc_info())).split('\n')
def generate_key_bytes(hashclass, salt, key, nbytes):
"""
Given a password, passphrase, or other human-source key, scramble it
through a secure hash into some keyworthy bytes. This specific algorithm
is used for encrypting/decrypting private key files.
@param hashclass: class from L{Crypto.Hash} that can be used as a secure
hashing function (like C{MD5} or C{SHA}).
@type hashclass: L{Crypto.Hash}
@param salt: data to salt the hash with.
@type salt: string
@param key: human-entered password or passphrase.
@type key: string
@param nbytes: number of bytes to generate.
@type nbytes: int
@return: key data
@rtype: string
"""
keydata = ''
digest = ''
if len(salt) > 8:
salt = salt[:8]
while nbytes > 0:
hash = hashclass.new()
if len(digest) > 0:
hash.update(digest)
hash.update(key)
hash.update(salt)
digest = hash.digest()
size = min(nbytes, len(digest))
keydata += digest[:size]
nbytes -= size
return keydata