I got motivated by my friend Bob Fleck to do passwords right in Mailman. Bob wrote an implementation of PBKDF2 from RFC 2898 in Python. PBKDF2 is a scheme that uses a random salt and an iteration count to hash a password in a fairly secure manner. It also uses a pseudorandom function which we basically hardcode to SHA1, though we could support other PRFs in the future if we wanted.

While this was pretty cool, it really motivated me to do something I've wanted to do since about day one, and that is, to remove clear text user passwords from Mailman. One of the longest and most valid complaints from security-conscious users has been the monthly password reminders that Mailman sends out. This is only possible because Mailman stores users passwords in the clear. This is insane, but it's been like this forever. I've already ripped out the password reminder scripts in the Mailman 2.2 svn, but I hadn't yet gotten rid of cleartext user passwords. Now I have.

There is another RFC that I've looked at; RFC 2307 describes a scheme for specifying password hashes, which I believe originated with LDAP. Essentially, the scheme is prepended to the hash, delimited by curly braces. For example:

{SSHA}jUHT9S1YvZGygZCe3RhbMysOkD95ohVdEhSMG0S0Us6xbZJE2pt3fQ==

This says that what follows is the Base64 encoded salted SHA1 hashed password. To confirm a user's password, we simply perform these on their response:

  • Base64 decode the user's hashed password
  • Chop off everything after the 20th byte as the salt
  • Hash the response and the salt and check it against the hashed user password minus the salt

The other important thing is that because we know the scheme used to hash the user's password, we can easily upgrade this to other hash schemes when the ones we've chosen get cracked. IOW, say we found out that salted-SHA1 hashes are easily cracked. Well, we switch to PBKDF2 or some other scheme and then whenever a user successfully logs in with their old password, we simply rehash it with the new scheme and store that in the database.

I've now committed all the code to support this, and also to use these hashed passwords for user passwords, list owner and moderator passwords, and site and list creator passwords. It really touched a relative small amount of code for a big improvement in security.

I now need to implement password resets since of course, user passwords cannot be recovered anymore. A user (or for that matter, list owner) should be able to request a password reset in a secure manner. Since I have a feeling most of that will be u/i work, and because we will eventually rewrite the current web interface, I'm deferring that for now. The other thing that I think will have an impact here will be a switch to role-based authorization, which is the next thing I think I'm going to work on.

Oh yeah, one other thing. Let's say the scheme were something like:

{URL}https://auth.example.com/$user/$challenge

Now you have the ability to easily authenticate the user against an external web service.


Comments

Tokio Kikuchi

Hi Barry,

I'm testing with fresh installation of 2.2 and found a password/cookie

  • problem.  I can start HTTPRunner, create a new list using a site password created by bin/mmsitepass, and visit the admin page.  But, if I stop the qrunner and start again, I can't login again at the admin page.  I get this error if I visit the admlogin page.

Traceback (most recent call last):

  File "/usr/local/mailman/Mailman/Cgi/wsgi_app.py", line 130, in mailman_app
    sys.modules[modname].main()

  File "/usr/local/mailman/Mailman/Cgi/admin.py", line 79, in main
    cgidata.getvalue('adminpw', '')):

  File "/usr/local/mailman/Mailman/SecurityManager.py", line 183, in WebAuthenticate
    ac = self.Authenticate(authcontexts, response, user)

  File "/usr/local/mailman/Mailman/SecurityManager.py", line 150, in Authenticate
    if passwords.check_response(secret, response):

  File "/usr/local/mailman/Mailman/passwords.py", line 182, in check_response
    return scheme_class.check_response(rest, response, *scheme_parts[1:])

  File "/usr/local/mailman/Mailman/passwords.py", line 100, in check_response
    challenge_bytes = decode(challenge)

  File "/usr/lib/python2.4/base64.py", line 112, in urlsafe_b64decode
    return b64decode(s, '-_')

  File "/usr/lib/python2.4/base64.py", line 71, in b64decode
    s = _translate(s, {altchars[0]: '+', altchars[1]: '/'})

  File "/usr/lib/python2.4/base64.py", line 36, in _translate
    return s.translate(''.join(translation))

TypeError: character mapping must return integer, None or unicode

whta's wrong?

Tokio Kikuchi

Ah, looks like this was the problem:

--- passwords.py        (revision 8159)
+++ passwords.py        (working copy)
@@ -97,7 +97,7 @@
     @staticmethod
     def check_response(challenge, response):
         # Get the salt from the challenge
-        challenge_bytes = decode(challenge)
+        challenge_bytes = decode(str(challenge))
         digest = challenge_bytes[:20]
         salt = challenge_bytes[20:]
         h = sha.new(response) 

Unicode or string ?  

MailmanWiki: DEV/Passwords done right (last edited 2007-01-13 22:10:52 by barry)