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.