[CalendarServer-changes] [10625] CalendarServer/branches/users/glyph/digest-auth-redux

source_changes at macosforge.org source_changes at macosforge.org
Fri Feb 1 15:53:17 PST 2013


Revision: 10625
          http://trac.calendarserver.org//changeset/10625
Author:   glyph at apple.com
Date:     2013-02-01 15:53:17 -0800 (Fri, 01 Feb 2013)
Log Message:
-----------
Replace the guts of digest auth implementation with Twisted's, since it's somewhat redundant.

Modified Paths:
--------------
    CalendarServer/branches/users/glyph/digest-auth-redux/twext/web2/auth/digest.py
    CalendarServer/branches/users/glyph/digest-auth-redux/twext/web2/test/test_httpauth.py

Property Changed:
----------------
    CalendarServer/branches/users/glyph/digest-auth-redux/

Modified: CalendarServer/branches/users/glyph/digest-auth-redux/twext/web2/auth/digest.py
===================================================================
--- CalendarServer/branches/users/glyph/digest-auth-redux/twext/web2/auth/digest.py	2013-02-01 23:52:26 UTC (rev 10624)
+++ CalendarServer/branches/users/glyph/digest-auth-redux/twext/web2/auth/digest.py	2013-02-01 23:53:17 UTC (rev 10625)
@@ -28,17 +28,25 @@
 
 http://www.faqs.org/rfcs/rfc2617.html
 """
-import sys
-import time
-import random
 
-from zope.interface import implements, Interface
+from zope.interface import implements
 
 from twisted.python.hashlib import md5, sha1
-from twisted.cred import credentials, error
-from twisted.internet.defer import succeed
+from twisted.cred import credentials
+
+# FIXME: Technically speaking - although you can't tell from looking at them -
+# these APIs are private, they're defined within twisted.cred._digest.  There
+# should probably be some upstream bugs agains Twisted to more aggressively hide
+# implementation details like these if they're not supposed to be used, so we
+# can see the private-ness more clearly.  The fix is really just to eliminate
+# this whole module though, and use the Twisted stuff via the public interface,
+# which should be sufficient to do digest auth.
+
+from twisted.cred.credentials import (calcHA1 as _origCalcHA1,
+                                      calcResponse as _origCalcResponse,
+                                      calcHA2 as _origCalcHA2)
+from twisted.internet.defer import maybeDeferred
 from twext.web2.auth.interfaces import ICredentialFactory
-from twext.web2.http_headers import Token, tokenize, split, parseKeyValue
 
 
 # The digest math
@@ -50,58 +58,29 @@
 }
 
 # DigestCalcHA1
-def calcHA1(
-    pszAlg,
-    pszUserName,
-    pszRealm,
-    pszPassword,
-    pszNonce,
-    pszCNonce,
-    preHA1=None
-):
+def calcHA1(pszAlg, pszUserName, pszRealm, pszPassword, pszNonce, pszCNonce,
+            preHA1=None):
     """
     @param pszAlg: The name of the algorithm to use to calculate the digest.
         Currently supported are md5 md5-sess and sha.
 
     @param pszUserName: The username
+
     @param pszRealm: The realm
+
     @param pszPassword: The password
+
     @param pszNonce: The nonce
+
     @param pszCNonce: The cnonce
 
     @param preHA1: If available this is a str containing a previously
-       calculated HA1 as a hex string. If this is given then the values for
-       pszUserName, pszRealm, and pszPassword are ignored.
+        calculated HA1 as a hex string.  If this is given then the values for
+        pszUserName, pszRealm, and pszPassword are ignored.
     """
+    return _origCalcHA1(pszAlg, pszUserName, pszRealm, pszPassword, pszNonce,
+                        pszCNonce, preHA1)
 
-    if (preHA1 and (pszUserName or pszRealm or pszPassword)):
-        raise TypeError(("preHA1 is incompatible with the pszUserName, "
-                         "pszRealm, and pszPassword arguments"))
-
-    if preHA1 is None:
-        # We need to calculate the HA1 from the username:realm:password
-        m = algorithms[pszAlg]()
-        m.update(pszUserName)
-        m.update(":")
-        m.update(pszRealm)
-        m.update(":")
-        m.update(pszPassword)
-        HA1 = m.digest()
-    else:
-        # We were given a username:realm:password
-        HA1 = preHA1.decode('hex')
-
-    if pszAlg == "md5-sess":
-        m = algorithms[pszAlg]()
-        m.update(HA1)
-        m.update(":")
-        m.update(pszNonce)
-        m.update(":")
-        m.update(pszCNonce)
-        HA1 = m.digest()
-
-    return HA1.encode('hex')
-
 # DigestCalcResponse
 def calcResponse(
     HA1,
@@ -114,285 +93,35 @@
     pszDigestUri,
     pszHEntity,
 ):
-    m = algorithms[algo]()
-    m.update(pszMethod)
-    m.update(":")
-    m.update(pszDigestUri)
-    if pszQop == "auth-int":
-        m.update(":")
-        m.update(pszHEntity)
-    HA2 = m.digest().encode('hex')
+    return _origCalcResponse(HA1, _origCalcHA2(algo, pszMethod, pszDigestUri,
+                                               pszQop, pszHEntity),
+                             algo, pszNonce, pszNonceCount, pszCNonce, pszQop)
 
-    m = algorithms[algo]()
-    m.update(HA1)
-    m.update(":")
-    m.update(pszNonce)
-    m.update(":")
-    if pszNonceCount and pszCNonce: # pszQop:
-        m.update(pszNonceCount)
-        m.update(":")
-        m.update(pszCNonce)
-        m.update(":")
-        m.update(pszQop)
-        m.update(":")
-    m.update(HA2)
-    respHash = m.digest().encode('hex')
-    return respHash
 
 
-class IUsernameDigestHash(Interface):
-    """
-    This credential is used when a CredentialChecker has access to the hash
-    of the username:realm:password as in an Apache .htdigest file.
-    """
-    def checkHash(self, digestHash):
-        """
-        @param digestHash: The hashed username:realm:password to check against.
+DigestedCredentials = credentials.DigestedCredentials
 
-        @return: a deferred which becomes, or a boolean indicating if the
-            hash matches.
-        """
-
-
-class DigestedCredentials:
-    """Yet Another Simple HTTP Digest authentication scheme"""
-
-    implements(credentials.IUsernameHashedPassword,
-               IUsernameDigestHash)
-
-    def __init__(self, username, method, realm, fields, originalMethod=None):
-        self.username = username
-        self.method = method
-        self.realm = realm
-        self.fields = fields
-        if originalMethod:
-            self.originalMethod = originalMethod
-        else:
-            self.originalMethod = method
-
-    def checkPassword(self, password):
-        response = self.fields.get('response')
-        uri = self.fields.get('uri')
-        nonce = self.fields.get('nonce')
-        cnonce = self.fields.get('cnonce')
-        nc = self.fields.get('nc')
-        algo = self.fields.get('algorithm', 'md5').lower()
-        qop = self.fields.get('qop', 'auth')
-
-        expected = calcResponse(
-            calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
-            algo, nonce, nc, cnonce, qop, self.originalMethod, uri, None
-        )
-
-        if expected == response:
-            return True
-
-        # IE7 sends cnonce and nc values, but auth fails if they are used.
-        # So try again without them...
-        # They can be omitted for backwards compatibility [RFC 2069].
-        if nc is not None or cnonce is not None:
-            expected = calcResponse(
-                calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
-                algo, nonce, None, None, qop, self.originalMethod, uri, None
-            )
-            if expected == response:
-                return True
-
-    def checkHash(self, digestHash):
-        response = self.fields.get('response')
-        uri = self.fields.get('uri')
-        nonce = self.fields.get('nonce')
-        cnonce = self.fields.get('cnonce')
-        nc = self.fields.get('nc')
-        algo = self.fields.get('algorithm', 'md5').lower()
-        qop = self.fields.get('qop', 'auth')
-
-        expected = calcResponse(
-            calcHA1(algo, None, None, None, nonce, cnonce, preHA1=digestHash),
-            algo, nonce, nc, cnonce, qop, self.originalMethod, uri, None
-        )
-
-        return expected == response
-
-
 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)
 
-    CHALLENGE_LIFETIME_SECS = 15 * 60    # 15 minutes
-
-    scheme = "digest"
-
     def __init__(self, algorithm, realm):
-        """
-        @type algorithm: C{str}
-        @param algorithm: case insensitive string that specifies
-            the hash algorithm used, should be either, md5, md5-sess
-            or sha
+        self._real = credentials.DigestCredentialFactory(algorithm, realm)
 
-        @type realm: C{str}
-        @param realm: case sensitive string that specifies the realm
-            portion of the challenge
-        """
-        self.algorithm = algorithm
-        self.realm = realm
+    scheme = 'digest'
 
-        c = tuple([random.randrange(sys.maxint) for _ in range(3)])
+    def getChallenge(self, peer):
+        return maybeDeferred(self._real.getChallenge, peer.host)
 
-        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 generateOpaque(self, *a, **k):
+        return self._real._generateOpaque(*a, **k)
 
-    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(key + self.privateKey).hexdigest()
-        ekey = key.encode('base64')
-        return "%s-%s" % (digest, ekey.replace('\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.
+        return self._real._verifyOpaque(opaque, nonce, clientip)
 
-        @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: C{True} if the opaque was successfully verified.
-
-        @raise error.LoginFailed: if C{opaque} could not be parsed or
-            contained the wrong values.
-        """
-
-        # 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(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
-
-        @param peer: The L{IAddress} of the requesting client.
-
-        @return: The C{dict} that can be used to generate a WWW-Authenticate
-            header.
-        """
-
-        c = self.generateNonce()
-        o = self.generateOpaque(c, peer.host)
-
-        return succeed({'nonce': c,
-            'opaque': o,
-            'qop': 'auth',
-            'algorithm': self.algorithm,
-            'realm': self.realm,
-        })
-
     def decode(self, response, request):
-        """
-        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{twext.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.
-        """
-        response = ' '.join(response.splitlines())
-        
-        try:
-            parts = split(tokenize((response,), foldCase=False), Token(","))
-    
-            auth = {}
-    
-            for (k, v) in [parseKeyValue(p) for p in parts]:
-                auth[k.strip()] = v.strip()
-        except ValueError:
-            raise error.LoginFailed('Invalid response.')
-            
-        username = auth.get('username')
-        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):
-
-            if hasattr(request, "originalMethod"):
-                originalMethod = request.originalMethod
-            else:
-                originalMethod = None
-
-            return succeed(DigestedCredentials(username,
-                                               request.method,
-                                               self.realm,
-                                               auth,
-                                               originalMethod))
+        method = getattr(request, "originalMethod", request.method)
+        host = request.remoteAddr.host
+        return self._real.decode(response, method, host)

Modified: CalendarServer/branches/users/glyph/digest-auth-redux/twext/web2/test/test_httpauth.py
===================================================================
--- CalendarServer/branches/users/glyph/digest-auth-redux/twext/web2/test/test_httpauth.py	2013-02-01 23:52:26 UTC (rev 10624)
+++ CalendarServer/branches/users/glyph/digest-auth-redux/twext/web2/test/test_httpauth.py	2013-02-01 23:53:17 UTC (rev 10625)
@@ -24,16 +24,27 @@
 
     def __init__(self, *args, **kwargs):
         super(FakeDigestCredentialFactory, self).__init__(*args, **kwargs)
+        self._real.privateKey = self._fakeStaticPrivateKey
 
-        self.privateKey = "0"
+        # FIXME: These tests are somewhat redundant with the tests for Twisted's
+        # built-in digest auth; these private values need to be patched to
+        # create deterministic results, but at some future point the whole
+        # digest module should be removed from twext.web2 (as all of twext.web2
+        # should be removed) and we can just get rid of this.
 
-    def generateNonce(self):
+        self._real._generateNonce = self._fakeStaticNonce
+        self._real._getTime = self._fakeStaticTime
+
+
+    _fakeStaticPrivateKey = "0"
+
+    def _fakeStaticNonce(self):
         """
         Generate a static nonce
         """
         return '178288758716122392881254770685'
 
-    def _getTime(self):
+    def _fakeStaticTime(self):
         """
         Return a stable time
         """
@@ -376,7 +387,8 @@
             key = '%s,%s,%s' % (challenge['nonce'],
                                 clientAddress.host,
                                 '-137876876')
-            digest = md5(key + credentialFactory.privateKey).hexdigest()
+            digest = (md5(key + credentialFactory._fakeStaticPrivateKey)
+                      .hexdigest())
             ekey = key.encode('base64')
 
             oldNonceOpaque = '%s-%s' % (digest, ekey.strip('\n'))
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130201/4573f743/attachment-0001.html>


More information about the calendarserver-changes mailing list