[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