[CalendarServer-changes] [1517]
CalendarServer/branches/users/cdaboo/digest-1510
source_changes at macosforge.org
source_changes at macosforge.org
Tue May 8 09:59:23 PDT 2007
Revision: 1517
http://trac.macosforge.org/projects/calendarserver/changeset/1517
Author: cdaboo at apple.com
Date: 2007-05-08 09:59:23 -0700 (Tue, 08 May 2007)
Log Message:
-----------
Move digest db patch out of Twisted and into CalendarServer code.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/digest-1510/twistedcaldav/directory/digest.py
Removed Paths:
-------------
CalendarServer/branches/users/cdaboo/digest-1510/lib-patches/Twisted/twisted.web2.auth.digest.patch
CalendarServer/branches/users/cdaboo/digest-1510/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch
Deleted: CalendarServer/branches/users/cdaboo/digest-1510/lib-patches/Twisted/twisted.web2.auth.digest.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/digest-1510/lib-patches/Twisted/twisted.web2.auth.digest.patch 2007-05-08 08:26:19 UTC (rev 1516)
+++ CalendarServer/branches/users/cdaboo/digest-1510/lib-patches/Twisted/twisted.web2.auth.digest.patch 2007-05-08 16:59:23 UTC (rev 1517)
@@ -1,340 +0,0 @@
-Index: twisted/web2/auth/digest.py
-===================================================================
---- twisted/web2/auth/digest.py (revision 19773)
-+++ twisted/web2/auth/digest.py (working copy)
-@@ -173,14 +173,110 @@
- return expected == response
-
-
-+class IDigestCredentialsDatabase(Interface):
-+ """
-+ An interface to a digest credentials database that is used to hold per-client digest credentials so that fast
-+ re-authentication can be done with replay attacks etc prevented.
-+ """
-+
-+ def has_key(self, key):
-+ """
-+ See whether the matching key exists.
-+
-+ @param key: the key to check.
-+ @type key: C{str}.
-+
-+ @return: C{True} if the key exists, C{False} otherwise.
-+ """
-+ pass
-+
-+ def set(self, key, value):
-+ """
-+ Store per-client credential information the first time a nonce is generated and used.
-+
-+ @param key: the key for the data to store.
-+ @type key: C{str}
-+ @param value: the data to store.
-+ @type value: any.
-+ """
-+ pass
-+
-+ def get(self, key):
-+ """
-+ Validate client supplied credentials by comparing with the cached values. If valid, store the new
-+ cnonce value in the database so that it can be used on the next validate.
-+
-+ @param key: the key to check.
-+ @type key: C{str}.
-+
-+ @return: the value for the corresponding key, or C{None} if the key is not found.
-+ """
-+ pass
-+
-+ def delete(self, key):
-+ """
-+ Remove the record associated with the supplied key.
-+
-+ @param key: the key to remove.
-+ @type key: C{str}
-+ """
-+ pass
-+
-+ def keys(self):
-+ """
-+ Return all the keys currently available.
-+
-+ @return: a C{list} of C{str} for each key currently in the database.
-+ """
-+ pass
-+
-+class DigestCredentialsMap:
-+
-+ implements(IDigestCredentialsDatabase)
-+
-+ def __init__(self, *args):
-+ self.db = {}
-+
-+ def has_key(self, key):
-+ """
-+ See IDigestCredentialsDatabase.
-+ """
-+ return self.db.has_key(key)
-+
-+ def set(self, key, value):
-+ """
-+ See IDigestCredentialsDatabase.
-+ """
-+ self.db[key] = value
-+
-+ def get(self, key):
-+ """
-+ See IDigestCredentialsDatabase.
-+ """
-+ if self.db.has_key(key):
-+ return self.db[key]
-+ else:
-+ return None
-+
-+ def delete(self, key):
-+ """
-+ See IDigestCredentialsDatabase.
-+ """
-+ if self.db.has_key(key):
-+ del self.db[key]
-+
-+ def keys(self):
-+ """
-+ See IDigestCredentialsDatabase.
-+ """
-+ return self.db.keys()
-+
- class DigestCredentialFactory(object):
- """
- Support for RFC2617 HTTP Digest Authentication
-
- @cvar CHALLENGE_LIFETIME_SECS: The number of seconds for which an
- opaque should be valid.
--
-- @ivar privateKey: A random string used for generating the secure opaque.
- """
-
- implements(ICredentialFactory)
-@@ -189,7 +285,7 @@
-
- scheme = "digest"
-
-- def __init__(self, algorithm, realm):
-+ def __init__(self, algorithm, realm, mapper=DigestCredentialsMap, mapperargs=[]):
- """
- @type algorithm: C{str}
- @param algorithm: case insensitive string that specifies
-@@ -199,87 +295,19 @@
- @type realm: C{str}
- @param realm: case sensitive string that specifies the realm
- portion of the challenge
-+
-+ @type mapper: C{IDigestCredentialsDatabase}
-+ @param realm: the class of mapper to use with this factory
- """
- self.algorithm = algorithm
- self.realm = realm
-+ self.db = mapper(*mapperargs)
-
-- c = tuple([random.randrange(sys.maxint) for _ in range(3)])
--
-- self.privateKey = '%d%d%d' % c
--
- def generateNonce(self):
- c = tuple([random.randrange(sys.maxint) for _ in range(3)])
- c = '%d%d%d' % c
- return c
-
-- def _getTime(self):
-- """
-- Parameterize the time based seed used in generateOpaque
-- so we can deterministically unittest it's behavior.
-- """
-- return time.time()
--
-- def generateOpaque(self, nonce, clientip):
-- """
-- Generate an opaque to be returned to the client.
-- This should be a unique string that can be returned to us and verified.
-- """
--
-- # Now, what we do is encode the nonce, client ip and a timestamp
-- # in the opaque value with a suitable digest
-- key = "%s,%s,%s" % (nonce, clientip, str(int(self._getTime())))
-- digest = md5.new(key + self.privateKey).hexdigest()
-- ekey = key.encode('base64')
-- return "%s-%s" % (digest, ekey.strip('\n'))
--
-- def verifyOpaque(self, opaque, nonce, clientip):
-- """
-- Given the opaque and nonce from the request, as well as the clientip
-- that made the request, verify that the opaque was generated by us.
-- And that it's not too old."
--
-- @param opaque: The opaque value from the Digest response
-- @param nonce: The nonce value from the Digest response
-- @param clientip: The remote IP address of the client making the request
--
-- @return: Return True if the opaque was successfully verified
--
-- @raise L{twisted.cred.error.LoginFailed}
-- """
--
-- # First split the digest from the key
-- opaqueParts = opaque.split('-')
-- if len(opaqueParts) != 2:
-- raise error.LoginFailed('Invalid response, invalid opaque value')
--
-- # Verify the key
-- key = opaqueParts[1].decode('base64')
-- keyParts = key.split(',')
--
-- if len(keyParts) != 3:
-- raise error.LoginFailed('Invalid response, invalid opaque value')
--
-- if keyParts[0] != nonce:
-- raise error.LoginFailed(
-- 'Invalid response, incompatible opaque/nonce values')
--
-- if keyParts[1] != clientip:
-- raise error.LoginFailed(
-- 'Invalid response, incompatible opaque/client values')
--
-- if (int(self._getTime()) - int(keyParts[2]) >
-- DigestCredentialFactory.CHALLENGE_LIFETIME_SECS):
--
-- raise error.LoginFailed(
-- 'Invalid response, incompatible opaque/nonce too old')
--
-- # Verify the digest
-- digest = md5.new(key + self.privateKey).hexdigest()
-- if digest != opaqueParts[0]:
-- raise error.LoginFailed('Invalid response, invalid opaque value')
--
-- return True
--
- def getChallenge(self, peer):
- """
- Generate the challenge for use in the WWW-Authenticate header
-@@ -291,10 +319,15 @@
- """
-
- c = self.generateNonce()
-- o = self.generateOpaque(c, peer.host)
-+
-+ # Make sure it is not a duplicate
-+ if self.db.has_key(c):
-+ raise AssertionError("nonce value already cached in credentials database: %s" % (c,))
-
-+ # The database record is a tuple of (client ip, nonce-count, timestamp)
-+ self.db.set(c, (peer.host, 0, time.time()))
-+
- return {'nonce': c,
-- 'opaque': o,
- 'qop': 'auth',
- 'algorithm': self.algorithm,
- 'realm': self.realm}
-@@ -331,18 +364,96 @@
- if not username:
- raise error.LoginFailed('Invalid response, no username given.')
-
-- if 'opaque' not in auth:
-- raise error.LoginFailed('Invalid response, no opaque given.')
--
- if 'nonce' not in auth:
- raise error.LoginFailed('Invalid response, no nonce given.')
-
-- # Now verify the nonce/opaque values for this client
-- if self.verifyOpaque(auth.get('opaque'),
-- auth.get('nonce'),
-- request.remoteAddr.host):
-+ # Now verify the nonce/cnonce values for this client
-+ if self.validate(auth, request):
-
- return DigestedCredentials(username,
- request.method,
- self.realm,
- auth)
-+ else:
-+ raise error.LoginFailed('Invalid nonce/cnonce values')
-+
-+ def validate(self, auth, request):
-+ """
-+ Check that the parameters in the response represent a valid set of credentials that
-+ may be being re-used.
-+
-+ @param auth: the response parameters.
-+ @type auth: C{dict}
-+ @param request: the request being processed.
-+ @type request: L{twisted.web2.server.Request}
-+
-+ @return: C{True} if validated.
-+ @raise LoginFailed: if validation fails.
-+ """
-+
-+ nonce = auth.get('nonce')
-+ clientip = request.remoteAddr.host
-+ nonce_count = auth.get('nc')
-+
-+ # First check we have this nonce
-+ if not self.db.has_key(nonce):
-+ raise error.LoginFailed('Invalid nonce value: %s' % (nonce,))
-+ db_clientip, db_nonce_count, db_timestamp = self.db.get(nonce)
-+
-+ # Next check client ip
-+ if db_clientip != clientip:
-+ self.invalidate(nonce)
-+ raise error.LoginFailed('Client IPs do not match: %s and %s' % (clientip, db_clientip,))
-+
-+ # cnonce and nonce-count MUST be present if qop is present
-+ if auth.get('qop') is not None:
-+ if auth.get('cnonce') is None:
-+ self.invalidate(nonce)
-+ raise error.LoginFailed('cnonce is required when qop is specified')
-+ if nonce_count is None:
-+ self.invalidate(nonce)
-+ raise error.LoginFailed('nonce-count is required when qop is specified')
-+
-+ # Next check the nonce-count is one greater than the previous one and update it in the DB
-+ try:
-+ nonce_count = int(nonce_count, 16)
-+ except ValueError:
-+ self.invalidate(nonce)
-+ raise error.LoginFailed('nonce-count is not a valid hex string: %s' % (auth.get('nonce-count'),))
-+ if nonce_count != db_nonce_count + 1:
-+ self.invalidate(nonce)
-+ raise error.LoginFailed('nonce-count value out of sequence: %s should be one more than %s' % (nonce_count, db_nonce_count,))
-+ self.db.set(nonce, (db_clientip, nonce_count, db_timestamp))
-+ else:
-+ # When not using qop the stored nonce-count must always be zero.
-+ # i.e. we can't allow a qop auth then a non-qop auth with the same nonce
-+ if db_nonce_count != 0:
-+ self.invalidate(nonce)
-+ raise error.LoginFailed('nonce-count was sent with this nonce: %s' % (nonce,))
-+
-+ # Now check timestamp
-+ if db_timestamp + DigestCredentialFactory.CHALLENGE_LIFETIME_SECS <= time.time():
-+ self.invalidate(nonce)
-+ raise error.LoginFailed('Digest credentials expired')
-+
-+ return True
-+
-+ def invalidate(self, nonce):
-+ """
-+ Invalidate cached credentials for the specified nonce value.
-+
-+ @param nonce: the nonce for the record to invalidate.
-+ @type nonce: C{str}
-+ """
-+ self.db.delete(nonce)
-+
-+ def cleanup(self):
-+ """
-+ This should be called at regular intervals to remove expired credentials from the cache.
-+ """
-+ keys = self.db.keys()
-+ oldest_allowed = time.time() - DigestCredentialFactory.CHALLENGE_LIFETIME_SECS
-+ for key in keys:
-+ ignore_clientip, ignore_cnonce, db_timestamp = self.db.get(key)
-+ if db_timestamp <= oldest_allowed:
-+ self.invalidate(key)
Deleted: CalendarServer/branches/users/cdaboo/digest-1510/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/digest-1510/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch 2007-05-08 08:26:19 UTC (rev 1516)
+++ CalendarServer/branches/users/cdaboo/digest-1510/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch 2007-05-08 16:59:23 UTC (rev 1517)
@@ -1,294 +0,0 @@
-Index: twisted/web2/test/test_httpauth.py
-===================================================================
---- twisted/web2/test/test_httpauth.py (revision 19773)
-+++ twisted/web2/test/test_httpauth.py (working copy)
-@@ -20,21 +20,13 @@
- def __init__(self, *args, **kwargs):
- super(FakeDigestCredentialFactory, self).__init__(*args, **kwargs)
-
-- self.privateKey = "0"
--
- def generateNonce(self):
- """
- Generate a static nonce
- """
- return '178288758716122392881254770685'
-
-- def _getTime(self):
-- """
-- Return a stable time
-- """
-- return 0
-
--
- class BasicAuthTestCase(unittest.TestCase):
- def setUp(self):
- self.credentialFactory = basic.BasicCredentialFactory('foo')
-@@ -86,18 +78,17 @@
- challengeResponse = ('digest',
- {'nonce': challengeNonce,
- 'qop': 'auth', 'realm': 'test realm',
-- 'algorithm': 'md5',
-- 'opaque': challengeOpaque})
-+ 'algorithm': 'md5',})
-
- cnonce = "29fc54aa1641c6fa0e151419361c8f23"
-
- authRequest1 = ('username="username", realm="test realm", nonce="%s", '
-- 'uri="/write/", response="%s", opaque="%s", algorithm="md5", '
-+ 'uri="/write/", response="%s", algorithm="md5", '
- 'cnonce="29fc54aa1641c6fa0e151419361c8f23", nc=00000001, '
- 'qop="auth"')
-
- authRequest2 = ('username="username", realm="test realm", nonce="%s", '
-- 'uri="/write/", response="%s", opaque="%s", algorithm="md5", '
-+ 'uri="/write/", response="%s", algorithm="md5", '
- 'cnonce="29fc54aa1641c6fa0e151419361c8f23", nc=00000002, '
- 'qop="auth"')
-
-@@ -147,7 +138,6 @@
- self.assertEquals(challenge['realm'], 'test realm')
- self.assertEquals(challenge['algorithm'], 'md5')
- self.assertTrue(challenge.has_key("nonce"))
-- self.assertTrue(challenge.has_key("opaque"))
-
- def test_response(self):
- """
-@@ -159,7 +149,7 @@
- clientResponse = authRequest1 % (
- challenge['nonce'],
- self.getDigestResponse(challenge, "00000001"),
-- challenge['opaque'])
-+ )
-
- creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
- self.failUnless(creds.checkPassword('password'))
-@@ -175,7 +165,7 @@
- clientResponse = authRequest1 % (
- challenge['nonce'],
- self.getDigestResponse(challenge, "00000001"),
-- challenge['opaque'])
-+ )
-
- creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
- self.failUnless(creds.checkPassword('password'))
-@@ -183,7 +173,7 @@
- clientResponse = authRequest2 % (
- challenge['nonce'],
- self.getDigestResponse(challenge, "00000002"),
-- challenge['opaque'])
-+ )
-
- creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
- self.failUnless(creds.checkPassword('password'))
-@@ -199,7 +189,7 @@
- clientResponse = authRequest1 % (
- challenge['nonce'],
- self.getDigestResponse(challenge, "00000001"),
-- challenge['opaque'])
-+ )
-
- creds = self.credentialFactory.decode(clientResponse,
- SimpleRequest(None, 'POST', '/'))
-@@ -236,17 +226,6 @@
- _trivial_GET)
- self.assertEquals(str(e), "Invalid response, no nonce given.")
-
-- def test_noOpaque(self):
-- """
-- Test that login fails when our response does not contain a nonce
-- """
--
-- e = self.assertRaises(error.LoginFailed,
-- self.credentialFactory.decode,
-- 'realm="Test",username="Foo"',
-- _trivial_GET)
-- self.assertEquals(str(e), "Invalid response, no opaque given.")
--
- def test_checkHash(self):
- """
- Check that given a hash of the form 'username:realm:password'
-@@ -258,7 +237,7 @@
- clientResponse = authRequest1 % (
- challenge['nonce'],
- self.getDigestResponse(challenge, "00000001"),
-- challenge['opaque'])
-+ )
-
- creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
-
-@@ -268,41 +247,36 @@
- self.failIf(creds.checkHash(
- md5.md5('username:test realm:bogus').hexdigest()))
-
-- def test_invalidOpaque(self):
-+ def test_invalidNonceCount(self):
- """
-- Test that login fails when the opaque does not contain all the required
-- parts.
-+ Test that login fails when the nonce-count is repeated.
- """
-
- credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
-
- challenge = credentialFactory.getChallenge(clientAddress)
-
-- self.assertRaises(
-- error.LoginFailed,
-- credentialFactory.verifyOpaque,
-- 'badOpaque',
-+ clientResponse1 = authRequest1 % (
- challenge['nonce'],
-- clientAddress.host)
-+ self.getDigestResponse(challenge, "00000001"),
-+ )
-
-- badOpaque = ('foo-%s' % (
-- 'nonce,clientip'.encode('base64').strip('\n'),))
--
-- self.assertRaises(
-- error.LoginFailed,
-- credentialFactory.verifyOpaque,
-- badOpaque,
-+ clientResponse2 = authRequest2 % (
- challenge['nonce'],
-- clientAddress.host)
-+ self.getDigestResponse(challenge, "00000002"),
-+ )
-
-+ credentialFactory.decode(clientResponse1, _trivial_GET)
-+ credentialFactory.decode(clientResponse2, _trivial_GET)
-+
- self.assertRaises(
- error.LoginFailed,
-- credentialFactory.verifyOpaque,
-- '',
-- challenge['nonce'],
-- clientAddress.host)
-+ self.credentialFactory.decode,
-+ clientResponse2,
-+ _trivial_GET
-+ )
-
-- def test_incompatibleNonce(self):
-+ def test_invalidNonce(self):
- """
- Test that login fails when the given nonce from the response, does not
- match the nonce encoded in the opaque.
-@@ -311,24 +285,19 @@
- credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
-
- challenge = credentialFactory.getChallenge(clientAddress)
-+ challenge['nonce'] = "noNoncense"
-
-- badNonceOpaque = credentialFactory.generateOpaque(
-- '1234567890',
-- clientAddress.host)
--
-- self.assertRaises(
-- error.LoginFailed,
-- credentialFactory.verifyOpaque,
-- badNonceOpaque,
-+ clientResponse = authRequest1 % (
- challenge['nonce'],
-- clientAddress.host)
-+ self.getDigestResponse(challenge, "00000001"),
-+ )
-
- self.assertRaises(
- error.LoginFailed,
-- credentialFactory.verifyOpaque,
-- badNonceOpaque,
-- '',
-- clientAddress.host)
-+ credentialFactory.decode,
-+ clientResponse,
-+ _trivial_GET
-+ )
-
- def test_incompatibleClientIp(self):
- """
-@@ -338,18 +307,19 @@
-
- credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
-
-- challenge = credentialFactory.getChallenge(clientAddress)
-+ challenge = credentialFactory.getChallenge(address.IPv4Address('TCP', '127.0.0.2', 80))
-
-- badNonceOpaque = credentialFactory.generateOpaque(
-+ clientResponse = authRequest1 % (
- challenge['nonce'],
-- '10.0.0.1')
-+ self.getDigestResponse(challenge, "00000001"),
-+ )
-
- self.assertRaises(
- error.LoginFailed,
-- credentialFactory.verifyOpaque,
-- badNonceOpaque,
-- challenge['nonce'],
-- clientAddress.host)
-+ credentialFactory.decode,
-+ clientResponse,
-+ _trivial_GET
-+ )
-
- def test_oldNonce(self):
- """
-@@ -360,47 +330,20 @@
- credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
-
- challenge = credentialFactory.getChallenge(clientAddress)
-+ clientip, nonce_count, timestamp = credentialFactory.db.get(challenge['nonce'])
-+ credentialFactory.db.set(challenge['nonce'], (clientip, nonce_count, timestamp - 2 * digest.DigestCredentialFactory.CHALLENGE_LIFETIME_SECS))
-
-- key = '%s,%s,%s' % (challenge['nonce'],
-- clientAddress.host,
-- '-137876876')
-- digest = md5.new(key + credentialFactory.privateKey).hexdigest()
-- ekey = key.encode('base64')
--
-- oldNonceOpaque = '%s-%s' % (digest, ekey.strip('\n'))
--
-- self.assertRaises(
-- error.LoginFailed,
-- credentialFactory.verifyOpaque,
-- oldNonceOpaque,
-+ clientResponse = authRequest1 % (
- challenge['nonce'],
-- clientAddress.host)
-+ self.getDigestResponse(challenge, "00000001"),
-+ )
-
-- def test_mismatchedOpaqueChecksum(self):
-- """
-- Test that login fails when the opaque checksum fails verification
-- """
--
-- credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
--
-- challenge = credentialFactory.getChallenge(clientAddress)
--
--
-- key = '%s,%s,%s' % (challenge['nonce'],
-- clientAddress.host,
-- '0')
--
-- digest = md5.new(key + 'this is not the right pkey').hexdigest()
--
-- badChecksum = '%s-%s' % (digest,
-- key.encode('base64').strip('\n'))
--
- self.assertRaises(
- error.LoginFailed,
-- credentialFactory.verifyOpaque,
-- badChecksum,
-- challenge['nonce'],
-- clientAddress.host)
-+ credentialFactory.decode,
-+ clientResponse,
-+ _trivial_GET
-+ )
-
- def test_incompatibleCalcHA1Options(self):
- """
Modified: CalendarServer/branches/users/cdaboo/digest-1510/twistedcaldav/directory/digest.py
===================================================================
--- CalendarServer/branches/users/cdaboo/digest-1510/twistedcaldav/directory/digest.py 2007-05-08 08:26:19 UTC (rev 1516)
+++ CalendarServer/branches/users/cdaboo/digest-1510/twistedcaldav/directory/digest.py 2007-05-08 16:59:23 UTC (rev 1517)
@@ -18,11 +18,14 @@
from twistedcaldav.sql import AbstractSQLDatabase
-from twisted.web2.auth.digest import DigestCredentialFactory, IDigestCredentialsDatabase
+from twisted.web2.auth.digest import DigestCredentialFactory
-from zope.interface.declarations import implements
+from zope.interface import implements, Interface
import cPickle as pickle
+from twisted.cred import error
+from twisted.web2.auth.digest import DigestedCredentials
+import time
import os
"""
@@ -31,6 +34,104 @@
"""
+class IDigestCredentialsDatabase(Interface):
+ """
+ An interface to a digest credentials database that is used to hold per-client digest credentials so that fast
+ re-authentication can be done with replay attacks etc prevented.
+ """
+
+ def has_key(self, key):
+ """
+ See whether the matching key exists.
+
+ @param key: the key to check.
+ @type key: C{str}.
+
+ @return: C{True} if the key exists, C{False} otherwise.
+ """
+ pass
+
+ def set(self, key, value):
+ """
+ Store per-client credential information the first time a nonce is generated and used.
+
+ @param key: the key for the data to store.
+ @type key: C{str}
+ @param value: the data to store.
+ @type value: any.
+ """
+ pass
+
+ def get(self, key):
+ """
+ Validate client supplied credentials by comparing with the cached values. If valid, store the new
+ cnonce value in the database so that it can be used on the next validate.
+
+ @param key: the key to check.
+ @type key: C{str}.
+
+ @return: the value for the corresponding key, or C{None} if the key is not found.
+ """
+ pass
+
+ def delete(self, key):
+ """
+ Remove the record associated with the supplied key.
+
+ @param key: the key to remove.
+ @type key: C{str}
+ """
+ pass
+
+ def keys(self):
+ """
+ Return all the keys currently available.
+
+ @return: a C{list} of C{str} for each key currently in the database.
+ """
+ pass
+
+class DigestCredentialsMap:
+
+ implements(IDigestCredentialsDatabase)
+
+ def __init__(self, *args):
+ self.db = {}
+
+ def has_key(self, key):
+ """
+ See IDigestCredentialsDatabase.
+ """
+ return self.db.has_key(key)
+
+ def set(self, key, value):
+ """
+ See IDigestCredentialsDatabase.
+ """
+ self.db[key] = value
+
+ def get(self, key):
+ """
+ See IDigestCredentialsDatabase.
+ """
+ if self.db.has_key(key):
+ return self.db[key]
+ else:
+ return None
+
+ def delete(self, key):
+ """
+ See IDigestCredentialsDatabase.
+ """
+ if self.db.has_key(key):
+ del self.db[key]
+
+ def keys(self):
+ """
+ See IDigestCredentialsDatabase.
+ """
+ return self.db.keys()
+
class DigestCredentialsDB(AbstractSQLDatabase):
implements(IDigestCredentialsDatabase)
@@ -177,16 +278,36 @@
@type db_path: C{str}
@param db_path: path where the credentials cache is to be stored
"""
- super(QopDigestCredentialFactory, self).__init__(algorithm, realm, mapper=DigestCredentialsDB, mapperargs=[db_path])
+ super(QopDigestCredentialFactory, self).__init__(algorithm, realm)
self.qop = qop
+ self.db = DigestCredentialsDB(db_path)
def getChallenge(self, peer):
"""
+ Generate the challenge for use in the WWW-Authenticate header
Do the default behavior but then strip out any 'qop' from the challenge fields
if no qop was specified.
+
+ @param peer: The L{IAddress} of the requesting client.
+
+ @return: The C{dict} that can be used to generate a WWW-Authenticate
+ header.
"""
- challenge = super(QopDigestCredentialFactory, self).getChallenge(peer)
+ c = self.generateNonce()
+
+ # Make sure it is not a duplicate
+ if self.db.has_key(c):
+ raise AssertionError("nonce value already cached in credentials database: %s" % (c,))
+
+ # The database record is a tuple of (client ip, nonce-count, timestamp)
+ self.db.set(c, (peer.host, 0, time.time()))
+
+ challenge = {'nonce': c,
+ 'qop': 'auth',
+ 'algorithm': self.algorithm,
+ 'realm': self.realm}
+
if self.qop:
challenge['qop'] = self.qop
else:
@@ -200,7 +321,130 @@
if no qop was specified.
"""
- credentials = super(QopDigestCredentialFactory, self).decode(response, request)
- if not self.qop and credentials.fields.has_key('qop'):
- del credentials.fields['qop']
- return credentials
+ """
+ Decode the given response and attempt to generate a
+ L{DigestedCredentials} from it.
+
+ @type response: C{str}
+ @param response: A string of comma seperated key=value pairs
+
+ @type request: L{twisted.web2.server.Request}
+ @param request: the request being processed
+
+ @return: L{DigestedCredentials}
+
+ @raise: L{error.LoginFailed} if the response does not contain a
+ username, a nonce, an opaque, or if the opaque is invalid.
+ """
+ def unq(s):
+ if s[0] == s[-1] == '"':
+ return s[1:-1]
+ return s
+ response = ' '.join(response.splitlines())
+ parts = response.split(',')
+
+ auth = {}
+
+ for (k, v) in [p.split('=', 1) for p in parts]:
+ auth[k.strip()] = unq(v.strip())
+
+ username = auth.get('username')
+ if not username:
+ raise error.LoginFailed('Invalid response, no username given.')
+
+ if 'nonce' not in auth:
+ raise error.LoginFailed('Invalid response, no nonce given.')
+
+ # Now verify the nonce/cnonce values for this client
+ if self.validate(auth, request):
+
+ credentials = DigestedCredentials(username,
+ request.method,
+ self.realm,
+ auth)
+ if not self.qop and credentials.fields.has_key('qop'):
+ del credentials.fields['qop']
+ return credentials
+ else:
+ raise error.LoginFailed('Invalid nonce/cnonce values')
+
+ def validate(self, auth, request):
+ """
+ Check that the parameters in the response represent a valid set of credentials that
+ may be being re-used.
+
+ @param auth: the response parameters.
+ @type auth: C{dict}
+ @param request: the request being processed.
+ @type request: L{twisted.web2.server.Request}
+
+ @return: C{True} if validated.
+ @raise LoginFailed: if validation fails.
+ """
+
+ nonce = auth.get('nonce')
+ clientip = request.remoteAddr.host
+ nonce_count = auth.get('nc')
+
+ # First check we have this nonce
+ if not self.db.has_key(nonce):
+ raise error.LoginFailed('Invalid nonce value: %s' % (nonce,))
+ db_clientip, db_nonce_count, db_timestamp = self.db.get(nonce)
+
+ # Next check client ip
+ if db_clientip != clientip:
+ self.invalidate(nonce)
+ raise error.LoginFailed('Client IPs do not match: %s and %s' % (clientip, db_clientip,))
+
+ # cnonce and nonce-count MUST be present if qop is present
+ if auth.get('qop') is not None:
+ if auth.get('cnonce') is None:
+ self.invalidate(nonce)
+ raise error.LoginFailed('cnonce is required when qop is specified')
+ if nonce_count is None:
+ self.invalidate(nonce)
+ raise error.LoginFailed('nonce-count is required when qop is specified')
+
+ # Next check the nonce-count is one greater than the previous one and update it in the DB
+ try:
+ nonce_count = int(nonce_count, 16)
+ except ValueError:
+ self.invalidate(nonce)
+ raise error.LoginFailed('nonce-count is not a valid hex string: %s' % (auth.get('nonce-count'),))
+ if nonce_count != db_nonce_count + 1:
+ self.invalidate(nonce)
+ raise error.LoginFailed('nonce-count value out of sequence: %s should be one more than %s' % (nonce_count, db_nonce_count,))
+ self.db.set(nonce, (db_clientip, nonce_count, db_timestamp))
+ else:
+ # When not using qop the stored nonce-count must always be zero.
+ # i.e. we can't allow a qop auth then a non-qop auth with the same nonce
+ if db_nonce_count != 0:
+ self.invalidate(nonce)
+ raise error.LoginFailed('nonce-count was sent with this nonce: %s' % (nonce,))
+
+ # Now check timestamp
+ if db_timestamp + DigestCredentialFactory.CHALLENGE_LIFETIME_SECS <= time.time():
+ self.invalidate(nonce)
+ raise error.LoginFailed('Digest credentials expired')
+
+ return True
+
+ def invalidate(self, nonce):
+ """
+ Invalidate cached credentials for the specified nonce value.
+
+ @param nonce: the nonce for the record to invalidate.
+ @type nonce: C{str}
+ """
+ self.db.delete(nonce)
+
+ def cleanup(self):
+ """
+ This should be called at regular intervals to remove expired credentials from the cache.
+ """
+ keys = self.db.keys()
+ oldest_allowed = time.time() - DigestCredentialFactory.CHALLENGE_LIFETIME_SECS
+ for key in keys:
+ ignore_clientip, ignore_cnonce, db_timestamp = self.db.get(key)
+ if db_timestamp <= oldest_allowed:
+ self.invalidate(key)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20070508/eb75e7ca/attachment.html
More information about the calendarserver-changes
mailing list