07 May, 2011

PAM python module for out-of-band one-time tokens

I previous wrote about integrating openid with unix shell logins for my public shell server barwen.ch. This post talks about the out-of-band PAM token module that I wrote as part of that - where I'm generating the tokens when the user is authenticated by OpenID. But I guess it could also have use when there's some other out of band mechanism (such as sending something by SMS?)

Subject to some other non-PAM web-based authentication (openid in this case, but it could be anything really), I want to issue a token value to the user in-band with respect to that other authentication (i.e. in a web page shown to the user), which is out-of-band with respect to PAM. That token should then be usable for a short period of time to make a single PAM authorisation to sshd.

That is, if you can log into the web-based authentication, you should then be able to log into the ssh system.

So on one side I need a PAM module (which I will write in Python) to check the tokens, and on the other side I need something (a command line tool) to issue the tokens. To complete the loop, I need some database (the filesystem) to store the tokens on the server side.

and secondly the PAM module, which is based on the PAM module in my previous post:

import os
import syslog
import pickle
import time
def pam_sm_authenticate(pamh, flags, argv):
syslog.syslog("start benc")
pamh.authtok
if pamh.authtok == None:
syslog.syslog("got no password in authtok - trying through conversation")
passmsg = pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, "Monkeyballs?")
rsp = pamh.conversation(passmsg)
syslog.syslog("response is "+rsp.resp)
pamh.authtok = rsp.resp
# so we should at this point have the password either through the
# prompt or from previous module
syslog.syslog("got password: "+pamh.authtok)
# now look for token
# SECURITY BUG TODO: we're using this to make a path so we need to make sure
# we're not being directed out into some other directory. We know the
# range of characters that can be used in a token so we can reject if
# anything other than those exists.
# Especially as we delete the token file on use - otherwise could delete
# arbitrary files on the system...
tfn = "/root/" + pamh.authtok + ".token"
if os.path.exists(tfn):
fh = open(tfn)
tokendata = pickle.load(fh)
tokenuser = tokendata[0]
tokentime = tokendata[1]
fh.close()
os.remove(tfn);
# will remove the token even if it was for the wrong user
# not sure if there's any security different wrt leaving it there if
# its the wrong user?
if tokentime < time.time():
syslog.syslog("token time expired")
return pamh.PAM_AUTH_ERR
if tokenuser != pamh.user:
syslog.syslog("token user "+tokenuser+" does not match requested user "+pamh.user)
return pamh.PAM_AUTH_ERR
return pamh.PAM_SUCCESS
return pamh.PAM_AUTH_ERR
def pam_sm_setcred(pamh, flags, argv):
return pamh.PAM_SUCCESS