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

initial import

(automatically generated log message)
This commit is contained in:
Robey Pointer 2003-11-04 08:34:24 +00:00
commit 51607386c7
19 changed files with 3424 additions and 0 deletions

504
LICENSE Normal file
View File

@ -0,0 +1,504 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

13
MANIFEST Normal file
View File

@ -0,0 +1,13 @@
README
ber.py
channel.py
dsskey.py
kex_gex.py
kex_group1.py
message.py
rsakey.py
secsh.py
setup.py
transport.py
util.py
demo.py

22
Makefile Normal file
View File

@ -0,0 +1,22 @@
# releases:
# aerodactyl (13sep03)
# bulbasaur
# charmander
RELEASE=bulbasaur
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:
#
# setup.py
# secsh.py
# README

13
NOTES Normal file
View File

@ -0,0 +1,13 @@
+-------------------+ +-----------------+
(Socket)InputStream ---> | secsh transport | <===> | secsh channel |
(Socket)OutputStream --> | (auth, pipe) | N | (buffer) |
+-------------------+ +-----------------+
@ feeder thread | |
- read InputStream | +-> InputStream
- feed into channel +---> OutputStream
buffers
SIS <-- @ --> (parse, find chan) --> secsh chan: buffer <-- SSHInputStream
SSHOutputStream --> secsh chan --> secsh transport --> SOS [no thread]

166
README Normal file
View File

@ -0,0 +1,166 @@
secsh 0.1
"bulbasaur" release, 18 sep 2003
(c) 2003 Robey Pointer <robey@lag.net>
http://www.lag.net/~robey/secsh/
*** WHAT
secsh is 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
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
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 secsh module.
it is written entirely in python (no C or platform-dependent code) and is
released under the GNU LGPL (lesser GPL).
*** REQUIREMENTS
python 2.3 <http://www.python.org/>
pyCrypto <http://www.amk.ca/python/code/crypto.html>
*** PORTABILITY
i code and test this library on Linux and MacOS X. for that reason, i'm
pretty sure that it works for all posix platforms, including MacOS. i also
think it will work on Windows, though i've never tested it there. if you
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
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.
so don't call "fileno()" on a Channel on Windows. (the problem is that pipes
are used to simulate an open socket, so that the ssh "socket" has an OS-level
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
the demo app (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
run on posix, because it uses select.
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
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
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
logs as it works. but the most interesting part is probably the code itself,
which hopefully demonstrates how you can use the secsh library.
*** USE
(this section could probably be improved a lot.)
first, create a Transport by passing in an existing socket (connected to the
desired server). call "start_client(event)", passing in an event which will
be triggered when the negotiation is finished (either successfully or not).
the event is required because each new Transport creates a new worker thread
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).
*** CHANGELOG
2003-08-24:
* implemented the other hashes: all 4 from the draft are working now
* added 'aes128-cbc' and '3des-cbc' cipher support
* fixed channel eof/close semantics
2003-09-12: version "aerodactyl"
* implemented group-exchange kex ("kex-gex")
* implemented RSA/DSA private key auth
2003-09-13:
* fixed inflate_long and deflate_long to handle negatives, even though
they're never used in the current ssh protocol
2003-09-14:
* fixed session_id handling: re-keying works now
* added the ability for a Channel to have a fileno() for select/poll
purposes, although this will cause worse window performance if the
client app isn't careful
2003-09-16: version "bulbasaur"
* fixed pipe (fileno) method to be nonblocking and it seems to work now
* fixed silly bug that caused large blocks to be truncated
2003-10-08:
* patch to fix Channel.invoke_subsystem and add Channel.exec_command
[vaclav dvorak]
* patch to add Channel.sendall [vaclav dvorak]
* patch to add Channel.shutdown [vaclav dvorak]
* patch to add Channel.makefile and a ChannelFile class which emulates
a python file object [vaclav dvorak]
2003-10-26:
* thread creation no longer happens during construction -- use the new
method "start_client(event)" to get things rolling
* re-keying now takes place after 1GB of data or 1 billion packets
(these limits can be easily changed per-session if needed)
*** MISSING LINKS
* 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)
* should have a simple synchronous method that handles all auth & events,
by pre-seeding the password or key info, and the expected key

224
auth_transport.py Normal file
View File

@ -0,0 +1,224 @@
#!/usr/bin/python
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 SSHException
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \
DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14
AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED = range(3)
class Transport(BaseTransport):
"BaseTransport with the auth framework hooked up"
def __init__(self, sock):
BaseTransport.__init__(self, sock)
self.auth_event = None
# for server mode:
self.auth_username = None
self.auth_fail_count = 0
self.auth_complete = 0
def request_auth(self):
m = Message()
m.add_byte(chr(MSG_SERVICE_REQUEST))
m.add_string('ssh-userauth')
self.send_message(m)
def auth_key(self, username, key, event):
if (not self.active) or (not self.initial_kex_done):
# we should never try to send the password unless we're on a secure link
raise SSHException('No existing session')
try:
self.lock.acquire()
self.auth_event = event
self.auth_method = 'publickey'
self.username = username
self.private_key = key
self.request_auth()
finally:
self.lock.release()
def auth_password(self, username, password, event):
'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 SSHException('No existing session')
try:
self.lock.acquire()
self.auth_event = event
self.auth_method = 'password'
self.username = username
self.password = password
self.request_auth()
finally:
self.lock.release()
def disconnect_service_not_available(self):
m = Message()
m.add_byte(chr(MSG_DISCONNECT))
m.add_int(DISCONNECT_SERVICE_NOT_AVAILABLE)
m.add_string('Service not available')
m.add_string('en')
self.send_message(m)
self.close()
def disconnect_no_more_auth(self):
m = Message()
m.add_byte(chr(MSG_DISCONNECT))
m.add_int(DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE)
m.add_string('No more auth methods available')
m.add_string('en')
self.send_message(m)
self.close()
def parse_service_request(self, m):
service = m.get_string()
if self.server_mode and (service == 'ssh-userauth'):
# accepted
m = Message()
m.add_byte(chr(MSG_SERVICE_ACCEPT))
m.add_string(service)
self.send_message(m)
return
# dunno this one
self.disconnect_service_not_available()
def parse_service_accept(self, m):
service = m.get_string()
if service == 'ssh-userauth':
self.log(DEBUG, 'userauth is OK')
m = Message()
m.add_byte(chr(MSG_USERAUTH_REQUEST))
m.add_string(self.username)
m.add_string('ssh-connection')
m.add_string(self.auth_method)
if self.auth_method == 'password':
m.add_boolean(0)
m.add_string(self.password.encode('UTF-8'))
elif self.auth_method == 'publickey':
m.add_boolean(1)
m.add_string(self.private_key.get_name())
m.add_string(str(self.private_key))
m.add_string(self.private_key.sign_ssh_session(self.randpool, self.H, self.username))
else:
raise SSHException('Unknown auth method "%s"' % self.auth_method)
self.send_message(m)
else:
self.log(DEBUG, 'Service request "%s" accepted (?)' % service)
def get_allowed_auths(self):
"override me!"
return 'password'
def check_auth_none(self, username):
"override me! return tuple of (int, string) ==> (auth status, list of acceptable auth methods)"
return (AUTH_FAILED, self.get_allowed_auths())
def check_auth_password(self, username, password):
"override me! return tuple of (int, string) ==> (auth status, list of acceptable auth methods)"
return (AUTH_FAILED, self.get_allowed_auths())
def check_auth_publickey(self, username, key):
"override me! return tuple of (int, string) ==> (auth status, list of acceptable auth methods)"
return (AUTH_FAILED, self.get_allowed_auths())
def parse_userauth_request(self, m):
if not self.server_mode:
# er, uh... what?
m = Message()
m.add_byte(chr(MSG_USERAUTH_FAILURE))
m.add_string('none')
m.add_boolean(0)
self.send_message(m)
return
if self.auth_complete:
# ignore
return
username = m.get_string()
service = m.get_string()
method = m.get_string()
if service != 'ssh-connection':
self.disconnect_service_not_available()
return
if (self.auth_username is not None) and (self.auth_username != username):
# trying to change username in mid-flight!
self.disconnect_no_more_auth()
return
if method == 'none':
result = self.check_auth_none(username)
elif method == 'password':
changereq = m.get_boolean()
password = m.get_string().decode('UTF-8')
if changereq:
# always treated as failure, since we don't support changing passwords, but collect
# the list of valid auth types from the callback anyway
newpassword = m.get_string().decode('UTF-8')
result = self.check_auth_password(username, password)
result = (AUTH_FAILED, result[1])
else:
result = self.check_auth_password(username, password)
elif method == 'publickey':
# FIXME
result = self.check_auth_none(username)
result = (AUTH_FAILED, result[1])
else:
result = self.check_auth_none(username)
result = (AUTH_FAILED, result[1])
# okay, send result
m = Message()
if result[0] == AUTH_SUCCESSFUL:
m.add_byte(chr(MSG_USERAUTH_SUCCESSFUL))
self.auth_complete = 1
else:
m.add_byte(chr(MSG_USERAUTH_FAILURE))
m.add_string(result[1])
if result[0] == AUTH_PARTIALLY_SUCCESSFUL:
m.add_boolean(1)
else:
m.add_boolean(0)
self.auth_fail_count += 1
self.send_message(m)
if self.auth_fail_count >= 10:
self.disconnect_no_more_auth()
def parse_userauth_success(self, m):
self.log(INFO, 'Authentication successful!')
self.authenticated = 1
if self.auth_event != None:
self.auth_event.set()
def parse_userauth_failure(self, m):
authlist = m.get_list()
partial = m.get_boolean()
if partial:
self.log(INFO, 'Authentication continues...')
self.log(DEBUG, 'Methods: ' + str(partial))
# FIXME - do something
pass
self.log(INFO, 'Authentication failed.')
self.authenticated = 0
self.close()
if self.auth_event != None:
self.auth_event.set()
def parse_userauth_banner(self, m):
banner = m.get_string()
lang = m.get_string()
self.log(INFO, 'Auth banner: ' + banner)
# who cares.
handler_table = BaseTransport.handler_table.copy()
handler_table.update({
MSG_SERVICE_REQUEST: parse_service_request,
MSG_SERVICE_ACCEPT: parse_service_accept,
MSG_USERAUTH_REQUEST: parse_userauth_request,
MSG_USERAUTH_SUCCESS: parse_userauth_success,
MSG_USERAUTH_FAILURE: parse_userauth_failure,
MSG_USERAUTH_BANNER: parse_userauth_banner,
})

112
ber.py Normal file
View File

@ -0,0 +1,112 @@
#!/usr/bin/python
import struct
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 BER(object):
def __init__(self, content=''):
self.content = content
self.idx = 0
def __str__(self):
return self.content
def __repr__(self):
return 'BER(' + repr(self.content) + ')'
def decode(self):
return self.decode_next()
def decode_next(self):
if self.idx >= len(self.content):
return None
id = ord(self.content[self.idx])
self.idx += 1
if (id & 31) == 31:
# identifier > 30
id = 0
while self.idx < len(self.content):
t = ord(self.content[self.idx])
if not (t & 0x80):
break
id = (id << 7) | (t & 0x7f)
self.idx += 1
if self.idx >= len(self.content):
return None
# now fetch length
size = ord(self.content[self.idx])
self.idx += 1
if size & 0x80:
# more complimicated...
# FIXME: theoretically should handle indefinite-length (0x80)
t = size & 0x7f
if self.idx + t > len(self.content):
return None
size = 0
while t > 0:
size = (size << 8) | ord(self.content[self.idx])
self.idx += 1
t -= 1
if self.idx + size > len(self.content):
# can't fit
return None
data = self.content[self.idx : self.idx + size]
self.idx += size
# now switch on id
if id == 0x30:
# sequence
return self.decode_sequence(data)
elif id == 2:
# int
return inflate_long(data)
else:
# 1: boolean (00 false, otherwise true)
raise Exception('Unknown ber encoding type %d (robey is lazy)' % id)
def decode_sequence(data):
out = []
b = BER(data)
while 1:
x = b.decode_next()
if x == None:
return out
out.append(x)
decode_sequence = staticmethod(decode_sequence)

608
channel.py Normal file
View File

@ -0,0 +1,608 @@
from message import Message
from secsh import SSHException
from transport import MSG_CHANNEL_REQUEST, MSG_CHANNEL_CLOSE, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, \
MSG_CHANNEL_EOF
import time, threading, logging, socket, os
from logging import DEBUG
# this is ugly, and won't work on windows
def set_nonblocking(fd):
import fcntl
fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
class Channel(object):
"""
Abstraction for a secsh channel.
"""
def __init__(self, chanid, transport):
self.chanid = chanid
self.transport = transport
self.active = 0
self.eof_received = 0
self.eof_sent = 0
self.in_buffer = ''
self.timeout = None
self.closed = 0
self.lock = threading.Lock()
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.pipe_rfd = self.pipe_wfd = None
def __repr__(self):
out = '<secsh.Channel %d' % self.chanid
if self.closed:
out += ' (closed)'
elif self.active:
if self.eof_received:
out += ' (EOF received)'
if self.eof_sent:
out += ' (EOF sent)'
out += ' (open) window=%d' % (self.out_window_size)
if len(self.in_buffer) > 0:
out += ' in-buffer=%d' % (len(self.in_buffer),)
out += ' -> ' + repr(self.transport)
out += '>'
return out
def log(self, level, msg):
self.logger.log(level, msg)
def set_window(self, window_size, max_packet_size):
self.in_window_size = window_size
self.in_max_packet_size = max_packet_size
# threshold of bytes we receive before we bother to send a window update
self.in_window_threshold = window_size // 10
self.in_window_sofar = 0
def set_server_channel(self, chanid, window_size, max_packet_size):
self.server_chanid = chanid
self.out_window_size = window_size
self.out_max_packet_size = max_packet_size
self.active = 1
def request_success(self, m):
self.log(DEBUG, 'Sesch channel %d request ok' % self.chanid)
return
def request_failed(self, m):
self.close()
def feed(self, m):
s = m.get_string()
try:
self.lock.acquire()
self.log(DEBUG, 'fed %d bytes' % len(s))
if self.pipe_wfd != None:
self.feed_pipe(s)
else:
self.in_buffer += s
self.in_buffer_cv.notifyAll()
self.log(DEBUG, '(out from feed)')
finally:
self.lock.release()
def window_adjust(self, m):
nbytes = m.get_int()
try:
self.lock.acquire()
self.log(DEBUG, 'window up %d' % nbytes)
self.out_window_size += nbytes
self.out_buffer_cv.notifyAll()
finally:
self.lock.release()
def handle_request(self, m):
key = m.get_string()
if key == 'exit-status':
self.exit_status = m.get_int()
return
elif key == 'xon-xoff':
# ignore
return
else:
self.log(DEBUG, 'Unhandled channel request "%s"' % key)
def handle_eof(self, m):
self.eof_received = 1
try:
self.lock.acquire()
self.in_buffer_cv.notifyAll()
if self.pipe_wfd != None:
os.close(self.pipe_wfd)
self.pipe_wfd = None
finally:
self.lock.release()
self.log(DEBUG, 'EOF received')
def handle_close(self, m):
self.close()
try:
self.lock.acquire()
self.in_buffer_cv.notifyAll()
self.out_buffer_cv.notifyAll()
if self.pipe_wfd != None:
os.close(self.pipe_wfd)
self.pipe_wfd = None
finally:
self.lock.release()
# API for external use
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:
raise SSHException('Channel is not open')
m = Message()
m.add_byte(chr(MSG_CHANNEL_REQUEST))
m.add_int(self.server_chanid)
m.add_string('pty-req')
m.add_boolean(0)
m.add_string(term)
m.add_int(width)
m.add_int(height)
# pixel height, width (usually useless)
m.add_int(0).add_int(0)
m.add_string('')
self.transport.send_message(m)
def invoke_shell(self):
if self.closed or self.eof_received or self.eof_sent or not self.active:
raise SSHException('Channel is not open')
m = Message()
m.add_byte(chr(MSG_CHANNEL_REQUEST))
m.add_int(self.server_chanid)
m.add_string('shell')
m.add_boolean(1)
self.transport.send_message(m)
def exec_command(self, command):
if self.closed or self.eof_received or self.eof_sent or not self.active:
raise SSHException('Channel is not open')
m = Message()
m.add_byte(chr(MSG_CHANNEL_REQUEST))
m.add_int(self.server_chanid)
m.add_string('exec')
m.add_boolean(1)
m.add_string(command)
self.transport.send_message(m)
def invoke_subsystem(self, subsystem):
if self.closed or self.eof_received or self.eof_sent or not self.active:
raise SSHException('Channel is not open')
m = Message()
m.add_byte(chr(MSG_CHANNEL_REQUEST))
m.add_int(self.server_chanid)
m.add_string('subsystem')
m.add_boolean(1)
m.add_string(subsystem)
self.transport.send_message(m)
def resize_pty(self, width=80, height=24):
if self.closed or self.eof_received or self.eof_sent or not self.active:
raise SSHException('Channel is not open')
m = Message()
m.add_byte(chr(MSG_CHANNEL_REQUEST))
m.add_int(self.server_chanid)
m.add_string('window-change')
m.add_boolean(0)
m.add_int(width)
m.add_int(height)
m.add_int(0).add_int(0)
self.transport.send_message(m)
def get_transport(self):
return self.transport
def set_name(self, name):
self.name = name
self.logger = logging.getLogger('secsh.chan.' + name)
def get_name(self):
return self.name
def send_eof(self):
if self.eof_sent:
return
m = Message()
m.add_byte(chr(MSG_CHANNEL_EOF))
m.add_int(self.server_chanid)
self.transport.send_message(m)
self.eof_sent = 1
self.log(DEBUG, 'EOF sent')
return
# socket equivalency methods...
def settimeout(self, timeout):
self.timeout = timeout
def gettimeout(self):
return self.timeout
def setblocking(self, blocking):
if blocking:
self.settimeout(None)
else:
self.settimeout(0.0)
def close(self):
if self.closed or not self.active:
return
self.send_eof()
m = Message()
m.add_byte(chr(MSG_CHANNEL_CLOSE))
m.add_int(self.server_chanid)
self.transport.send_message(m)
self.closed = 1
self.transport.unlink_channel(self.chanid)
def recv_ready(self):
"doesn't work if you've called fileno()"
try:
self.lock.acquire()
if len(self.in_buffer) == 0:
return 0
return 1
finally:
self.lock.release()
def recv(self, nbytes):
out = ''
try:
self.lock.acquire()
if self.pipe_rfd != None:
# use the pipe
return self.read_pipe(nbytes)
if len(self.in_buffer) == 0:
if self.closed or self.eof_received:
return out
# should we block?
if self.timeout == 0.0:
raise socket.timeout()
# loop here in case we get woken up but a different thread has grabbed everything in the buffer
timeout = self.timeout
while (len(self.in_buffer) == 0) and not self.closed and not self.eof_received:
then = time.time()
self.in_buffer_cv.wait(timeout)
if timeout != None:
timeout -= time.time() - then
if timeout <= 0.0:
raise socket.timeout()
# something in the buffer and we have the lock
if len(self.in_buffer) <= nbytes:
out = self.in_buffer
self.in_buffer = ''
else:
out = self.in_buffer[:nbytes]
self.in_buffer = self.in_buffer[nbytes:]
self.check_add_window(len(out))
finally:
self.lock.release()
return out
def send(self, s):
size = 0
if self.closed or self.eof_sent:
return size
try:
self.lock.acquire()
if self.out_window_size == 0:
# should we block?
if self.timeout == 0.0:
raise socket.timeout()
# loop here in case we get woken up but a different thread has filled the buffer
timeout = self.timeout
while self.out_window_size == 0:
then = time.time()
self.out_buffer_cv.wait(timeout)
if timeout != None:
timeout -= time.time() - then
if timeout <= 0.0:
raise socket.timeout()
# we have some window to squeeze into
if self.closed:
return 0
size = len(s)
if self.out_window_size < size:
size = self.out_window_size
if self.out_max_packet_size < size:
size = self.out_max_packet_size
m = Message()
m.add_byte(chr(MSG_CHANNEL_DATA))
m.add_int(self.server_chanid)
m.add_string(s[:size])
self.transport.send_message(m)
self.out_window_size -= size
finally:
self.lock.release()
return size
def sendall(self, s):
while s:
if self.closed:
# this doesn't seem useful, but it is the documented behavior of Socket
raise socket.error('Socket is closed')
sent = self.send(s)
s = s[sent:]
return None
def makefile(self, *params):
return ChannelFile(*([self] + list(params)))
def fileno(self):
"""
returns an OS-level fd which can be used for polling and reading (but
NOT for writing). this is primarily to allow python's \"select\" module
to work. the first time this function is called, a pipe is created to
simulate real OS-level fd behavior. because of this, two actual fds are
created: one to return and one to feed. this may be inefficient if you
plan to use many fds.
the channel's receive window will be updated as data comes in, not as
you read it, so if you fail to poll the channel often enough, it may
block ALL channels across the transport.
"""
try:
self.lock.acquire()
if self.pipe_rfd != None:
return self.pipe_rfd
# create the pipe and feed in any existing data
self.pipe_rfd, self.pipe_wfd = os.pipe()
set_nonblocking(self.pipe_wfd)
set_nonblocking(self.pipe_rfd)
if len(self.in_buffer) > 0:
x = self.in_buffer
self.in_buffer = ''
self.feed_pipe(x)
return self.pipe_rfd
finally:
self.lock.release()
def shutdown(self, how):
if (how == 0) or (how == 2):
# feign "read" shutdown
self.eof_received = 1
if (how == 1) or (how == 2):
self.send_eof()
# internal use...
def feed_pipe(self, data):
"you are already holding the lock"
if len(self.in_buffer) > 0:
self.in_buffer += data
return
try:
n = os.write(self.pipe_wfd, data)
if n < len(data):
# at least on linux, this will never happen, as the writes are
# considered atomic... but just in case.
self.in_buffer = data[n:]
self.check_add_window(n)
self.in_buffer_cv.notifyAll()
return
except OSError, e:
pass
if len(data) > 1:
# try writing just one byte then
x = data[0]
data = data[1:]
try:
os.write(self.pipe_wfd, x)
self.in_buffer = data
self.check_add_window(1)
self.in_buffer_cv.notifyAll()
return
except OSError, e:
data = x + data
# pipe is very full
self.in_buffer = data
self.in_buffer_cv.notifyAll()
def read_pipe(self, nbytes):
"you are already holding the lock"
try:
x = os.read(self.pipe_rfd, nbytes)
if len(x) > 0:
self.push_pipe(len(x))
return x
except OSError, e:
pass
# nothing in the pipe
if self.closed or self.eof_received:
return ''
# should we block?
if self.timeout == 0.0:
raise socket.timeout()
# loop here in case we get woken up but a different thread has grabbed everything in the buffer
timeout = self.timeout
while not self.closed and not self.eof_received:
then = time.time()
self.in_buffer_cv.wait(timeout)
if timeout != None:
timeout -= time.time() - then
if timeout <= 0.0:
raise socket.timeout()
try:
x = os.read(self.pipe_rfd, nbytes)
if len(x) > 0:
self.push_pipe(len(x))
return x
except OSError, e:
pass
pass
def push_pipe(self, nbytes):
# successfully read N bytes from the pipe, now re-feed the pipe if necessary
# (assumption: the pipe can hold as many bytes as were read out)
if len(self.in_buffer) == 0:
return
if len(self.in_buffer) <= nbytes:
os.write(self.pipe_wfd, self.in_buffer)
self.in_buffer = ''
return
x = self.in_buffer[:nbytes]
self.in_buffer = self.in_buffer[nbytes:]
os.write(self.pipd_wfd, x)
def unlink(self):
if self.closed or not self.active:
return
self.closed = 1
self.transport.unlink_channel(self.chanid)
def check_add_window(self, n):
# already holding the lock!
if self.closed or self.eof_received or not self.active:
return
self.log(DEBUG, 'addwindow %d' % n)
self.in_window_sofar += n
if self.in_window_sofar > self.in_window_threshold:
self.log(DEBUG, 'addwindow send %d' % self.in_window_sofar)
m = Message()
m.add_byte(chr(MSG_CHANNEL_WINDOW_ADJUST))
m.add_int(self.server_chanid)
m.add_int(self.in_window_sofar)
self.transport.send_message(m)
self.in_window_sofar = 0
class ChannelFile(object):
"""
A file-like wrapper around Channel.
Doesn't have the non-portable side effect of Channel.fileno().
XXX Todo: the channel and its file-wrappers should be able to be closed or
garbage-collected independently, for compatibility with real sockets and
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):
self.channel = channel
self.mode = mode
if buf_size < 0:
self.buf_size = 1024
self.line_buffered = 0
elif buf_size == 1:
self.buf_size = 1
self.line_buffered = 1
else:
self.buf_size = buf_size
self.line_buffered = 0
self.wbuffer = ""
self.rbuffer = ""
self.readable = ("r" in mode)
self.writable = ("w" in mode) or ("+" in mode) or ("a" in mode)
self.binary = ("b" in mode)
if not self.binary:
raise NotImplementedError("text mode not supported")
self.softspace = 0
def __iter__(self):
return self
def next(self):
line = self.readline()
if not line:
raise StopIteration
return line
def write(self, str):
if not self.writable:
raise IOError("file not open for writing")
if self.buf_size == 0 and not self.line_buffered:
self.channel.sendall(str)
return
self.wbuffer += str
if self.line_buffered:
last_newline_pos = self.wbuffer.rfind("\n")
if last_newline_pos >= 0:
self.channel.sendall(self.wbuffer[:last_newline_pos+1])
self.wbuffer = self.wbuffer[last_newline_pos+1:]
else:
if len(self.wbuffer) >= self.buf_size:
self.channel.sendall(self.wbuffer)
self.wbuffer = ""
return
def writelines(self, sequence):
for s in sequence:
self.write(s)
return
def flush(self):
self.channel.sendall(self.wbuffer)
self.wbuffer = ""
return
def read(self, size = None):
if not self.readable:
raise IOError("file not open for reading")
if size is None or size < 0:
result = self.rbuffer
self.rbuffer = ""
while not self.channel.eof_received:
new_data = self.channel.recv(65536)
if not new_data:
break
result += new_data
return result
if size <= len(self.rbuffer):
result = self.rbuffer[:size]
self.rbuffer = self.rbuffer[size:]
return result
while len(self.rbuffer) < size and not self.channel.eof_received:
new_data = self.channel.recv(max(self.buf_size, size-len(self.rbuffer)))
if not new_data:
break
self.rbuffer += new_data
result = self.rbuffer[:size]
self.rbuffer[size:]
return result
def readline(self, size = None):
line = ""
while "\n" not in line:
if size >= 0:
new_data = self.read(size - len(line))
else:
new_data = self.read(64)
if not new_data:
break
line += new_data
newline_pos = line.find("\n")
if newline_pos >= 0:
self.rbuffer = line[newline_pos+1:] + self.rbuffer
return line[:newline_pos+1]
elif len(line) > size:
self.rbuffer = line[size:] + self.rbuffer
return line[:size]
return line
def readlines(self, sizehint = None):
lines = []
while 1:
line = self.readline()
if not line:
break
lines.append(line)
return lines
def xreadlines(self):
return self
def close(self):
self.flush()
return
# vim: set shiftwidth=4 expandtab :

56
demo-server.py Executable file
View File

@ -0,0 +1,56 @@
#!/usr/bin/python
import sys, os, socket, threading, logging, traceback
import secsh
# setup logging
l = logging.getLogger("secsh")
l.setLevel(logging.DEBUG)
if len(l.handlers) == 0:
f = open('demo-server.log', 'w')
lh = logging.StreamHandler(f)
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.read_private_key_file('/home/robey/sshkey/ssh_host_rsa_key')
# now connect
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', 2200))
except Exception, e:
print '*** Bind failed: ' + str(e)
traceback.print_exc()
sys.exit(1)
try:
sock.listen(100)
client, addr = sock.accept()
except Exception, e:
print '*** Listen/accept failed: ' + str(e)
traceback.print_exc()
sys.exit(1)
try:
event = threading.Event()
t = secsh.Transport(client)
t.add_server_key(host_key)
t.ultra_debug = 1
t.start_server(event)
# print repr(t)
event.wait(10)
if not t.is_active():
print '*** SSH negotiation failed.'
sys.exit(1)
# print repr(t)
except Exception, e:
print '*** Caught exception: ' + str(e.__class__) + ': ' + str(e)
traceback.print_exc()
try:
t.close()
except:
pass
sys.exit(1)

180
demo.py Executable file
View File

@ -0,0 +1,180 @@
#!/usr/bin/python
import sys, os, socket, threading, getpass, logging, time, base64, select, termios, tty, traceback
import secsh
##### utility functions
def load_host_keys():
filename = os.environ['HOME'] + '/.ssh/known_hosts'
keys = {}
try:
f = open(filename, 'r')
except Exception, e:
print '*** Unable to open host keys file (%s)' % filename
return
for line in f:
keylist = line.split(' ')
if len(keylist) != 3:
continue
hostlist, keytype, key = keylist
hosts = hostlist.split(',')
for host in hosts:
if not keys.has_key(host):
keys[host] = {}
keys[host][keytype] = base64.decodestring(key)
f.close()
return keys
##### main demo
# setup logging
l = logging.getLogger("secsh")
l.setLevel(logging.DEBUG)
if len(l.handlers) == 0:
f = open('demo.log', 'w')
lh = logging.StreamHandler(f)
lh.setFormatter(logging.Formatter('%(levelname)-.3s [%(asctime)s] %(name)s: %(message)s', '%Y%m%d:%H%M%S'))
l.addHandler(lh)
username = ''
if len(sys.argv) > 1:
hostname = sys.argv[1]
if hostname.find('@') >= 0:
username, hostname = hostname.split('@')
else:
hostname = raw_input('Hostname: ')
if len(hostname) == 0:
print '*** Hostname required.'
sys.exit(1)
port = 22
if hostname.find(':') >= 0:
hostname, portstr = hostname.split(':')
port = int(portstr)
# now connect
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((hostname, port))
except Exception, e:
print '*** Connect failed: ' + str(e)
traceback.print_exc()
sys.exit(1)
try:
event = threading.Event()
t = secsh.Transport(sock)
t.ultra_debug = 1
t.start_client(event)
# print repr(t)
event.wait(10)
if not t.is_active():
print '*** SSH negotiation failed.'
sys.exit(1)
# print repr(t)
keys = load_host_keys()
keytype, hostkey = t.get_host_key()
if not keys.has_key(hostname):
print '*** WARNING: Unknown host key!'
elif not keys[hostname].has_key(keytype):
print '*** WARNING: Unknown host key!'
elif keys[hostname][keytype] != hostkey:
print '*** WARNING: Host key has changed!!!'
sys.exit(1)
else:
print '*** Host key OK.'
event.clear()
# get username
if username == '':
default_username = getpass.getuser()
username = raw_input('Username [%s]: ' % default_username)
if len(username) == 0:
username = default_username
# ask for what kind of authentication to try
default_auth = 'p'
auth = raw_input('Auth by (p)assword, (r)sa key, or (d)ss key? [%s] ' % default_auth)
if len(auth) == 0:
auth = default_auth
if auth == 'r':
key = secsh.RSAKey()
default_path = os.environ['HOME'] + '/.ssh/id_rsa'
path = raw_input('RSA key [%s]: ' % default_path)
if len(path) == 0:
path = default_path
key.read_private_key_file(path)
t.auth_key(username, key, event)
elif auth == 'd':
key = secsh.DSSKey()
default_path = os.environ['HOME'] + '/.ssh/id_dsa'
path = raw_input('DSS key [%s]: ' % default_path)
if len(path) == 0:
path = default_path
key.read_private_key_file(path)
t.auth_key(username, key, event)
else:
pw = getpass.getpass('Password for %s@%s: ' % (username, hostname))
t.auth_password(username, pw, event)
event.wait(10)
# print repr(t)
if not t.is_authenticated():
print '*** Authentication failed. :('
t.close()
sys.exit(1)
chan = t.open_session()
chan.get_pty()
chan.invoke_shell()
print '*** Here we go!'
print
try:
oldtty = termios.tcgetattr(sys.stdin)
tty.setraw(sys.stdin.fileno())
tty.setcbreak(sys.stdin.fileno())
chan.settimeout(0.0)
while 1:
r, w, e = select.select([chan, sys.stdin], [], [])
if chan in r:
try:
x = chan.recv(1024)
if len(x) == 0:
print
print '*** EOF\r\n',
break
sys.stdout.write(x)
sys.stdout.flush()
except socket.timeout:
pass
if sys.stdin in r:
# FIXME: reading 1 byte at a time is incredibly dumb.
x = sys.stdin.read(1)
if len(x) == 0:
print
print '*** Bye.\r\n',
break
chan.send(x)
finally:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
chan.close()
t.close()
except Exception, e:
print '*** Caught exception: ' + str(e.__class__) + ': ' + str(e)
traceback.print_exc()
try:
t.close()
except:
pass
sys.exit(1)

121
dsskey.py Normal file
View File

@ -0,0 +1,121 @@
#!/usr/bin/python
import base64
from message import Message
from transport import MSG_USERAUTH_REQUEST
from util import inflate_long, deflate_long
from Crypto.PublicKey import DSA
from Crypto.Hash import SHA
from ber import BER
from util import format_binary
class DSSKey(object):
def __init__(self, msg=None):
self.valid = 0
if (msg == None) or (msg.get_string() != 'ssh-dss'):
return
self.p = msg.get_mpint()
self.q = msg.get_mpint()
self.g = msg.get_mpint()
self.y = msg.get_mpint()
self.size = len(deflate_long(self.p, 0))
self.valid = 1
def __str__(self):
if not self.valid:
return ''
m = Message()
m.add_string('ssh-dss')
m.add_mpint(self.p)
m.add_mpint(self.q)
m.add_mpint(self.g)
m.add_mpint(self.y)
return str(m)
def get_name(self):
return 'ssh-dss'
def verify_ssh_sig(self, data, msg):
if not self.valid:
return 0
if len(str(msg)) == 40:
# spies.com bug: signature has no header
sig = str(msg)
else:
kind = msg.get_string()
if kind != 'ssh-dss':
return 0
sig = msg.get_string()
# pull out (r, s) which are NOT encoded as mpints
sigR = inflate_long(sig[:20], 1)
sigS = inflate_long(sig[20:], 1)
sigM = inflate_long(SHA.new(data).digest(), 1)
dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q)))
return dss.verify(sigM, (sigR, sigS))
def sign_ssh_data(self, data):
hash = SHA.new(data).digest()
dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x)))
# generate a suitable k
qsize = len(deflate_long(self.q, 0))
while 1:
k = inflate_long(randpool.get_bytes(qsize), 1)
if (k > 2) and (k < self.q):
break
r, s = dss.sign(inflate_long(hash, 1), k)
m = Message()
m.add_string('ssh-dss')
m.add_string(deflate_long(r, 0) + deflate_long(s, 0))
return str(m)
rsa = RSA.construct((long(self.n), long(self.e), long(self.d)))
sig = deflate_long(rsa.sign(self.pkcs1imify(hash), '')[0], 0)
m = Message()
m.add_string('ssh-rsa')
m.add_string(sig)
return str(m)
def read_private_key_file(self, filename):
# private key file contains:
# DSAPrivateKey = { version = 0, p, q, g, y, x }
self.valid = 0
try:
f = open(filename, 'r')
lines = f.readlines()
f.close()
except:
return
if lines[0].strip() != '-----BEGIN DSA PRIVATE KEY-----':
return
try:
data = base64.decodestring(''.join(lines[1:-1]))
except:
return
keylist = BER(data).decode()
if (type(keylist) != type([])) or (len(keylist) < 6) or (keylist[0] != 0):
return
self.p = keylist[1]
self.q = keylist[2]
self.g = keylist[3]
self.y = keylist[4]
self.x = keylist[5]
self.size = len(deflate_long(self.p, 0))
self.valid = 1
def sign_ssh_session(self, randpool, sid, username):
m = Message()
m.add_string(sid)
m.add_byte(chr(MSG_USERAUTH_REQUEST))
m.add_string(username)
m.add_string('ssh-connection')
m.add_string('publickey')
m.add_boolean(1)
m.add_string('ssh-dss')
m.add_string(str(self))
return self.sign_ssh_data(str(m))

180
kex_gex.py Normal file
View File

@ -0,0 +1,180 @@
#!/usr/bin/python
# variant on group1 (see kex_group1.py) where the prime "p" and generator "g"
# are provided by the server. a bit more work is required on our side (and a
# LOT more on the server side).
from message import Message, inflate_long, deflate_long
from secsh import SSHException
from transport import MSG_NEWKEYS
from Crypto.Hash import SHA
from Crypto.Util import number
from logging import DEBUG
MSG_KEXDH_GEX_GROUP, MSG_KEXDH_GEX_INIT, MSG_KEXDH_GEX_REPLY, MSG_KEXDH_GEX_REQUEST = range(31, 35)
class KexGex(object):
name = 'diffie-hellman-group-exchange-sha1'
min_bits = 1024
max_bits = 8192
preferred_bits = 2048
def __init__(self, transport):
self.transport = transport
def start_kex(self):
if self.transport.server_mode:
self.transport.expected_packet = MSG_KEXDH_GEX_REQUEST
return
# request a bit range: we accept (min_bits) to (max_bits), but prefer
# (preferred_bits). according to the spec, we shouldn't pull the
# minimum up above 1024.
m = Message()
m.add_byte(chr(MSG_KEXDH_GEX_REQUEST))
m.add_int(self.min_bits)
m.add_int(self.preferred_bits)
m.add_int(self.max_bits)
self.transport.send_message(m)
self.transport.expected_packet = MSG_KEXDH_GEX_GROUP
def parse_next(self, ptype, m):
if ptype == MSG_KEXDH_GEX_REQUEST:
return self.parse_kexdh_gex_request(m)
elif ptype == MSG_KEXDH_GEX_GROUP:
return self.parse_kexdh_gex_group(m)
elif ptype == MSG_KEXDH_GEX_INIT:
return self.parse_kexdh_gex_init(m)
elif ptype == MSG_KEXDH_GEX_REPLY:
return self.parse_kexdh_gex_reply(m)
raise SSHException('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):
# generate an "x" (1 < x < (p-1)/2).
q = (self.p - 1) // 2
qnorm = deflate_long(q, 0)
qhbyte = ord(qnorm[0])
bytes = len(qnorm)
qmask = 0xff
while not (qhbyte & 0x80):
qhbyte <<= 1
qmask >>= 1
while 1:
self.transport.randpool.stir()
x_bytes = self.transport.randpool.get_bytes(bytes)
x_bytes = chr(ord(x_bytes[0]) & qmask) + x_bytes[1:]
x = inflate_long(x_bytes, 1)
if (x > 1) and (x < q):
break
self.x = x
def parse_kexdh_gex_request(self, m):
min = m.get_int()
preferred = m.get_int()
max = m.get_int()
# smoosh the user's preferred size into our own limits
if preferred > self.max_bits:
preferred = self.max_bits
if preferred < self.min_bits:
preferred = self.min_bits
# now save a copy
self.min_bits = min
self.preferred_bits = preferred
self.max_bits = max
# generate prime
while 1:
self.transport.log(DEBUG, 'stir...')
self.transport.randpool.stir()
self.transport.log(DEBUG, 'get-prime %d...' % preferred)
self.p = number.getRandomNumber(preferred, self.transport.randpool.get_bytes)
self.transport.log(DEBUG, 'got ' + repr(self.p))
if number.isPrime((self.p - 1) // 2):
break
self.g = 2
m = Message()
m.add_byte(chr(MSG_KEXDH_GEX_GROUP))
m.add_mpint(self.p)
m.add_mpint(self.g)
self.transport.send_message(m)
self.transport.expected_packet = MSG_KEXDH_GEX_INIT
def parse_kexdh_gex_group(self, m):
self.p = m.get_mpint()
self.g = m.get_mpint()
# reject if p's bit length < 1024 or > 8192
bitlen = self.bit_length(self.p)
if (bitlen < 1024) or (bitlen > 8192):
raise SSHException('Server-generated gex p (don\'t ask) is out of range (%d bits)' % bitlen)
self.transport.log(DEBUG, 'Got server p (%d bits)' % bitlen)
self.generate_x()
# now compute e = g^x mod p
self.e = pow(self.g, self.x, self.p)
m = Message()
m.add_byte(chr(MSG_KEXDH_GEX_INIT))
m.add_mpint(self.e)
self.transport.send_message(m)
self.transport.expected_packet = MSG_KEXDH_GEX_REPLY
def parse_kexdh_gex_init(self, m):
self.e = m.get_mpint()
if (self.e < 1) or (self.e > self.p - 1):
raise SSHException('Client kex "e" is out of range')
self.generate_x()
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 || min || n || max || p || g || e || f || K)
hm = Message().add(self.transport.remote_version).add(self.transport.local_version)
hm.add(self.transport.remote_kex_init).add(self.transport.local_kex_init).add(key)
hm.add_int(self.min_bits)
hm.add_int(self.preferred_bits)
hm.add_int(self.max_bits)
hm.add_mpint(self.p)
hm.add_mpint(self.g)
hm.add(self.e).add(self.f).add(K)
H = SHA.new(str(hm)).digest()
self.transport.set_K_H(K, H)
# sign it
sig = self.transport.get_server_key().sign_ssh_data(H)
# send reply
m = Message()
m.add_byte(chr(MSG_KEXDH_GEX_REPLY))
m.add_string(key)
m.add_mpint(self.f)
m.add_string(sig)
self.transport.send_message(m)
self.transport.activate_outbound()
self.transport.expected_packet = MSG_NEWKEYS
def parse_kexdh_gex_reply(self, m):
host_key = m.get_string()
self.f = m.get_mpint()
sig = m.get_string()
if (self.f < 1) or (self.f > self.p - 1):
raise SSHException('Server kex "f" is out of range')
K = pow(self.f, self.x, self.p)
# okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K)
hm = Message().add(self.transport.local_version).add(self.transport.remote_version)
hm.add(self.transport.local_kex_init).add(self.transport.remote_kex_init).add(host_key)
hm.add_int(self.min_bits)
hm.add_int(self.preferred_bits)
hm.add_int(self.max_bits)
hm.add_mpint(self.p)
hm.add_mpint(self.g)
hm.add(self.e).add(self.f).add(K)
self.transport.set_K_H(K, SHA.new(str(hm)).digest())
self.transport.verify_key(host_key, sig)
self.transport.activate_outbound()
self.transport.expected_packet = MSG_NEWKEYS

104
kex_group1.py Normal file
View File

@ -0,0 +1,104 @@
#!/usr/bin/python
# standard SSH key exchange ("kex" if you wanna sound cool):
# diffie-hellman of 1024 bit key halves, using a known "p" prime and
# "g" generator.
from message import Message, inflate_long
from secsh import SSHException
from transport import MSG_NEWKEYS
from Crypto.Hash import SHA
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
MSG_KEXDH_INIT, MSG_KEXDH_REPLY = range(30, 32)
# draft-ietf-secsh-transport-09.txt, page 17
P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFFL
G = 2
class KexGroup1(object):
name = 'diffie-hellman-group1-sha1'
def __init__(self, transport):
self.transport = transport
def generate_x(self):
# generate an "x" (1 < x < q), where q is (p-1)/2.
# p is a 128-byte (1024-bit) number, where the first 64 bits are 1.
# therefore q can be approximated as a 2^1023. we drop the subset of
# potential x where the first 63 bits are 1, because some of those will be
# larger than q (but this is a tiny tiny subset of potential x).
while 1:
self.transport.randpool.stir()
x_bytes = self.transport.randpool.get_bytes(128)
x_bytes = chr(ord(x_bytes[0]) & 0x7f) + x_bytes[1:]
if (x_bytes[:8] != '\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF') and \
(x_bytes[:8] != '\x00\x00\x00\x00\x00\x00\x00\x00'):
break
self.x = inflate_long(x_bytes)
def start_kex(self):
self.generate_x()
if self.transport.server_mode:
# compute f = g^x mod p, but don't send it yet
self.f = pow(G, self.x, P)
self.transport.expected_packet = MSG_KEXDH_INIT
return
# compute e = g^x mod p (where g=2), and send it
self.e = pow(G, self.x, P)
m = Message()
m.add_byte(chr(MSG_KEXDH_INIT))
m.add_mpint(self.e)
self.transport.send_message(m)
self.transport.expected_packet = MSG_KEXDH_REPLY
def parse_next(self, ptype, m):
if self.transport.server_mode and (ptype == MSG_KEXDH_INIT):
return self.parse_kexdh_init(m)
elif not self.transport.server_mode and (ptype == MSG_KEXDH_REPLY):
return self.parse_kexdh_reply(m)
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 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)
hm = Message().add(self.transport.local_version).add(self.transport.remote_version)
hm.add(self.transport.local_kex_init).add(self.transport.remote_kex_init).add(host_key)
hm.add(self.e).add(self.f).add(K)
self.transport.set_K_H(K, SHA.new(str(hm)).digest())
self.transport.verify_key(host_key, sig)
self.transport.activate_outbound()
self.transport.expected_packet = MSG_NEWKEYS
def parse_kexdh_init(self, m):
# server mode
self.e = m.get_mpint()
if (self.e < 1) or (self.e > P - 1):
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)
hm = Message().add(self.transport.remote_version).add(self.transport.local_version)
hm.add(self.transport.remote_kex_init).add(self.transport.local_kex_init).add(key)
hm.add(self.e).add(self.f).add(K)
H = SHA.new(str(hm)).digest()
self.transport.set_K_H(K, H)
# sign it
sig = self.transport.get_server_key().sign_ssh_data(H)
# send reply
m = Message()
m.add_byte(chr(MSG_KEXDH_REPLY))
m.add_string(key)
m.add_mpint(self.f)
m.add_string(sig)
self.transport.send_message(m)
self.transport.activate_outbound()
self.transport.expected_packet = MSG_NEWKEYS

119
message.py Normal file
View File

@ -0,0 +1,119 @@
# implementation of a secsh "message"
import string, types, struct
from util import inflate_long, deflate_long
class Message(object):
"represents the encoding of a secsh message"
def __init__(self, content=''):
self.packet = content
self.idx = 0
self.seqno = -1
def __str__(self):
return self.packet
def __repr__(self):
return 'Message(' + repr(self.packet) + ')'
def get_remainder(self):
"remaining bytes still unparsed"
return self.packet[self.idx:]
def get_so_far(self):
"bytes that have been parsed"
return self.packet[:self.idx]
def get_bytes(self, n):
if self.idx + n > len(self.packet):
return '\x00'*n
b = self.packet[self.idx:self.idx+n]
self.idx = self.idx + n
return b
def get_byte(self):
return self.get_bytes(1)
def get_boolean(self):
b = self.get_bytes(1)
if b == '\x00':
return 0
else:
return 1
def get_int(self):
x = self.packet
i = self.idx
if i + 4 > len(x):
return 0
n = struct.unpack('>I', x[i:i+4])[0]
self.idx = i+4
return n
def get_mpint(self):
return inflate_long(self.get_string())
def get_string(self):
l = self.get_int()
if self.idx + l > len(self.packet):
return ''
str = self.packet[self.idx:self.idx+l]
self.idx = self.idx + l
return str
def get_list(self):
str = self.get_string()
l = string.split(str, ',')
return l
def add_bytes(self, b):
self.packet = self.packet + b
return self
def add_byte(self, b):
self.packet = self.packet + b
return self
def add_boolean(self, b):
if b:
self.add_byte('\x01')
else:
self.add_byte('\x00')
return self
def add_int(self, n):
self.packet = self.packet + struct.pack('>I', n)
return self
def add_mpint(self, z):
"this only works on positive numbers"
self.add_string(deflate_long(z))
return self
def add_string(self, s):
self.add_int(len(s))
self.packet = self.packet + s
return self
def add_list(self, l):
out = string.join(l, ',')
self.add_int(len(out))
self.packet = self.packet + out
return self
def add(self, i):
if type(i) == types.StringType:
return self.add_string(i)
elif type(i) == types.IntType:
return self.add_int(i)
elif type(i) == types.LongType:
if i > 0xffffffffL:
return self.add_mpint(i)
else:
return self.add_int(i)
elif type(i) == types.ListType:
return self.add_list(i)
else:
raise exception('Unknown type')

102
rsakey.py Normal file
View File

@ -0,0 +1,102 @@
#!/usr/bin/python
from message import Message
from transport import MSG_USERAUTH_REQUEST
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA
from ber import BER
from util import format_binary, inflate_long, deflate_long
import base64
class RSAKey(object):
def __init__(self, msg=None):
self.valid = 0
if (msg == None) or (msg.get_string() != 'ssh-rsa'):
return
self.e = msg.get_mpint()
self.n = msg.get_mpint()
self.size = len(deflate_long(self.n, 0))
self.valid = 1
def __str__(self):
if not self.valid:
return ''
m = Message()
m.add_string('ssh-rsa')
m.add_mpint(self.e)
m.add_mpint(self.n)
return str(m)
def get_name(self):
return 'ssh-rsa'
def pkcs1imify(self, data):
"""
turn a 20-byte SHA1 hash into a blob of data as large as the key's N,
using PKCS1's \"emsa-pkcs1-v1_5\" encoding. totally bizarre.
"""
SHA1_DIGESTINFO = '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'
filler = '\xff' * (self.size - len(SHA1_DIGESTINFO) - len(data) - 3)
return '\x00\x01' + filler + '\x00' + SHA1_DIGESTINFO + data
def verify_ssh_sig(self, data, msg):
if (not self.valid) or (msg.get_string() != 'ssh-rsa'):
return 0
sig = inflate_long(msg.get_string(), 1)
# verify the signature by SHA'ing the data and encrypting it using the
# public key. some wackiness ensues where we "pkcs1imify" the 20-byte
# hash into a string as long as the RSA key.
hash = inflate_long(self.pkcs1imify(SHA.new(data).digest()), 1)
rsa = RSA.construct((long(self.n), long(self.e)))
return rsa.verify(hash, (sig,))
def sign_ssh_data(self, data):
hash = SHA.new(data).digest()
rsa = RSA.construct((long(self.n), long(self.e), long(self.d)))
sig = deflate_long(rsa.sign(self.pkcs1imify(hash), '')[0], 0)
m = Message()
m.add_string('ssh-rsa')
m.add_string(sig)
return str(m)
def read_private_key_file(self, filename):
# private key file contains:
# RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p }
self.valid = 0
try:
f = open(filename, 'r')
lines = f.readlines()
f.close()
except:
return
if lines[0].strip() != '-----BEGIN RSA PRIVATE KEY-----':
return
try:
data = base64.decodestring(''.join(lines[1:-1]))
except:
return
keylist = BER(data).decode()
if (type(keylist) != type([])) or (len(keylist) < 4) or (keylist[0] != 0):
return
self.n = keylist[1]
self.e = keylist[2]
self.d = keylist[3]
# not really needed
self.p = keylist[4]
self.q = keylist[5]
self.size = len(deflate_long(self.n, 0))
self.valid = 1
def sign_ssh_session(self, randpool, sid, username):
m = Message()
m.add_string(sid)
m.add_byte(chr(MSG_USERAUTH_REQUEST))
m.add_string(username)
m.add_string('ssh-connection')
m.add_string('publickey')
m.add_boolean(1)
m.add_string('ssh-rsa')
m.add_string(str(self))
return self.sign_ssh_data(str(m))

23
secsh.py Normal file
View File

@ -0,0 +1,23 @@
#!/usr/bin/python
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.')
# FIXME rename
class SSHException(Exception):
pass
from auth_transport import Transport
from channel import Channel
from rsakey import RSAKey
from dsskey import DSSKey
__author__ = "Robey Pointer <robey@lag.net>"
__date__ = "18 Sep 2003"
__version__ = "0.1-bulbasaur"
__credits__ = "Huzzah!"

30
setup.py Normal file
View File

@ -0,0 +1,30 @@
from distutils.core import setup
longdesc = '''
This is a library for making client-side SSH2 connections (server-side is
coming soon). All major ciphers and hash methods are supported.
Required packages:
pyCrypto
'''
setup(name = "secsh",
version = "0.1-bulbasaur",
description = "SSH2 protocol library",
author = "Robey Pointer",
author_email = "robey@lag.net",
url = "http://www.lag.net/~robey/secsh/",
py_modules = [ 'secsh', 'transport', 'channel', 'message', 'util', 'ber',
'kex_group1', 'kex_gex', 'rsakey', 'dsskey' ],
scripts = [ 'demo.py' ],
download_url = 'http://www.lag.net/~robey/secsh/secsh-0.1-bulbasaur.zip',
license = 'LGPL',
platforms = 'Posix; MacOS X; Windows',
classifiers = [ 'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
'Operating System :: OS Independent',
'Topic :: Internet',
'Topic :: Security :: Cryptography' ],
long_description = longdesc,
)

758
transport.py Normal file
View File

@ -0,0 +1,758 @@
#!/usr/bin/python
MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED, MSG_DEBUG, MSG_SERVICE_REQUEST, \
MSG_SERVICE_ACCEPT = range(1, 7)
MSG_KEXINIT, MSG_NEWKEYS = range(20, 22)
MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \
MSG_USERAUTH_BANNER = range(50, 54)
MSG_USERAUTH_PK_OK = 60
MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \
MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA, \
MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST, \
MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE = range(90, 101)
import sys, os, string, threading, socket, logging, struct
from message import Message
from channel import Channel
from secsh import SSHException
from util import format_binary, safe_string, inflate_long, deflate_long
from rsakey import RSAKey
from dsskey import DSSKey
from kex_group1 import KexGroup1
from kex_gex import KexGex
# these come from PyCrypt
# http://www.amk.ca/python/writing/pycrypt/
# i believe this on the standards track.
# PyCrypt compiled for Win32 can be downloaded from the HashTar homepage:
# http://nitace.bsd.uchicago.edu:8080/hashtar
from Crypto.Util.randpool import PersistentRandomPool, RandomPool
from Crypto.Cipher import Blowfish, AES, DES3
from Crypto.Hash import SHA, MD5, HMAC
from Crypto.PublicKey import RSA
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
# channel request failed reasons:
CONNECTION_FAILED_CODE = {
1: 'Administratively prohibited',
2: 'Connect failed',
3: 'Unknown channel type',
4: 'Resource shortage'
}
# keep a crypto-strong PRNG nearby
try:
randpool = PersistentRandomPool(os.getenv('HOME') + '/.randpool')
except:
# the above will likely fail on Windows - fall back to non-persistent random pool
randpool = RandomPool()
randpool.randomize()
class BaseTransport(threading.Thread):
'''
An SSH Transport attaches to a stream (usually a socket), negotiates an
encrypted session, authenticates, and then creates stream tunnels, called
"channels", across the session. Multiple channels can be multiplexed
across a single session (and often are, in the case of port forwardings).
Transport expects to receive a "socket-like object" to talk to the SSH
server. This means it has a method "settimeout" which sets a timeout for
read/write calls, and a method "send()" to write bytes and "recv()" to
read bytes. "recv" returns from 1 to n bytes, or 0 if the stream has been
closed. EOFError may also be raised on a closed stream. (A return value
of 0 is converted to an EOFError internally.) "send(s)" writes from 1 to
len(s) bytes, and returns the number of bytes written, or returns 0 if the
stream has been closed. As with instream, EOFError may be raised instead
of returning 0.
FIXME: Describe events here.
'''
PROTO_ID = '2.0'
CLIENT_ID = 'pyssh_1.1'
preferred_ciphers = [ 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc' ]
preferred_macs = [ 'hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96' ]
preferred_keys = [ 'ssh-rsa', 'ssh-dss' ]
preferred_kex = [ 'diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1' ]
cipher_info = {
'blowfish-cbc': { 'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16 },
'aes128-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 16 },
'aes256-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 32 },
'3des-cbc': { 'class': DES3, 'mode': DES3.MODE_CBC, 'block-size': 8, 'key-size': 24 },
}
mac_info = {
'hmac-sha1': { 'class': SHA, 'size': 20 },
'hmac-sha1-96': { 'class': SHA, 'size': 12 },
'hmac-md5': { 'class': MD5, 'size': 16 },
'hmac-md5-96': { 'class': MD5, 'size': 12 },
}
kex_info = {
'diffie-hellman-group1-sha1': KexGroup1,
'diffie-hellman-group-exchange-sha1': KexGex,
}
REKEY_PACKETS = pow(2, 30)
REKEY_BYTES = pow(2, 30)
def __init__(self, sock):
threading.Thread.__init__(self)
self.randpool = randpool
self.sock = sock
self.sock.settimeout(0.1)
# negotiated crypto parameters
self.local_version = 'SSH-' + self.PROTO_ID + '-' + self.CLIENT_ID
self.remote_version = ''
self.block_size_out = self.block_size_in = 8
self.local_mac_len = self.remote_mac_len = 0
self.engine_in = self.engine_out = None
self.local_cipher = self.remote_cipher = ''
self.sequence_number_in = self.sequence_number_out = 0L
self.local_kex_init = self.remote_kex_init = None
self.session_id = None
# /negotiated crypto parameters
self.expected_packet = 0
self.active = 0
self.initial_kex_done = 0
self.write_lock = threading.Lock() # lock around outbound writes (packet computation)
self.lock = threading.Lock() # synchronization (always higher level than write_lock)
self.authenticated = 0
self.channels = { } # (id -> Channel)
self.channel_events = { } # (id -> Event)
self.channel_counter = 1
self.logger = logging.getLogger('secsh.transport')
self.window_size = 65536
self.max_packet_size = 2048
self.ultra_debug = 0
# used for noticing when to re-key:
self.received_bytes = 0
self.received_packets = 0
self.received_packets_overflow = 0
# user-defined event callbacks:
self.completion_event = None
# server mode:
self.server_mode = 0
self.server_key_dict = { }
def start_client(self, event=None):
self.completion_event = event
self.start()
def start_server(self, event=None):
self.server_mode = 1
self.completion_event = event
self.start()
def add_server_key(self, key):
self.server_key_dict[key.get_name()] = key
def get_server_key(self):
try:
return self.server_key_dict[self.host_key_type]
except KeyError:
return None
def __repr__(self):
if not self.active:
return '<secsh.Transport (unconnected)>'
out = '<sesch.Transport'
#if self.remote_version != '':
# out += ' (server version "%s")' % self.remote_version
if self.local_cipher != '':
out += ' (cipher %s)' % self.local_cipher
if self.authenticated:
if len(self.channels) == 1:
out += ' (active; 1 open channel)'
else:
out += ' (active; %d open channels)' % len(self.channels)
elif self.initial_kex_done:
out += ' (connected; awaiting auth)'
else:
out += ' (connecting)'
out += '>'
return out
def log(self, level, msg):
if type(msg) == type([]):
for m in msg:
self.logger.log(level, m)
else:
self.logger.log(level, msg)
def close(self):
self.active = 0
self.engine_in = self.engine_out = None
self.sequence_number_in = self.sequence_number_out = 0L
for chan in self.channels.values():
chan.unlink()
def get_host_key(self):
'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):
raise SSHException('No existing session')
key_msg = Message(self.host_key)
key_type = key_msg.get_string()
return key_type, self.host_key
def is_active(self):
return self.active
def is_authenticated(self):
return self.authenticated and self.active
def open_session(self):
return self.open_channel('session')
def open_channel(self, kind):
chan = None
try:
self.lock.acquire()
chanid = self.channel_counter
self.channel_counter += 1
m = Message()
m.add_byte(chr(MSG_CHANNEL_OPEN))
m.add_string(kind)
m.add_int(chanid)
m.add_int(self.window_size)
m.add_int(self.max_packet_size)
self.channels[chanid] = chan = Channel(chanid, self)
self.channel_events[chanid] = event = threading.Event()
chan.set_window(self.window_size, self.max_packet_size)
self.send_message(m)
finally:
self.lock.release()
while 1:
event.wait(0.1);
if not self.active:
return None
if event.isSet():
break
try:
self.lock.acquire()
if not self.channels.has_key(chanid):
chan = None
finally:
self.lock.release()
return chan
def unlink_channel(self, chanid):
try:
self.lock.acquire()
if self.channels.has_key(chanid):
del self.channels[chanid]
finally:
self.lock.release()
def read_all(self, n):
out = ''
while n > 0:
try:
x = self.sock.recv(n)
if len(x) == 0:
raise EOFError()
out += x
n -= len(x)
except socket.timeout:
if not self.active:
raise EOFError()
return out
def write_all(self, out):
while len(out) > 0:
n = self.sock.send(out)
if n <= 0:
raise EOFError()
if n == len(out):
return
out = out[n:]
return
def build_packet(self, payload):
# pad up at least 4 bytes, to nearest block-size (usually 8)
bsize = self.block_size_out
padding = 3 + bsize - ((len(payload) + 8) % bsize)
packet = struct.pack('>I', len(payload) + padding + 1)
packet += chr(padding)
packet += payload
packet += randpool.get_bytes(padding)
return packet
def send_message(self, data):
# encrypt this sucka
packet = self.build_packet(str(data))
if self.ultra_debug:
self.log(DEBUG, format_binary(packet, 'OUT: '))
if self.engine_out != None:
out = self.engine_out.encrypt(packet)
else:
out = packet
# + mac
try:
self.write_lock.acquire()
if self.engine_out != None:
payload = struct.pack('>I', self.sequence_number_out) + packet
out += HMAC.HMAC(self.mac_key_out, payload, self.local_mac_engine).digest()[:self.local_mac_len]
self.sequence_number_out += 1L
self.sequence_number_out %= 0x100000000L
self.write_all(out)
finally:
self.write_lock.release()
def read_message(self):
"only one thread will ever be in this function"
header = self.read_all(self.block_size_in)
if self.engine_in != None:
header = self.engine_in.decrypt(header)
if self.ultra_debug:
self.log(DEBUG, format_binary(header, 'IN: '));
packet_size = struct.unpack('>I', header[:4])[0]
# leftover contains decrypted bytes from the first block (after the length field)
leftover = header[4:]
if (packet_size - len(leftover)) % self.block_size_in != 0:
raise SSHException('Invalid packet blocking')
buffer = self.read_all(packet_size + self.remote_mac_len - len(leftover))
packet = buffer[:packet_size - len(leftover)]
post_packet = buffer[packet_size - len(leftover):]
if self.engine_in != None:
packet = self.engine_in.decrypt(packet)
if self.ultra_debug:
self.log(DEBUG, format_binary(packet, 'IN: '));
packet = leftover + packet
if self.remote_mac_len > 0:
mac = post_packet[:self.remote_mac_len]
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]
if my_mac != mac:
raise SSHException('Mismatched MAC')
padding = ord(packet[0])
payload = packet[1:packet_size - padding + 1]
randpool.add_event(packet[packet_size - padding + 1])
#self.log(DEBUG, 'Got payload (%d bytes, %d padding)' % (packet_size, padding))
msg = Message(payload[1:])
msg.seqno = self.sequence_number_in
self.sequence_number_in = (self.sequence_number_in + 1) & 0xffffffffL
# check for rekey
self.received_bytes += packet_size + self.remote_mac_len + 4
self.received_packets += 1
if (self.received_packets >= self.REKEY_PACKETS) or (self.received_bytes >= self.REKEY_BYTES):
# only ask once for rekeying
if self.local_kex_init is None:
self.log(DEBUG, 'Rekeying (hit %d packets, %d bytes)' % (self.received_packets,
self.received_bytes))
self.received_packets_overflow = 0
self.send_kex_init()
else:
# we've asked to rekey already -- give them 20 packets to
# comply, then just drop the connection
self.received_packets_overflow += 1
if self.received_packets_overflow >= 20:
raise SSHException('Remote transport is ignoring rekey requests')
return ord(payload[0]), msg
def set_K_H(self, k, h):
"used by a kex object to set the K (root key) and H (exchange hash)"
self.K = k
self.H = h
if self.session_id == None:
self.session_id = h
def verify_key(self, host_key, sig):
if self.host_key_type == 'ssh-rsa':
key = RSAKey(Message(host_key))
elif self.host_key_type == 'ssh-dss':
key = DSSKey(Message(host_key))
else:
key = None
if (key == None) or not key.valid:
raise SSHException('Unknown host key type')
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)
self.host_key = host_key
def compute_key(self, id, nbytes):
"id is 'A' - 'F' for the various keys used by ssh"
m = Message()
m.add_mpint(self.K)
m.add_bytes(self.H)
m.add_byte(id)
m.add_bytes(self.session_id)
out = sofar = SHA.new(str(m)).digest()
while len(out) < nbytes:
m = Message()
m.add_mpint(self.K)
m.add_bytes(self.H)
m.add_bytes(sofar)
hash = SHA.new(str(m)).digest()
out += hash
sofar += hash
return out[:nbytes]
def get_cipher(self, name, key, iv):
if not self.cipher_info.has_key(name):
raise SSHException('Unknown client cipher ' + name)
return self.cipher_info[name]['class'].new(key, self.cipher_info[name]['mode'], iv)
def run(self):
self.active = 1
try:
# SSH-1.99-OpenSSH_2.9p2
self.write_all(self.local_version + '\r\n')
self.check_banner()
self.send_kex_init()
self.expected_packet = MSG_KEXINIT
while self.active:
ptype, m = self.read_message()
if ptype == MSG_IGNORE:
continue
elif ptype == MSG_DISCONNECT:
self.parse_disconnect(m)
self.active = 0
break
elif ptype == MSG_DEBUG:
self.parse_debug(m)
continue
if self.expected_packet != 0:
if ptype != self.expected_packet:
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)
continue
if self.handler_table.has_key(ptype):
self.handler_table[ptype](self, m)
elif self.channel_handler_table.has_key(ptype):
chanid = m.get_int()
if self.channels.has_key(chanid):
self.channel_handler_table[ptype](self.channels[chanid], m)
else:
self.log(WARNING, 'Oops, unhandled type %d' % ptype)
msg = Message()
msg.add_byte(chr(MSG_UNIMPLEMENTED))
msg.add_int(m.seqno)
self.send_message(msg)
except SSHException, e:
self.log(DEBUG, 'Exception: ' + str(e))
except EOFError, e:
self.log(DEBUG, 'EOF')
except Exception, e:
self.log(DEBUG, 'Unknown exception: ' + str(e))
if self.active:
self.active = 0
if self.completion_event != None:
self.completion_event.set()
if self.auth_event != None:
self.auth_event.set()
for e in self.channel_events.values():
e.set()
self.sock.close()
### protocol stages
def renegotiate_keys(self):
self.completion_event = threading.Event()
self.send_kex_init()
while 1:
self.completion_event.wait(0.1);
if not self.active:
return 0
if self.completion_event.isSet():
break
return 1
def negotiate_keys(self, m):
# throws SSHException on anything unusual
if self.local_kex_init == None:
# remote side wants to renegotiate
self.send_kex_init()
self.parse_kex_init(m)
self.kex_engine.start_kex()
def check_banner(self):
# this is slow, but we only have to do it once
for i in range(5):
buffer = ''
while not '\n' in buffer:
buffer += self.read_all(1)
buffer = buffer[:-1]
if (len(buffer) > 0) and (buffer[-1] == '\r'):
buffer = buffer[:-1]
if buffer[:4] == 'SSH-':
break
self.log(DEBUG, 'Banner: ' + buffer)
if buffer[:4] != 'SSH-':
raise SSHException('Indecipherable protocol version "' + buffer + '"')
# save this server version string for later
self.remote_version = buffer
# pull off any attached comment
comment = ''
i = string.find(buffer, ' ')
if i >= 0:
comment = buffer[i+1:]
buffer = buffer[:i]
# parse out version string and make sure it matches
_unused, version, client = string.split(buffer, '-')
if version != '1.99' and version != '2.0':
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):
# send a really wimpy kex-init packet that says we're a bare-bones ssh client
m = Message()
m.add_byte(chr(MSG_KEXINIT))
m.add_bytes(randpool.get_bytes(16))
m.add(','.join(self.preferred_kex))
m.add(','.join(self.preferred_keys))
m.add(','.join(self.preferred_ciphers))
m.add(','.join(self.preferred_ciphers))
m.add(','.join(self.preferred_macs))
m.add(','.join(self.preferred_macs))
m.add('none')
m.add('none')
m.add('')
m.add('')
m.add_boolean(0)
m.add_int(0)
# save a copy for later (needed to compute a hash)
self.local_kex_init = str(m)
self.send_message(m)
def parse_kex_init(self, m):
# reset counters of when to re-key, since we are now re-keying
self.received_bytes = 0
self.received_packets = 0
self.received_packets_overflow = 0
cookie = m.get_bytes(16)
kex_algo_list = m.get_list()
server_key_algo_list = m.get_list()
client_encrypt_algo_list = m.get_list()
server_encrypt_algo_list = m.get_list()
client_mac_algo_list = m.get_list()
server_mac_algo_list = m.get_list()
client_compress_algo_list = m.get_list()
server_compress_algo_list = m.get_list()
client_lang_list = m.get_list()
server_lang_list = m.get_list()
kex_follows = m.get_boolean()
unused = m.get_int()
# no compression support (yet?)
if (not('none' in client_compress_algo_list) or
not('none' in server_compress_algo_list)):
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.
if self.server_mode:
agreed_kex = filter(self.preferred_kex.__contains__, kex_algo_list)
else:
agreed_kex = filter(kex_algo_list.__contains__, self.preferred_kex)
if len(agreed_kex) == 0:
raise SSHException('Incompatible ssh peer (no acceptable kex algorithm)')
self.kex_engine = self.kex_info[agreed_kex[0]](self)
if self.server_mode:
agreed_keys = filter(self.preferred_keys.__contains__, server_key_algo_list)
else:
agreed_keys = filter(server_key_algo_list.__contains__, self.preferred_keys)
if len(agreed_keys) == 0:
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 SSHException('Incompatible ssh peer (can\'t match requested host key type)')
if self.server_mode:
agreed_local_ciphers = filter(self.preferred_ciphers.__contains__,
server_encrypt_algo_list)
agreed_remote_ciphers = filter(self.preferred_ciphers.__contains__,
client_encrypt_algo_list)
else:
agreed_local_ciphers = filter(client_encrypt_algo_list.__contains__,
self.preferred_ciphers)
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 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))
if self.server_mode:
agreed_remote_macs = filter(self.preferred_macs.__contains__, client_mac_algo_list)
agreed_local_macs = filter(self.preferred_macs.__contains__, server_mac_algo_list)
else:
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 SSHException('Incompatible ssh server (no acceptable macs)')
self.local_mac = agreed_local_macs[0]
self.remote_mac = agreed_remote_macs[0]
self.log(DEBUG, 'kex algos:' + str(kex_algo_list) + ' server key:' + str(server_key_algo_list) + \
' client encrypt:' + str(client_encrypt_algo_list) + \
' server encrypt:' + str(server_encrypt_algo_list) + \
' client mac:' + str(client_mac_algo_list) + \
' server mac:' + str(server_mac_algo_list) + \
' client compress:' + str(client_compress_algo_list) + \
' server compress:' + str(server_compress_algo_list) + \
' client lang:' + str(client_lang_list) + \
' server lang:' + str(server_lang_list) + \
' kex follows?' + str(kex_follows))
self.log(DEBUG, 'using kex %s; server key type %s; cipher: local %s, remote %s; mac: local %s, remote %s' %
(agreed_kex[0], self.host_key_type, self.local_cipher, self.remote_cipher, self.local_mac,
self.remote_mac))
# save for computing hash later...
# now wait! openssh has a bug (and others might too) where there are
# actually some extra bytes (one NUL byte in openssh's case) added to
# the end of the packet but not parsed. turns out we need to throw
# away those bytes because they aren't part of the hash.
self.remote_kex_init = chr(MSG_KEXINIT) + m.get_so_far()
def activate_inbound(self):
"switch on newly negotiated encryption parameters for inbound traffic"
self.block_size_in = self.cipher_info[self.remote_cipher]['block-size']
if self.server_mode:
IV_in = self.compute_key('A', self.block_size_in)
key_in = self.compute_key('C', self.cipher_info[self.remote_cipher]['key-size'])
else:
IV_in = self.compute_key('B', self.block_size_in)
key_in = self.compute_key('D', self.cipher_info[self.remote_cipher]['key-size'])
self.engine_in = self.get_cipher(self.remote_cipher, key_in, IV_in)
self.remote_mac_len = self.mac_info[self.remote_mac]['size']
self.remote_mac_engine = self.mac_info[self.remote_mac]['class']
# initial mac keys are done in the hash's natural size (not the potentially truncated
# transmission size)
if self.server_mode:
self.mac_key_in = self.compute_key('E', self.remote_mac_engine.digest_size)
else:
self.mac_key_in = self.compute_key('F', self.remote_mac_engine.digest_size)
def activate_outbound(self):
"switch on newly negotiated encryption parameters for outbound traffic"
m = Message()
m.add_byte(chr(MSG_NEWKEYS))
self.send_message(m)
self.block_size_out = self.cipher_info[self.local_cipher]['block-size']
if self.server_mode:
IV_out = self.compute_key('B', self.block_size_out)
key_out = self.compute_key('D', self.cipher_info[self.local_cipher]['key-size'])
else:
IV_out = self.compute_key('A', self.block_size_out)
key_out = self.compute_key('C', self.cipher_info[self.local_cipher]['key-size'])
self.engine_out = self.get_cipher(self.local_cipher, key_out, IV_out)
self.local_mac_len = self.mac_info[self.local_mac]['size']
self.local_mac_engine = self.mac_info[self.local_mac]['class']
# initial mac keys are done in the hash's natural size (not the potentially truncated
# transmission size)
if self.server_mode:
self.mac_key_out = self.compute_key('F', self.local_mac_engine.digest_size)
else:
self.mac_key_out = self.compute_key('E', self.local_mac_engine.digest_size)
def parse_newkeys(self, m):
self.log(DEBUG, 'Switch to new keys ...')
self.activate_inbound()
# can also free a bunch of stuff here
self.local_kex_init = self.remote_kex_init = None
self.e = self.f = self.K = self.x = None
if not self.initial_kex_done:
# this was the first key exchange
self.initial_kex_done = 1
# send an event?
if self.completion_event != None:
self.completion_event.set()
return
def parse_disconnect(self, m):
code = m.get_int()
desc = m.get_string()
self.log(INFO, 'Disconnect (code %d): %s' % (code, desc))
def parse_channel_open_success(self, m):
chanid = m.get_int()
server_chanid = m.get_int()
server_window_size = m.get_int()
server_max_packet_size = m.get_int()
if not self.channels.has_key(chanid):
self.log(WARNING, 'Success for unrequested channel! [??]')
return
try:
self.lock.acquire()
chan = self.channels[chanid]
chan.set_server_channel(server_chanid, server_window_size, server_max_packet_size)
self.log(INFO, 'Secsh channel %d opened.' % chanid)
if self.channel_events.has_key(chanid):
self.channel_events[chanid].set()
del self.channel_events[chanid]
finally:
self.lock.release()
return
def parse_channel_open_failure(self, m):
chanid = m.get_int()
reason = m.get_int()
reason_str = m.get_string()
lang = m.get_string()
if CONNECTION_FAILED_CODE.has_key(reason):
reason_text = CONNECTION_FAILED_CODE[reason]
else:
reason_text = '(unknown code)'
self.log(INFO, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text))
try:
self.lock.aquire()
if self.channels.has_key(chanid):
del self.channels[chanid]
if self.channel_events.has_key(chanid):
self.channel_events[chanid].set()
del self.channel_events[chanid]
finally:
self.lock_release()
return
def parse_channel_open(self, m):
kind = m.get_string()
self.log(DEBUG, 'Rejecting "%s" channel request from server.' % kind)
chanid = m.get_int()
msg = Message()
msg.add_byte(chr(MSG_CHANNEL_OPEN_FAILURE))
msg.add_int(chanid)
msg.add_int(1)
msg.add_string('Client connections are not allowed.')
msg.add_string('en')
self.send_message(msg)
def parse_debug(self, m):
always_display = m.get_boolean()
msg = m.get_string()
lang = m.get_string()
self.log(DEBUG, 'Debug msg: ' + safe_string(msg))
handler_table = {
MSG_NEWKEYS: parse_newkeys,
MSG_CHANNEL_OPEN_SUCCESS: parse_channel_open_success,
MSG_CHANNEL_OPEN_FAILURE: parse_channel_open_failure,
MSG_CHANNEL_OPEN: parse_channel_open,
MSG_KEXINIT: negotiate_keys,
}
channel_handler_table = {
MSG_CHANNEL_SUCCESS: Channel.request_success,
MSG_CHANNEL_FAILURE: Channel.request_failed,
MSG_CHANNEL_DATA: Channel.feed,
MSG_CHANNEL_WINDOW_ADJUST: Channel.window_adjust,
MSG_CHANNEL_REQUEST: Channel.handle_request,
MSG_CHANNEL_EOF: Channel.handle_eof,
MSG_CHANNEL_CLOSE: Channel.handle_close,
}

89
util.py Normal file
View File

@ -0,0 +1,89 @@
#!/usr/bin/python
import struct
def inflate_long(s, always_positive=0):
"turns a normalized byte string into a long-int (adapted from Crypto.Util.number)"
out = 0L
negative = 0
if not always_positive and (len(s) > 0) and (ord(s[0]) >= 0x80):
negative = 1
if len(s) % 4:
filler = '\x00'
if negative:
filler = '\xff'
s = filler * (4 - len(s) % 4) + s
for i in range(0, len(s), 4):
out = (out << 32) + struct.unpack('>I', s[i:i+4])[0]
if negative:
out -= (1L << (8 * len(s)))
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) and (n != -1):
s = struct.pack('>I', n & 0xffffffffL) + s
n = n >> 32
# strip off leading zeros, FFs
for i in enumerate(s):
if (n == 0) and (i[1] != '\000'):
break
if (n == -1) and (i[1] != '\xff'):
break
else:
# degenerate case, n was either 0 or -1
i = (0,)
if n == 0:
s = '\000'
else:
s = '\xff'
s = s[i[0]:]
if add_sign_padding:
if (n == 0) and (ord(s[0]) >= 0x80):
s = '\x00' + s
if (n == -1) and (ord(s[0]) < 0x80):
s = '\xff' + s
return s
def format_binary_weird(data):
out = ''
for i in enumerate(data):
out += '%02X' % ord(i[1])
if i[0] % 2:
out += ' '
if i[0] % 16 == 15:
out += '\n'
return out
def format_binary(data, prefix=''):
x = 0
out = []
while len(data) > x + 16:
out.append(format_binary_line(data[x:x+16]))
x += 16
if x < len(data):
out.append(format_binary_line(data[x:]))
return [prefix + x for x in out]
def format_binary_line(data):
left = ' '.join(['%02X' % ord(c) for c in data])
right = ''.join([('.%c..' % c)[(ord(c)+61)//94] for c in data])
return '%-50s %s' % (left, right)
def hexify(s):
"turn a string into a hex sequence"
return ''.join(['%02X' % ord(c) for c in s])
def safe_string(s):
out = ''
for c in s:
if (ord(c) >= 32) and (ord(c) <= 127):
out += c
else:
out += '%%%02X' % ord(c)
return out
# ''.join([['%%%02X' % ord(c), c][(ord(c) >= 32) and (ord(c) <= 127)] for c in s])