watch this The wheels are turning, slowly turning. home
Twisted Conch in 60 Seconds: Password Authentication 2011-03-24

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.