import base64 as BN
import hashlib as H
import hmac as HM
+import itertools as I
import os as OS
import cgi as CGI
###
### Once we've satisfied ourselves of the user's credentials, we issue a
### short-lived session token, stored in a cookie namde `chpwd-token'. This
-### token has the form `DATE.NONCE.TAG.USER': here, DATE is the POSIX time of
-### issue, as a decimal number; NONCE is a randomly chosen string, encoded in
-### base64, USER is the user's login name, and TAG is a cryptographic MAC tag
-### on the string `DATE.NONCE.USER'. (The USER name is on the end so that it
-### can contain `.' characters without introducing parsing difficulties.)
+### token has the form `DATE.TAG.USER': here, DATE is the POSIX time of
+### issue, as a decimal number; USER is the user's login name; and TAG is a
+### cryptographic MAC tag on the string `chpwd-token.DATE.USER'. (The USER
+### name is on the end so that it can contain `.' characters without
+### introducing parsing difficulties.)
###
### Secrets for these MAC tags are stored in the database: secrets expire
### after 30 minutes (invalidating all tokens issued with them); we only
### to forge, and the cookie will be supplied automatically by the user
### agent. Showing the user some information we were quite happy to release
### anyway isn't an interesting attack, but we must certainly require
-### something stronger for state-change requests. Here, we also check that a
-### special request parameter `%nonce' matches the token's NONCE field: forms
-### setting up a `POST' action must include an appropriate hidden input
-### element.
+### something stronger for state-change requests. Here, we also check a
+### special request parameter `%nonce': forms setting up a `POST' action must
+### include an appropriate hidden input element. The `%nonce' parameter has
+### the form `LEFT.RIGHT', where LEFT and RIGHT are two base-64 strings such
+### that their XOR is the (deterministic) MAC tag on `chpwd-nonce.DATE.USER'.
+### (The LEFT string is chosen at random, and the RIGHT string is set to the
+### appropriate TAG XOR LEFT.)
###
### Messing about with cookies is a bit annoying, but it's hard to come up
### with alternatives. I'm trying to keep the URLs fairly pretty, and anyway
"""Return the octet string S, in a vaguely pretty form."""
return BN.b64encode(s, '+$').rstrip('=')
-def auth_tag(sec, stamp, nonce, user):
- """Compute a tag using secret SEC on `STAMP.NONCE.USER'."""
+def unhack_octets(s):
+ """Reverse the operation done by `hack_octets'."""
+ pad = (len(s) + 3)&3 - len(s)
+ return BN.b64decode(s + '='*pad, '+$')
+
+def auth_tag(sec, stamp, user):
+ """Compute a tag using secret SEC on `STAMP.USER'."""
hmac = HM.HMAC(sec, digestmod = CFG.AUTHHASH)
- hmac.update('%d.%s.%s' % (stamp, nonce, user))
+ hmac.update('chpwd-token.%d.%s' % (stamp, user))
return hack_octets(hmac.digest())
+def csrf_tag(sec, stamp, user):
+ """Compute a tag using secret SEC on `STAMP.USER'."""
+ hmac = HM.HMAC(sec, digestmod = CFG.AUTHHASH)
+ hmac.update('chpwd-nonce.%d.%s' % (stamp, user))
+ return hmac.digest()
+
+def xor_strings(x, y):
+ """Return the bitwise XOR of two octet strings."""
+ return ''.join(chr(ord(xc) ^ ord(yc)) for xc, yc in I.izip(x, y))
+
+def mint_csrf_nonce(sec, ntag):
+ left = OS.urandom(len(ntag))
+ right = xor_strings(left, ntag)
+ return '%s.%s' % (hack_octets(left), hack_octets(right))
+
def mint_token(user):
"""Make and return a fresh token for USER."""
sec = freshsecret()
- nonce = hack_octets(OS.urandom(16))
- tag = auth_tag(sec, U.NOW, nonce, user)
- return '%d.%s.%s.%s' % (U.NOW, nonce, tag, user)
+ tag = auth_tag(sec, U.NOW, user)
+ return '%d.%s.%s' % (U.NOW, tag, user)
## Long messages for reasons why one might have been redirected back to the
## login page.
Check that the TOKEN is valid, comparing it against the NONCE if this is
not `None'.
- If the token is OK, then return the correct user name, and set `NONCE' set
- to the appropriate portion of the token. Otherwise raise an
- `AuthenticationFailed' exception with an appropriate `why'.
+ If the token is OK, then return the correct user name, and set `NONCE' to a
+ new nonce for the next request. Otherwise raise an `AuthenticationFailed'
+ exception with an appropriate `why'.
"""
global NONCE
## Parse the token.
bits = token.split('.', 3)
- if len(bits) != 4: raise AuthenticationFailed, 'BADTOKEN'
- stamp, NONCE, tag, user = bits
-
- ## Check that the nonce matches, if one was supplied.
- if nonce is not None and nonce != NONCE:
- raise AuthenticationFailed, 'BADNONCE'
+ if len(bits) != 3: raise AuthenticationFailed, 'BADTOKEN'
+ stamp, tag, user = bits
## Check the stamp, and find the right secret.
if not stamp.isdigit(): raise AuthenticationFailed, 'BADTIME'
if sec is None: raise AuthenticationFailed, 'EXPIRED'
## Check the tag.
- t = auth_tag(sec, when, NONCE, user)
+ t = auth_tag(sec, when, user)
if t != tag: raise AuthenticationFailed, 'BADTAG'
+ ## Determine the correct CSRF tag.
+ ntag = csrf_tag(sec, when, user)
+
+ ## Check that the nonce matches, if one was supplied.
+ if nonce is not None:
+ bits = nonce.split('.', 2)
+ if len(bits) != 2: raise AuthenticationFailed, 'BADNONCE'
+ try: left, right = map(unhack_octets, bits)
+ except TypeError: raise AuthenticationFailed, 'BADNONCE'
+ if len(left) != len(right) or len(left) != len(ntag):
+ raise AuthenticationFailed, 'BADNONCE'
+ gtag = xor_strings(left, right)
+ if gtag != ntag: raise AuthenticationFailed, 'BADNONCE'
+
+ ## Make a new nonce string for use in forms.
+ NONCE = mint_csrf_nonce(sec, ntag)
+
## Make sure the user still exists.
try: acct = S.SERVICES['master'].find(user)
except S.UnknownUser: raise AuthenticationFailed, 'NOUSER'