[CalendarServer-changes] [747]
CalendarServer/trunk/lib-patches/Twisted
source_changes at macosforge.org
source_changes at macosforge.org
Thu Dec 7 13:10:06 PST 2006
Revision: 747
http://trac.macosforge.org/projects/calendarserver/changeset/747
Author: cdaboo at apple.com
Date: 2006-12-07 13:10:06 -0800 (Thu, 07 Dec 2006)
Log Message:
-----------
Rework digest authentication so that clients can replay nonce/opaque values within a 15 minute period.
Added Paths:
-----------
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch
Added: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch 2006-12-07 21:10:06 UTC (rev 747)
@@ -0,0 +1,92 @@
+Index: twisted/web2/auth/digest.py
+===================================================================
+--- twisted/web2/auth/digest.py (revision 18545)
++++ twisted/web2/auth/digest.py (working copy)
+@@ -5,6 +5,7 @@
+
+ http://www.faqs.org/rfcs/rfc2617.html
+ """
++import time
+
+ from twisted.cred import credentials, error
+ from zope.interface import implements
+@@ -118,10 +119,12 @@
+ class DigestCredentialFactory:
+ implements(ICredentialFactory)
+
+- CHALLENGE_LIFETIME = 15
++ CHALLENGE_LIFETIME_SECS = 15 * 60 # 15 minutes
+
+ scheme = "digest"
+
++ pkey = '%d%d%d' % tuple([random.randrange(sys.maxint) for _ in range(3)])
++
+ def __init__(self, algorithm, realm):
+ """@type algorithm: C{str}
+ @param algorithm: case insensitive string that specifies
+@@ -132,7 +135,6 @@
+ @param realm: case sensitive string that specifies the realm
+ portion of the challenge
+ """
+- self.outstanding = {}
+ self.algorithm = algorithm
+ self.realm = realm
+
+@@ -141,13 +143,41 @@
+ c = '%d%d%d' % c
+ return c
+
+- def generateOpaque(self):
+- return str(random.randrange(sys.maxint))
++ def generateOpaque(self, nonce, clientip):
++ # 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(time.time())))
++ digest = md5.new(key + DigestCredentialFactory.pkey).hexdigest()
++ ekey = key.encode('base64')
++ return "%s-%s" % (digest, ekey.replace('\n', ''),)
+
++ def verifyOpaque(self, opaque, nonce, clientip):
++ # First split the digest from the key
++ opaque_parts = opaque.split('-')
++ if len(opaque_parts) != 2:
++ raise error.LoginFailed('Invalid response, invalid opaque value')
++
++ # Verify the key
++ key = opaque_parts[1].decode('base64')
++ key_parts = key.split(',')
++ if len(key_parts) != 3:
++ raise error.LoginFailed('Invalid response, invalid opaque value')
++ if key_parts[0] != nonce:
++ raise error.LoginFailed('Invalid response, incompatible opaque/nonce values')
++ if key_parts[1] != clientip:
++ raise error.LoginFailed('Invalid response, incompatible opaque/client values')
++ if int(time.time()) - int(key_parts[2]) > DigestCredentialFactory.CHALLENGE_LIFETIME_SECS:
++ raise error.LoginFailed('Invalid response, incompatible opaque/nonce too old')
++
++ # Verify the digest
++ digest = md5.new(key + DigestCredentialFactory.pkey).hexdigest()
++ if digest != opaque_parts[0]:
++ raise error.LoginFailed('Invalid response, invalid opaque value')
++
+ def getChallenge(self, peer):
+ c = self.generateNonce()
+- o = self.generateOpaque()
+- self.outstanding[o] = c
++ o = self.generateOpaque(c, peer.host)
++
+ return {'nonce': c,
+ 'opaque': o,
+ 'qop': 'auth',
+@@ -167,9 +197,7 @@
+ if not username:
+ raise error.LoginFailed('Invalid response, no username given')
+
+- if auth.get('opaque') not in self.outstanding:
+- raise error.LoginFailed('Invalid response, opaque not outstanding')
+-
+- del self.outstanding[auth['opaque']]
++ # Now verify the nonce/opaque values for this client
++ self.verifyOpaque(auth.get('opaque'), auth.get('nonce'), request.remoteAddr.host)
+
+ return DigestedCredentials(username, request.method, self.realm, auth)
Added: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch 2006-12-07 21:10:06 UTC (rev 747)
@@ -0,0 +1,110 @@
+Index: twisted/web2/test/test_httpauth.py
+===================================================================
+--- twisted/web2/test/test_httpauth.py (revision 18545)
++++ twisted/web2/test/test_httpauth.py (working copy)
+@@ -1,3 +1,6 @@
++import md5
++import time
++from twisted.internet import address
+ from twisted.trial import unittest
+ from twisted.internet import defer
+ from twisted.cred import error
+@@ -12,8 +15,13 @@
+ def generateNonce(self):
+ return '178288758716122392881254770685'
+
+- def generateOpaque(self):
+- return '1041524039'
++ def generateOpaque(self, nonce, clientip):
++ # 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(0)))
++ digest = md5.new(key + "0").hexdigest()
++ ekey = key.encode('base64')
++ return "%s-%s" % (digest, ekey.replace('\n', ''),)
+
+ class BasicAuthTestCase(unittest.TestCase):
+ def setUp(self):
+@@ -56,32 +64,63 @@
+
+ challengeResponse = ('digest', {'nonce': '178288758716122392881254770685',
+ 'qop': 'auth', 'realm': 'test realm',
+- 'algorithm': 'md5', 'opaque': '1041524039'})
++ 'algorithm': 'md5',
++ 'opaque': '75c4bd95b96b7b7341c646c6502f0833-MTc4Mjg4NzU4NzE2MTIyMzkyODgxMjU0NzcwNjg1LHJlbW90ZWhvc3QsMA=='})
+
+-authRequest = 'username="username", realm="test realm", nonce="178288758716122392881254770685", uri="/write/", response="62f388be1cf678fbdfce87910871bcc5", opaque="1041524039", algorithm="md5", cnonce="29fc54aa1641c6fa0e151419361c8f23", nc=00000001, qop="auth"'
++cnonce = "29fc54aa1641c6fa0e151419361c8f23"
+
++authRequest1 = 'username="username", realm="test realm", nonce="%s", uri="/write/", response="%s", opaque="%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", cnonce="29fc54aa1641c6fa0e151419361c8f23", nc=00000002, qop="auth"'
++
+ namelessAuthRequest = 'realm="test realm",nonce="doesn\'t matter"'
+
+ class DigestAuthTestCase(unittest.TestCase):
+ def setUp(self):
+- self.credentialFactory = FakeDigestCredentialFactory('md5',
++ self.credentialFactory = digest.DigestCredentialFactory('md5',
+ 'test realm')
+
++ def getDigestResponse(self, challenge, ncount):
++ nonce = challenge.get('nonce')
++ algo = challenge.get('algorithm').lower()
++ qop = challenge.get('qop')
++
++ expected = digest.calcResponse(
++ digest.calcHA1(algo, "username", "test realm", "password", nonce, cnonce),
++ algo, nonce, ncount, cnonce, qop, "GET", "/write/", None
++ )
++ return expected
++
+ def testGetChallenge(self):
+- self.assertEquals(
+- self.credentialFactory.getChallenge(None),
+- challengeResponse[1])
++ challenge = self.credentialFactory.getChallenge(address.IPv4Address('TCP', "127.0.0.1", 80))
++ self.assertEquals(challenge['qop'], 'auth')
++ self.assertEquals(challenge['realm'], 'test realm')
++ self.assertEquals(challenge['algorithm'], 'md5')
++ self.assertTrue(challenge.has_key("nonce"))
++ self.assertTrue(challenge.has_key("opaque"))
+
+ def testResponse(self):
+- challenge = self.credentialFactory.getChallenge(None)
++ challenge = self.credentialFactory.getChallenge(address.IPv4Address('TCP', "127.0.0.1", 80))
+
+- creds = self.credentialFactory.decode(authRequest, _trivial_GET)
++ clientResponse = authRequest1 % (challenge['nonce'], self.getDigestResponse(challenge, "00000001"), challenge['opaque'])
++ creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
+ self.failUnless(creds.checkPassword('password'))
+
++ def testMultiResponse(self):
++ challenge = self.credentialFactory.getChallenge(address.IPv4Address('TCP', "127.0.0.1", 80))
++
++ clientResponse = authRequest1 % (challenge['nonce'], self.getDigestResponse(challenge, "00000001"), challenge['opaque'])
++ creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
++ self.failUnless(creds.checkPassword('password'))
++
++ clientResponse = authRequest2 % (challenge['nonce'], self.getDigestResponse(challenge, "00000002"), challenge['opaque'])
++ creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
++ self.failUnless(creds.checkPassword('password'))
++
+ def testFailsWithDifferentMethod(self):
+- challenge = self.credentialFactory.getChallenge(None)
++ challenge = self.credentialFactory.getChallenge(address.IPv4Address('TCP', "127.0.0.1", 80))
+
+- creds = self.credentialFactory.decode(authRequest, SimpleRequest(None, 'POST', '/'))
++ clientResponse = authRequest1 % (challenge['nonce'], self.getDigestResponse(challenge, "00000001"), challenge['opaque'])
++ creds = self.credentialFactory.decode(clientResponse, SimpleRequest(None, 'POST', '/'))
+ self.failIf(creds.checkPassword('password'))
+
+ def testNoUsername(self):
+@@ -221,7 +260,7 @@
+
+ def respond(ign):
+ d = self.assertResponse((root, 'http://localhost/',
+- {'authorization': authRequest}),
++ {'authorization': authRequest1}),
+ (200,
+ {},
+ None))
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20061207/e7004689/attachment.html
More information about the calendarserver-changes
mailing list