Wednesday, March 23, 2011

Twisted Conch in 60 Seconds: Introduction to an SSH server

Greetings once more, readers.  A year ago I introduced you to the simplicity of web programming with Twisted.  Since then, I've been eager to try to duplicate the success of the series for another topic.  Welcome to Twisted Conch in 60 Seconds.

Twisted Conch implements the SSH protocol for both clients and servers.  It also implements client and server applications.  It is also a library for writing custom SSH client and server software.  This series will focus on using Twisted Conch as an SSH library, and the first articles will cover writing custom SSH servers, with clients covered later.

In this first installment, the goal is to set up an SSH server which will accept SSH client connections but
which cannot actually authenticate any users (so no one will be able to log in).

Before getting into any code, the first thing we need for an SSH server is a server key.  This can be generated using ssh-keygen from OpenSSH or with Conch's own key generation tool, ckeygen.  Since ample examples of ssh-keygen can be found elsewhere, here's an example of generating a key with ckeygen:

exarkun@boson:/tmp$ ckeygen -t rsa -f id_rsa
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in id_rsa
Your public key has been saved in id_rsa.pub
The key fingerprint is:
d5:1d:ba:47:a4:75:1a:fe:d4:ba:46:2e:44:67:0d:8b
exarkun@boson:/tmp$ ls -l id_rsa id_rsa.pub
-rw------- 1 1000 1000 886 2011-03-23 22:59 id_rsa
-rwxr-xr-x 1 1000 1000 226 2011-03-23 22:59 id_rsa.pub
exarkun@boson:/tmp$ 

Here's where the actual Conch code begins.  First, some imports:

from twisted.conch.ssh.factory import SSHFactory


SSHFactory is an IProtocolFactory which is used to create protocol instances to handle new connections that arrive at a listening port.  SSHFactory in particular creates protocol instances that can carry on the server side of an SSH connection.  We need an instance to hook up to a listening port:

factory = SSHFactory()


Nothing very exciting going on there.  SSHFactory doesn't take any initializer arguments.  It does have some useful attributes you can set, though.  Two important attributes are publicKeys and privateKeys.  These define what key the server uses to identify itself to clients.  A server can have multiple keys, so these attributes are bound to dictionaries.  In this example we'll just give the server one key though.  For this, we need to have a couple Key objects:

from twisted.conch.ssh.keys import Key

with open('id_rsa') as privateBlobFile:
privateBlob = privateBlobFile.read()
privateKey = Key.fromString(data=privateBlob)

with open('id_rsa.pub') as publicBlobFile:
publicBlob = publicBlobFile.read()
publicKey = Key.fromString(data=publicBlob)

The Key.fromString method can parse several formats, including the standard format keys are kept in by OpenSSH (which is also the format ckeygen emits).
Now we can set up the factory to know about these keys:

factory.privateKeys = {'ssh-rsa': privateKey}
factory.publicKeys = {'ssh-rsa': publicKey}

Clients connecting to this server will get to see this public key and then use it to challenge the server to prove it also has the private key by signing some random data correctly.
One more attribute needs to be set on the factory.  privateKeys and publicKeys let clients identify the server.  Now the server needs some way to identify clients.  This is done using a Portal from twisted.cred.  However, I said this server wouldn't be able to authenticate users, so this example will only set a placeholder value here which will fail all authentication attempts:

from twisted.cred.portal import Portal
factory.portal = Portal(None)

Later we'll revisit this attribute so that users can actually log in to the server.

The example is almost done now.  The only thing left to do is listen on a port with this factory and run the reactor.  For this we'll need to import the reactor.  We have several options for how to listen on a port, but for this example I'll use the classic reactor.listenTCP:

from twisted.internet import reactor
reactor.listenTCP(2022, factory)


This listens on TCP port 2022.  Whenever a connection arrives, factory will be used to create a new protocol instance to handle it.  This factory is going to create SSHServerTransport instances, but don't worry about that for now.

Since the reactor is the mainloop that drives any Twisted-based application, nothing actually happens until we run it:

reactor.run()


With that, you now have an SSH server.  It's functional enough to prove its identity to clients that connect to it and even accept authentication attempts from them, but it will always reject their attempts.  So it's not the most useful SSH server (but add a little logging and maybe you have a simple SSH honey pot!), but if you tune in next time, I'll demonstrate how it can be extended to support some more useful authentication options.  Here's a full code listing for this example:

from twisted.cred.portal import Portal
from twisted.conch.ssh.factory import SSHFactory
from twisted.internet import reactor
from twisted.conch.ssh.keys import Key

with open('id_rsa') as privateBlobFile:
privateBlob = privateBlobFile.read()
privateKey = Key.fromString(data=privateBlob)

with open('id_rsa.pub') as publicBlobFile:
publicBlob = publicBlobFile.read()
publicKey = Key.fromString(data=publicBlob)

factory = SSHFactory()
factory.privateKeys = {'ssh-rsa': privateKey}
factory.publicKeys = {'ssh-rsa': publicKey}
factory.portal = Portal(None)

reactor.listenTCP(2022, factory)
reactor.run()

No comments:

Post a Comment