Thursday, March 24, 2011

Twisted Conch in 60 Seconds: Password Authentication

Welcome back to Twisted Conch in 60 Seconds, a series about writing custom SSH software with Twisted.  Last time, I showed you how to set up a basic SSH server which could accept connections and then reject all authentication attempts.  This time we'll extend that server to support password authentication.
Let's consider the factory created in the first example:

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

The object in this snippet responsible for authenticating the client is Portal(None). However, this portal is missing two things it really needs in order to be useful. First, it's missing a username/password checker. Twisted includes one that's easy to use and reads credentials out of a file, so we'll start with that one:

from twisted.cred.checkers import FilePasswordDB

factory.portal.registerChecker(FilePasswordDB("ssh-passwords"))

FilePasswordDB will now try to read information from ssh-passwords in order to authenticate SSH client connection attempts. The file should be populated with lines like this:

alice:goodpassword
bob:badpassword
jpcalderone:supersecretpassword

The second thing the Portal needs is a Realm. After the FilePasswordDB says a user supplied the correct username/password combination, Conch needs an object that will represent the user who just authenticated. This object will be used to determine what actions the SSH user is allowed to take, and what consequences they will have. A realm only needs to implement one method:

from twisted.conch.avatar import ConchUser

def nothing():
    pass

class SimpleRealm(object):
    def requestAvatar(self, avatarId, mind, *interfaces):
        return IConchUser, ConchUser(), nothing

After authentication succeeds, requestAvatar is called. The avatarId parameter tells the realm the name of the user who just authenticated successfully. The mind isn't used by Conch. The interfaces indicate what kind of user is being requested; in this case, it will include twisted.conch.interfaces.IConchUser (and we just assume that it does for now). The method must return a three-tuple. The first element is the kind of user the realm decided to give back (this must be one of the requested interfaces - again, we're assuming IConchUser for now). This just lets the calling code know what kind of user it ended up with. The second element is the user object itself. Conch conveniently provides us with a basic user class that implements almost no behavior, so it's suitable to be used directly in this simple example. The final element of the tuple is a logout callable. This will be invoked for us when the user logs out. This example has no custom logout logic, so we return a no-op function.
Portal construction will now look like this:

factory.portal = Portal(SimpleRealm())

These are all the pieces necessary to do username/password authentication of SSH users. Here's the full code listing for this version:

from twisted.cred.portal import Portal
from twisted.cred.checkers import FilePasswordDB
from twisted.internet import reactor
from twisted.conch.ssh.factory import SSHFactory
from twisted.conch.ssh.keys import Key
from twisted.conch.interfaces import IConchUser
from twisted.conch.avatar import ConchUser

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)

def nothing():
    pass

class SimpleRealm(object):
    def requestAvatar(self, avatarId, mind, *interfaces):
        return IConchUser, ConchUser(), nothing

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

factory.portal.registerChecker(FilePasswordDB("ssh-passwords"))

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

If you run this server and connect to it with one of the credentials in the password file you provide, then you should receive an extremely gratifying result along the lines of:

channel 0: open failed: unknown channel type: unknown channel

Sorry! Tune in next time to learn what a channel is and how to define one.

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()