import md5, sha, time, random
def generate_nonce(bits, randomness=None):
"This could be stronger"
if bits%8 != 0:
raise ValueError, "bits must be a multiple of 8"
nonce = sha.new(str(randomness) + str(time.time()) +
str(random.random()) ).hexdigest()
nonce = nonce[:bits/4]
return nonce
def parse_keqv_list(l):
"""Parse list of key=value strings where keys are not duplicated."""
parsed = {}
for elt in l:
k, v = elt.split('=', 1)
if v[0] == '"' and v[-1] == '"':
v = v[1:-1]
parsed[k] = v
return parsed
class DigestAuthServer:
def __init__(self, default_realm, algorithm="MD5"):
self.default_realm = default_realm
if algorithm != 'MD5':
raise ValueError, "Don't know about algorithm %s"%(MD5)
self.algorithm = algorithm
self._user_hashes = {}
def get_algorithm_impls(self, algorithm=None):
# lambdas assume digest modules are imported at the top level
if algorithm is None:
algorithm = self.algorithm
if algorithm == 'MD5':
H = lambda x: md5.new(x).hexdigest()
elif algorithm == 'SHA':
H = lambda x: sha.new(x).hexdigest()
# XXX MD5-sess
KD = lambda s, d, H=H: H("%s:%s" % (s, d))
return H, KD
def add_user(self, user, password, realm=None):
"add the given user and password"
H, KD = self.get_algorithm_impls()
if realm is None:
realm = self.default_realm
A1 = H('%s:%s:%s'%(user, realm, password))
self._user_hashes[(user, realm)] = A1
def add_user_hash(self, user, A1, realm=None):
"add the given user with the stated hash"
if realm is None:
realm = self.default_realm
self._user_hashes[(user, realm)] = A1
def parse_apache_digest_authfile(self, filename):
"Parse a password file, as generated by htdigest"
for line in open(filename, 'rU'):
line = line.strip()
user, realm, hash = line.split(':')
self.add_user_hash(user, hash, realm)
def generate_challenge(self, realm=None):
if realm is None:
realm = self.default_realm
# We should save off the nonce to make sure it's one we've
# offered already. And check for replay attacks :-(
chal = 'realm="%s", nonce="%s", ' \
'algorithm=%s, qop="auth"'%(realm,
generate_nonce(bits=208),
self.algorithm)
return chal
# Firebird
# username="anthony", realm="TestAuth",
# nonce="9da7db19648f95bd71f26a07b3423d91917b5205", uri="/test/foo",
# algorithm=MD5, response="f61ca0cb8a85e9bd985b7ab808978f1e",
# qop=auth, nc=00000001, cnonce="424a1ed1ddaa76ca"
# Konqi
# username="anthony", realm="TestAuth",
# nonce="7c8bdda0ed44db7de74bee97cec8dfd4fb59af0f", uri="/test/foo",
# algorithm="MD5", qop="auth", cnonce="ODQwMTk=", nc=00000001,
# response="1bebadb47d2aa5eab53cb419b94599f3"
def check_auth(self, header, method='GET'):
"Check a response to our auth challenge"
from urllib2 import parse_http_list
H, KD = self.get_algorithm_impls()
resp = parse_keqv_list(parse_http_list(header))
if resp.get('algorithm', 'MD5').upper() != self.algorithm:
return False, "unknown algo %s"%algorithm
user = resp['username']
realm = resp['realm']
nonce = resp['nonce']
# XXX Check the nonce is something we've issued
HA1 = self._user_hashes.get((user,realm))
if not HA1:
return False, "unknown user/realm %s/%s"%(user, realm)
qop = resp.get('qop')
if qop != 'auth':
return False, "unknown qop %r"%(qop)
cnonce, ncvalue = resp.get('cnonce'), resp.get('nc')
if not cnonce or not ncvalue:
return False, "failed to provide cnonce"
# Check the URI is correct!
A2 = '%s:%s'%(method, resp['uri'])
noncebit = "%s:%s:%s:%s:%s" % (nonce,ncvalue,cnonce,qop,H(A2))
respdig = KD(HA1, noncebit)
if respdig != resp['response']:
return False, "response incorrect"
print "all ok"
return True, "OK"
|