[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