class DigestAuthTests(TestCase): """ L{TestCase} mixin class which defines a number of tests for L{DigestCredentialFactory}. Because this mixin defines C{setUp}, it must be inherited before L{TestCase}. """ def setUp(self): """ Create a DigestCredentialFactory for testing """ self.username = "******" self.password = "******" self.realm = "test realm" self.algorithm = "md5" self.cnonce = "29fc54aa1641c6fa0e151419361c8f23" self.qop = "auth" self.uri = "/write/" self.clientAddress = IPv4Address('TCP', '10.2.3.4', 43125) self.method = 'GET' self.credentialFactory = DigestCredentialFactory( self.algorithm, self.realm) def test_MD5HashA1(self, _algorithm='md5', _hash=md5): """ L{calcHA1} accepts the C{'md5'} algorithm and returns an MD5 hash of its parameters, excluding the nonce and cnonce. """ nonce = 'abc123xyz' hashA1 = calcHA1(_algorithm, self.username, self.realm, self.password, nonce, self.cnonce) a1 = '%s:%s:%s' % (self.username, self.realm, self.password) expected = _hash(a1).hexdigest() self.assertEqual(hashA1, expected) def test_MD5SessionHashA1(self): """ L{calcHA1} accepts the C{'md5-sess'} algorithm and returns an MD5 hash of its parameters, including the nonce and cnonce. """ nonce = 'xyz321abc' hashA1 = calcHA1('md5-sess', self.username, self.realm, self.password, nonce, self.cnonce) a1 = '%s:%s:%s' % (self.username, self.realm, self.password) ha1 = md5(a1).digest() a1 = '%s:%s:%s' % (ha1, nonce, self.cnonce) expected = md5(a1).hexdigest() self.assertEqual(hashA1, expected) def test_SHAHashA1(self): """ L{calcHA1} accepts the C{'sha'} algorithm and returns a SHA hash of its parameters, excluding the nonce and cnonce. """ self.test_MD5HashA1('sha', sha1) def test_MD5HashA2Auth(self, _algorithm='md5', _hash=md5): """ L{calcHA2} accepts the C{'md5'} algorithm and returns an MD5 hash of its arguments, excluding the entity hash for QOP other than C{'auth-int'}. """ method = 'GET' hashA2 = calcHA2(_algorithm, method, self.uri, 'auth', None) a2 = '%s:%s' % (method, self.uri) expected = _hash(a2).hexdigest() self.assertEqual(hashA2, expected) def test_MD5HashA2AuthInt(self, _algorithm='md5', _hash=md5): """ L{calcHA2} accepts the C{'md5'} algorithm and returns an MD5 hash of its arguments, including the entity hash for QOP of C{'auth-int'}. """ method = 'GET' hentity = 'foobarbaz' hashA2 = calcHA2(_algorithm, method, self.uri, 'auth-int', hentity) a2 = '%s:%s:%s' % (method, self.uri, hentity) expected = _hash(a2).hexdigest() self.assertEqual(hashA2, expected) def test_MD5SessHashA2Auth(self): """ L{calcHA2} accepts the C{'md5-sess'} algorithm and QOP of C{'auth'} and returns the same value as it does for the C{'md5'} algorithm. """ self.test_MD5HashA2Auth('md5-sess') def test_MD5SessHashA2AuthInt(self): """ L{calcHA2} accepts the C{'md5-sess'} algorithm and QOP of C{'auth-int'} and returns the same value as it does for the C{'md5'} algorithm. """ self.test_MD5HashA2AuthInt('md5-sess') def test_SHAHashA2Auth(self): """ L{calcHA2} accepts the C{'sha'} algorithm and returns a SHA hash of its arguments, excluding the entity hash for QOP other than C{'auth-int'}. """ self.test_MD5HashA2Auth('sha', sha1) def test_SHAHashA2AuthInt(self): """ L{calcHA2} accepts the C{'sha'} algorithm and returns a SHA hash of its arguments, including the entity hash for QOP of C{'auth-int'}. """ self.test_MD5HashA2AuthInt('sha', sha1) def test_MD5HashResponse(self, _algorithm='md5', _hash=md5): """ L{calcResponse} accepts the C{'md5'} algorithm and returns an MD5 hash of its parameters, excluding the nonce count, client nonce, and QoP value if the nonce count and client nonce are C{None} """ hashA1 = 'abc123' hashA2 = '789xyz' nonce = 'lmnopq' response = '%s:%s:%s' % (hashA1, nonce, hashA2) expected = _hash(response).hexdigest() digest = calcResponse(hashA1, hashA2, _algorithm, nonce, None, None, None) self.assertEqual(expected, digest) def test_MD5SessionHashResponse(self): """ L{calcResponse} accepts the C{'md5-sess'} algorithm and returns an MD5 hash of its parameters, excluding the nonce count, client nonce, and QoP value if the nonce count and client nonce are C{None} """ self.test_MD5HashResponse('md5-sess') def test_SHAHashResponse(self): """ L{calcResponse} accepts the C{'sha'} algorithm and returns a SHA hash of its parameters, excluding the nonce count, client nonce, and QoP value if the nonce count and client nonce are C{None} """ self.test_MD5HashResponse('sha', sha1) def test_MD5HashResponseExtra(self, _algorithm='md5', _hash=md5): """ L{calcResponse} accepts the C{'md5'} algorithm and returns an MD5 hash of its parameters, including the nonce count, client nonce, and QoP value if they are specified. """ hashA1 = 'abc123' hashA2 = '789xyz' nonce = 'lmnopq' nonceCount = '00000004' clientNonce = 'abcxyz123' qop = 'auth' response = '%s:%s:%s:%s:%s:%s' % ( hashA1, nonce, nonceCount, clientNonce, qop, hashA2) expected = _hash(response).hexdigest() digest = calcResponse( hashA1, hashA2, _algorithm, nonce, nonceCount, clientNonce, qop) self.assertEqual(expected, digest) def test_MD5SessionHashResponseExtra(self): """ L{calcResponse} accepts the C{'md5-sess'} algorithm and returns an MD5 hash of its parameters, including the nonce count, client nonce, and QoP value if they are specified. """ self.test_MD5HashResponseExtra('md5-sess') def test_SHAHashResponseExtra(self): """ L{calcResponse} accepts the C{'sha'} algorithm and returns a SHA hash of its parameters, including the nonce count, client nonce, and QoP value if they are specified. """ self.test_MD5HashResponseExtra('sha', sha1) def formatResponse(self, quotes=True, **kw): """ Format all given keyword arguments and their values suitably for use as the value of an HTTP header. @types quotes: C{bool} @param quotes: A flag indicating whether to quote the values of each field in the response. @param **kw: Keywords and C{str} values which will be treated as field name/value pairs to include in the result. @rtype: C{str} @return: The given fields formatted for use as an HTTP header value. """ if 'username' not in kw: kw['username'] = self.username if 'realm' not in kw: kw['realm'] = self.realm if 'algorithm' not in kw: kw['algorithm'] = self.algorithm if 'qop' not in kw: kw['qop'] = self.qop if 'cnonce' not in kw: kw['cnonce'] = self.cnonce if 'uri' not in kw: kw['uri'] = self.uri if quotes: quote = '"' else: quote = '' return ', '.join([ '%s=%s%s%s' % (k, quote, v, quote) for (k, v) in kw.iteritems() if v is not None]) def getDigestResponse(self, challenge, ncount): """ Calculate the response for the given challenge """ nonce = challenge.get('nonce') algo = challenge.get('algorithm').lower() qop = challenge.get('qop') ha1 = calcHA1( algo, self.username, self.realm, self.password, nonce, self.cnonce) ha2 = calcHA2(algo, "GET", self.uri, qop, None) expected = calcResponse(ha1, ha2, algo, nonce, ncount, self.cnonce, qop) return expected def test_response(self, quotes=True): """ L{DigestCredentialFactory.decode} accepts a digest challenge response and parses it into an L{IUsernameHashedPassword} provider. """ challenge = self.credentialFactory.getChallenge(self.clientAddress.host) nc = "00000001" clientResponse = self.formatResponse( quotes=quotes, nonce=challenge['nonce'], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge['opaque']) creds = self.credentialFactory.decode( clientResponse, self.method, self.clientAddress.host) self.assertTrue(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + 'wrong')) def test_responseWithoutQuotes(self): """ L{DigestCredentialFactory.decode} accepts a digest challenge response which does not quote the values of its fields and parses it into an L{IUsernameHashedPassword} provider in the same way it would a response which included quoted field values. """ self.test_response(False) def test_responseWithCommaURI(self): """ L{DigestCredentialFactory.decode} accepts a digest challenge response which quotes the values of its fields and includes a C{b","} in the URI field. """ self.uri = b"/some,path/" self.test_response(True) def test_caseInsensitiveAlgorithm(self): """ The case of the algorithm value in the response is ignored when checking the credentials. """ self.algorithm = 'MD5' self.test_response() def test_md5DefaultAlgorithm(self): """ The algorithm defaults to MD5 if it is not supplied in the response. """ self.algorithm = None self.test_response() def test_responseWithoutClientIP(self): """ L{DigestCredentialFactory.decode} accepts a digest challenge response even if the client address it is passed is C{None}. """ challenge = self.credentialFactory.getChallenge(None) nc = "00000001" clientResponse = self.formatResponse( nonce=challenge['nonce'], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge['opaque']) creds = self.credentialFactory.decode(clientResponse, self.method, None) self.assertTrue(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + 'wrong')) def test_multiResponse(self): """ L{DigestCredentialFactory.decode} handles multiple responses to a single challenge. """ challenge = self.credentialFactory.getChallenge(self.clientAddress.host) nc = "00000001" clientResponse = self.formatResponse( nonce=challenge['nonce'], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge['opaque']) creds = self.credentialFactory.decode(clientResponse, self.method, self.clientAddress.host) self.assertTrue(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + 'wrong')) nc = "00000002" clientResponse = self.formatResponse( nonce=challenge['nonce'], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge['opaque']) creds = self.credentialFactory.decode(clientResponse, self.method, self.clientAddress.host) self.assertTrue(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + 'wrong')) def test_failsWithDifferentMethod(self): """ L{DigestCredentialFactory.decode} returns an L{IUsernameHashedPassword} provider which rejects a correct password for the given user if the challenge response request is made using a different HTTP method than was used to request the initial challenge. """ challenge = self.credentialFactory.getChallenge(self.clientAddress.host) nc = "00000001" clientResponse = self.formatResponse( nonce=challenge['nonce'], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge['opaque']) creds = self.credentialFactory.decode(clientResponse, 'POST', self.clientAddress.host) self.assertFalse(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + 'wrong')) def test_noUsername(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response has no username field or if the username field is empty. """ # Check for no username e = self.assertRaises( LoginFailed, self.credentialFactory.decode, self.formatResponse(username=None), self.method, self.clientAddress.host) self.assertEqual(str(e), "Invalid response, no username given.") # Check for an empty username e = self.assertRaises( LoginFailed, self.credentialFactory.decode, self.formatResponse(username=""), self.method, self.clientAddress.host) self.assertEqual(str(e), "Invalid response, no username given.") def test_noNonce(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response has no nonce. """ e = self.assertRaises( LoginFailed, self.credentialFactory.decode, self.formatResponse(opaque="abc123"), self.method, self.clientAddress.host) self.assertEqual(str(e), "Invalid response, no nonce given.") def test_noOpaque(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response has no opaque. """ e = self.assertRaises( LoginFailed, self.credentialFactory.decode, self.formatResponse(), self.method, self.clientAddress.host) self.assertEqual(str(e), "Invalid response, no opaque given.") def test_checkHash(self): """ L{DigestCredentialFactory.decode} returns an L{IUsernameDigestHash} provider which can verify a hash of the form 'username:realm:password'. """ challenge = self.credentialFactory.getChallenge(self.clientAddress.host) nc = "00000001" clientResponse = self.formatResponse( nonce=challenge['nonce'], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge['opaque']) creds = self.credentialFactory.decode(clientResponse, self.method, self.clientAddress.host) self.assertTrue(verifyObject(IUsernameDigestHash, creds)) cleartext = '%s:%s:%s' % (self.username, self.realm, self.password) hash = md5(cleartext) self.assertTrue(creds.checkHash(hash.hexdigest())) hash.update('wrong') self.assertFalse(creds.checkHash(hash.hexdigest())) def test_invalidOpaque(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the opaque value does not contain all the required parts. """ credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, 'badOpaque', challenge['nonce'], self.clientAddress.host) self.assertEqual(str(exc), 'Invalid response, invalid opaque value') badOpaque = 'foo-' + b64encode('nonce,clientip') exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badOpaque, challenge['nonce'], self.clientAddress.host) self.assertEqual(str(exc), 'Invalid response, invalid opaque value') exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, '', challenge['nonce'], self.clientAddress.host) self.assertEqual(str(exc), 'Invalid response, invalid opaque value') badOpaque = ( 'foo-' + b64encode('%s,%s,foobar' % ( challenge['nonce'], self.clientAddress.host))) exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badOpaque, challenge['nonce'], self.clientAddress.host) self.assertEqual( str(exc), 'Invalid response, invalid opaque/time values') def test_incompatibleNonce(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given nonce from the response does not match the nonce encoded in the opaque. """ credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) badNonceOpaque = credentialFactory._generateOpaque( '1234567890', self.clientAddress.host) exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badNonceOpaque, challenge['nonce'], self.clientAddress.host) self.assertEqual( str(exc), 'Invalid response, incompatible opaque/nonce values') exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badNonceOpaque, '', self.clientAddress.host) self.assertEqual( str(exc), 'Invalid response, incompatible opaque/nonce values') def test_incompatibleClientIP(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the request comes from a client IP other than what is encoded in the opaque. """ credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) badAddress = '10.0.0.1' # Sanity check self.assertNotEqual(self.clientAddress.host, badAddress) badNonceOpaque = credentialFactory._generateOpaque( challenge['nonce'], badAddress) self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badNonceOpaque, challenge['nonce'], self.clientAddress.host) def test_oldNonce(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given opaque is older than C{DigestCredentialFactory.CHALLENGE_LIFETIME_SECS} """ credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) key = '%s,%s,%s' % (challenge['nonce'], self.clientAddress.host, '-137876876') digest = md5(key + credentialFactory.privateKey).hexdigest() ekey = b64encode(key) oldNonceOpaque = '%s-%s' % (digest, ekey.strip('\n')) self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, oldNonceOpaque, challenge['nonce'], self.clientAddress.host) def test_mismatchedOpaqueChecksum(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the opaque checksum fails verification. """ credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) key = '%s,%s,%s' % (challenge['nonce'], self.clientAddress.host, '0') digest = md5(key + 'this is not the right pkey').hexdigest() badChecksum = '%s-%s' % (digest, b64encode(key)) self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badChecksum, challenge['nonce'], self.clientAddress.host) def test_incompatibleCalcHA1Options(self): """ L{calcHA1} raises L{TypeError} when any of the pszUsername, pszRealm, or pszPassword arguments are specified with the preHA1 keyword argument. """ arguments = ( ("user", "realm", "password", "preHA1"), (None, "realm", None, "preHA1"), (None, None, "password", "preHA1"), ) for pszUsername, pszRealm, pszPassword, preHA1 in arguments: self.assertRaises( TypeError, calcHA1, "md5", pszUsername, pszRealm, pszPassword, "nonce", "cnonce", preHA1=preHA1) def test_noNewlineOpaque(self): """ L{DigestCredentialFactory._generateOpaque} returns a value without newlines, regardless of the length of the nonce. """ opaque = self.credentialFactory._generateOpaque( "long nonce " * 10, None) self.assertNotIn('\n', opaque)
class DigestAuthTests(TestCase): """ L{TestCase} mixin class which defines a number of tests for L{DigestCredentialFactory}. Because this mixin defines C{setUp}, it must be inherited before L{TestCase}. """ def setUp(self): """ Create a DigestCredentialFactory for testing """ self.username = "******" self.password = "******" self.realm = "test realm" self.algorithm = "md5" self.cnonce = "29fc54aa1641c6fa0e151419361c8f23" self.qop = "auth" self.uri = "/write/" self.clientAddress = IPv4Address('TCP', '10.2.3.4', 43125) self.method = 'GET' self.credentialFactory = DigestCredentialFactory( self.algorithm, self.realm) def test_MD5HashA1(self, _algorithm='md5', _hash=md5): """ L{calcHA1} accepts the C{'md5'} algorithm and returns an MD5 hash of its parameters, excluding the nonce and cnonce. """ nonce = 'abc123xyz' hashA1 = calcHA1(_algorithm, self.username, self.realm, self.password, nonce, self.cnonce) a1 = '%s:%s:%s' % (self.username, self.realm, self.password) expected = _hash(a1).hexdigest() self.assertEqual(hashA1, expected) def test_MD5SessionHashA1(self): """ L{calcHA1} accepts the C{'md5-sess'} algorithm and returns an MD5 hash of its parameters, including the nonce and cnonce. """ nonce = 'xyz321abc' hashA1 = calcHA1('md5-sess', self.username, self.realm, self.password, nonce, self.cnonce) a1 = '%s:%s:%s' % (self.username, self.realm, self.password) ha1 = md5(a1).digest() a1 = '%s:%s:%s' % (ha1, nonce, self.cnonce) expected = md5(a1).hexdigest() self.assertEqual(hashA1, expected) def test_SHAHashA1(self): """ L{calcHA1} accepts the C{'sha'} algorithm and returns a SHA hash of its parameters, excluding the nonce and cnonce. """ self.test_MD5HashA1('sha', sha1) def test_MD5HashA2Auth(self, _algorithm='md5', _hash=md5): """ L{calcHA2} accepts the C{'md5'} algorithm and returns an MD5 hash of its arguments, excluding the entity hash for QOP other than C{'auth-int'}. """ method = 'GET' hashA2 = calcHA2(_algorithm, method, self.uri, 'auth', None) a2 = '%s:%s' % (method, self.uri) expected = _hash(a2).hexdigest() self.assertEqual(hashA2, expected) def test_MD5HashA2AuthInt(self, _algorithm='md5', _hash=md5): """ L{calcHA2} accepts the C{'md5'} algorithm and returns an MD5 hash of its arguments, including the entity hash for QOP of C{'auth-int'}. """ method = 'GET' hentity = 'foobarbaz' hashA2 = calcHA2(_algorithm, method, self.uri, 'auth-int', hentity) a2 = '%s:%s:%s' % (method, self.uri, hentity) expected = _hash(a2).hexdigest() self.assertEqual(hashA2, expected) def test_MD5SessHashA2Auth(self): """ L{calcHA2} accepts the C{'md5-sess'} algorithm and QOP of C{'auth'} and returns the same value as it does for the C{'md5'} algorithm. """ self.test_MD5HashA2Auth('md5-sess') def test_MD5SessHashA2AuthInt(self): """ L{calcHA2} accepts the C{'md5-sess'} algorithm and QOP of C{'auth-int'} and returns the same value as it does for the C{'md5'} algorithm. """ self.test_MD5HashA2AuthInt('md5-sess') def test_SHAHashA2Auth(self): """ L{calcHA2} accepts the C{'sha'} algorithm and returns a SHA hash of its arguments, excluding the entity hash for QOP other than C{'auth-int'}. """ self.test_MD5HashA2Auth('sha', sha1) def test_SHAHashA2AuthInt(self): """ L{calcHA2} accepts the C{'sha'} algorithm and returns a SHA hash of its arguments, including the entity hash for QOP of C{'auth-int'}. """ self.test_MD5HashA2AuthInt('sha', sha1) def test_MD5HashResponse(self, _algorithm='md5', _hash=md5): """ L{calcResponse} accepts the C{'md5'} algorithm and returns an MD5 hash of its parameters, excluding the nonce count, client nonce, and QoP value if the nonce count and client nonce are C{None} """ hashA1 = 'abc123' hashA2 = '789xyz' nonce = 'lmnopq' response = '%s:%s:%s' % (hashA1, nonce, hashA2) expected = _hash(response).hexdigest() digest = calcResponse(hashA1, hashA2, _algorithm, nonce, None, None, None) self.assertEqual(expected, digest) def test_MD5SessionHashResponse(self): """ L{calcResponse} accepts the C{'md5-sess'} algorithm and returns an MD5 hash of its parameters, excluding the nonce count, client nonce, and QoP value if the nonce count and client nonce are C{None} """ self.test_MD5HashResponse('md5-sess') def test_SHAHashResponse(self): """ L{calcResponse} accepts the C{'sha'} algorithm and returns a SHA hash of its parameters, excluding the nonce count, client nonce, and QoP value if the nonce count and client nonce are C{None} """ self.test_MD5HashResponse('sha', sha1) def test_MD5HashResponseExtra(self, _algorithm='md5', _hash=md5): """ L{calcResponse} accepts the C{'md5'} algorithm and returns an MD5 hash of its parameters, including the nonce count, client nonce, and QoP value if they are specified. """ hashA1 = 'abc123' hashA2 = '789xyz' nonce = 'lmnopq' nonceCount = '00000004' clientNonce = 'abcxyz123' qop = 'auth' response = '%s:%s:%s:%s:%s:%s' % ( hashA1, nonce, nonceCount, clientNonce, qop, hashA2) expected = _hash(response).hexdigest() digest = calcResponse( hashA1, hashA2, _algorithm, nonce, nonceCount, clientNonce, qop) self.assertEqual(expected, digest) def test_MD5SessionHashResponseExtra(self): """ L{calcResponse} accepts the C{'md5-sess'} algorithm and returns an MD5 hash of its parameters, including the nonce count, client nonce, and QoP value if they are specified. """ self.test_MD5HashResponseExtra('md5-sess') def test_SHAHashResponseExtra(self): """ L{calcResponse} accepts the C{'sha'} algorithm and returns a SHA hash of its parameters, including the nonce count, client nonce, and QoP value if they are specified. """ self.test_MD5HashResponseExtra('sha', sha1) def formatResponse(self, quotes=True, **kw): """ Format all given keyword arguments and their values suitably for use as the value of an HTTP header. @types quotes: C{bool} @param quotes: A flag indicating whether to quote the values of each field in the response. @param **kw: Keywords and C{str} values which will be treated as field name/value pairs to include in the result. @rtype: C{str} @return: The given fields formatted for use as an HTTP header value. """ if 'username' not in kw: kw['username'] = self.username if 'realm' not in kw: kw['realm'] = self.realm if 'algorithm' not in kw: kw['algorithm'] = self.algorithm if 'qop' not in kw: kw['qop'] = self.qop if 'cnonce' not in kw: kw['cnonce'] = self.cnonce if 'uri' not in kw: kw['uri'] = self.uri if quotes: quote = '"' else: quote = '' return ', '.join([ '%s=%s%s%s' % (k, quote, v, quote) for (k, v) in kw.iteritems() if v is not None]) def getDigestResponse(self, challenge, ncount): """ Calculate the response for the given challenge """ nonce = challenge.get('nonce') algo = challenge.get('algorithm').lower() qop = challenge.get('qop') ha1 = calcHA1( algo, self.username, self.realm, self.password, nonce, self.cnonce) ha2 = calcHA2(algo, "GET", self.uri, qop, None) expected = calcResponse(ha1, ha2, algo, nonce, ncount, self.cnonce, qop) return expected def test_response(self, quotes=True): """ L{DigestCredentialFactory.decode} accepts a digest challenge response and parses it into an L{IUsernameHashedPassword} provider. """ challenge = self.credentialFactory.getChallenge(self.clientAddress.host) nc = "00000001" clientResponse = self.formatResponse( quotes=quotes, nonce=challenge['nonce'], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge['opaque']) creds = self.credentialFactory.decode( clientResponse, self.method, self.clientAddress.host) self.assertTrue(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + 'wrong')) def test_responseWithoutQuotes(self): """ L{DigestCredentialFactory.decode} accepts a digest challenge response which does not quote the values of its fields and parses it into an L{IUsernameHashedPassword} provider in the same way it would a response which included quoted field values. """ self.test_response(False) def test_caseInsensitiveAlgorithm(self): """ The case of the algorithm value in the response is ignored when checking the credentials. """ self.algorithm = 'MD5' self.test_response() def test_md5DefaultAlgorithm(self): """ The algorithm defaults to MD5 if it is not supplied in the response. """ self.algorithm = None self.test_response() def test_responseWithoutClientIP(self): """ L{DigestCredentialFactory.decode} accepts a digest challenge response even if the client address it is passed is C{None}. """ challenge = self.credentialFactory.getChallenge(None) nc = "00000001" clientResponse = self.formatResponse( nonce=challenge['nonce'], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge['opaque']) creds = self.credentialFactory.decode(clientResponse, self.method, None) self.assertTrue(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + 'wrong')) def test_multiResponse(self): """ L{DigestCredentialFactory.decode} handles multiple responses to a single challenge. """ challenge = self.credentialFactory.getChallenge(self.clientAddress.host) nc = "00000001" clientResponse = self.formatResponse( nonce=challenge['nonce'], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge['opaque']) creds = self.credentialFactory.decode(clientResponse, self.method, self.clientAddress.host) self.assertTrue(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + 'wrong')) nc = "00000002" clientResponse = self.formatResponse( nonce=challenge['nonce'], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge['opaque']) creds = self.credentialFactory.decode(clientResponse, self.method, self.clientAddress.host) self.assertTrue(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + 'wrong')) def test_failsWithDifferentMethod(self): """ L{DigestCredentialFactory.decode} returns an L{IUsernameHashedPassword} provider which rejects a correct password for the given user if the challenge response request is made using a different HTTP method than was used to request the initial challenge. """ challenge = self.credentialFactory.getChallenge(self.clientAddress.host) nc = "00000001" clientResponse = self.formatResponse( nonce=challenge['nonce'], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge['opaque']) creds = self.credentialFactory.decode(clientResponse, 'POST', self.clientAddress.host) self.assertFalse(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + 'wrong')) def test_noUsername(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response has no username field or if the username field is empty. """ # Check for no username e = self.assertRaises( LoginFailed, self.credentialFactory.decode, self.formatResponse(username=None), self.method, self.clientAddress.host) self.assertEqual(str(e), "Invalid response, no username given.") # Check for an empty username e = self.assertRaises( LoginFailed, self.credentialFactory.decode, self.formatResponse(username=""), self.method, self.clientAddress.host) self.assertEqual(str(e), "Invalid response, no username given.") def test_noNonce(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response has no nonce. """ e = self.assertRaises( LoginFailed, self.credentialFactory.decode, self.formatResponse(opaque="abc123"), self.method, self.clientAddress.host) self.assertEqual(str(e), "Invalid response, no nonce given.") def test_noOpaque(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response has no opaque. """ e = self.assertRaises( LoginFailed, self.credentialFactory.decode, self.formatResponse(), self.method, self.clientAddress.host) self.assertEqual(str(e), "Invalid response, no opaque given.") def test_checkHash(self): """ L{DigestCredentialFactory.decode} returns an L{IUsernameDigestHash} provider which can verify a hash of the form 'username:realm:password'. """ challenge = self.credentialFactory.getChallenge(self.clientAddress.host) nc = "00000001" clientResponse = self.formatResponse( nonce=challenge['nonce'], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge['opaque']) creds = self.credentialFactory.decode(clientResponse, self.method, self.clientAddress.host) self.assertTrue(verifyObject(IUsernameDigestHash, creds)) cleartext = '%s:%s:%s' % (self.username, self.realm, self.password) hash = md5(cleartext) self.assertTrue(creds.checkHash(hash.hexdigest())) hash.update('wrong') self.assertFalse(creds.checkHash(hash.hexdigest())) def test_invalidOpaque(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the opaque value does not contain all the required parts. """ credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, 'badOpaque', challenge['nonce'], self.clientAddress.host) self.assertEqual(str(exc), 'Invalid response, invalid opaque value') badOpaque = 'foo-' + b64encode('nonce,clientip') exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badOpaque, challenge['nonce'], self.clientAddress.host) self.assertEqual(str(exc), 'Invalid response, invalid opaque value') exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, '', challenge['nonce'], self.clientAddress.host) self.assertEqual(str(exc), 'Invalid response, invalid opaque value') badOpaque = ( 'foo-' + b64encode('%s,%s,foobar' % ( challenge['nonce'], self.clientAddress.host))) exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badOpaque, challenge['nonce'], self.clientAddress.host) self.assertEqual( str(exc), 'Invalid response, invalid opaque/time values') def test_incompatibleNonce(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given nonce from the response does not match the nonce encoded in the opaque. """ credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) badNonceOpaque = credentialFactory._generateOpaque( '1234567890', self.clientAddress.host) exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badNonceOpaque, challenge['nonce'], self.clientAddress.host) self.assertEqual( str(exc), 'Invalid response, incompatible opaque/nonce values') exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badNonceOpaque, '', self.clientAddress.host) self.assertEqual( str(exc), 'Invalid response, incompatible opaque/nonce values') def test_incompatibleClientIP(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the request comes from a client IP other than what is encoded in the opaque. """ credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) badAddress = '10.0.0.1' # Sanity check self.assertNotEqual(self.clientAddress.host, badAddress) badNonceOpaque = credentialFactory._generateOpaque( challenge['nonce'], badAddress) self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badNonceOpaque, challenge['nonce'], self.clientAddress.host) def test_oldNonce(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given opaque is older than C{DigestCredentialFactory.CHALLENGE_LIFETIME_SECS} """ credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) key = '%s,%s,%s' % (challenge['nonce'], self.clientAddress.host, '-137876876') digest = md5(key + credentialFactory.privateKey).hexdigest() ekey = b64encode(key) oldNonceOpaque = '%s-%s' % (digest, ekey.strip('\n')) self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, oldNonceOpaque, challenge['nonce'], self.clientAddress.host) def test_mismatchedOpaqueChecksum(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the opaque checksum fails verification. """ credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) key = '%s,%s,%s' % (challenge['nonce'], self.clientAddress.host, '0') digest = md5(key + 'this is not the right pkey').hexdigest() badChecksum = '%s-%s' % (digest, b64encode(key)) self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badChecksum, challenge['nonce'], self.clientAddress.host) def test_incompatibleCalcHA1Options(self): """ L{calcHA1} raises L{TypeError} when any of the pszUsername, pszRealm, or pszPassword arguments are specified with the preHA1 keyword argument. """ arguments = ( ("user", "realm", "password", "preHA1"), (None, "realm", None, "preHA1"), (None, None, "password", "preHA1"), ) for pszUsername, pszRealm, pszPassword, preHA1 in arguments: self.assertRaises( TypeError, calcHA1, "md5", pszUsername, pszRealm, pszPassword, "nonce", "cnonce", preHA1=preHA1) def test_noNewlineOpaque(self): """ L{DigestCredentialFactory._generateOpaque} returns a value without newlines, regardless of the length of the nonce. """ opaque = self.credentialFactory._generateOpaque( "long nonce " * 10, None) self.assertNotIn('\n', opaque)
class DigestAuthTests(TestCase): """ L{TestCase} mixin class which defines a number of tests for L{DigestCredentialFactory}. Because this mixin defines C{setUp}, it must be inherited before L{TestCase}. """ def setUp(self): """ Create a DigestCredentialFactory for testing """ self.username = b"foobar" self.password = b"bazquux" self.realm = b"test realm" self.algorithm = b"md5" self.cnonce = b"29fc54aa1641c6fa0e151419361c8f23" self.qop = b"auth" self.uri = b"/write/" self.clientAddress = IPv4Address("TCP", "10.2.3.4", 43125) self.method = b"GET" self.credentialFactory = DigestCredentialFactory( self.algorithm, self.realm) def test_MD5HashA1(self, _algorithm=b"md5", _hash=md5): """ L{calcHA1} accepts the C{'md5'} algorithm and returns an MD5 hash of its parameters, excluding the nonce and cnonce. """ nonce = b"abc123xyz" hashA1 = calcHA1(_algorithm, self.username, self.realm, self.password, nonce, self.cnonce) a1 = b":".join((self.username, self.realm, self.password)) expected = hexlify(_hash(a1).digest()) self.assertEqual(hashA1, expected) def test_MD5SessionHashA1(self): """ L{calcHA1} accepts the C{'md5-sess'} algorithm and returns an MD5 hash of its parameters, including the nonce and cnonce. """ nonce = b"xyz321abc" hashA1 = calcHA1(b"md5-sess", self.username, self.realm, self.password, nonce, self.cnonce) a1 = self.username + b":" + self.realm + b":" + self.password ha1 = hexlify(md5(a1).digest()) a1 = ha1 + b":" + nonce + b":" + self.cnonce expected = hexlify(md5(a1).digest()) self.assertEqual(hashA1, expected) def test_SHAHashA1(self): """ L{calcHA1} accepts the C{'sha'} algorithm and returns a SHA hash of its parameters, excluding the nonce and cnonce. """ self.test_MD5HashA1(b"sha", sha1) def test_MD5HashA2Auth(self, _algorithm=b"md5", _hash=md5): """ L{calcHA2} accepts the C{'md5'} algorithm and returns an MD5 hash of its arguments, excluding the entity hash for QOP other than C{'auth-int'}. """ method = b"GET" hashA2 = calcHA2(_algorithm, method, self.uri, b"auth", None) a2 = method + b":" + self.uri expected = hexlify(_hash(a2).digest()) self.assertEqual(hashA2, expected) def test_MD5HashA2AuthInt(self, _algorithm=b"md5", _hash=md5): """ L{calcHA2} accepts the C{'md5'} algorithm and returns an MD5 hash of its arguments, including the entity hash for QOP of C{'auth-int'}. """ method = b"GET" hentity = b"foobarbaz" hashA2 = calcHA2(_algorithm, method, self.uri, b"auth-int", hentity) a2 = method + b":" + self.uri + b":" + hentity expected = hexlify(_hash(a2).digest()) self.assertEqual(hashA2, expected) def test_MD5SessHashA2Auth(self): """ L{calcHA2} accepts the C{'md5-sess'} algorithm and QOP of C{'auth'} and returns the same value as it does for the C{'md5'} algorithm. """ self.test_MD5HashA2Auth(b"md5-sess") def test_MD5SessHashA2AuthInt(self): """ L{calcHA2} accepts the C{'md5-sess'} algorithm and QOP of C{'auth-int'} and returns the same value as it does for the C{'md5'} algorithm. """ self.test_MD5HashA2AuthInt(b"md5-sess") def test_SHAHashA2Auth(self): """ L{calcHA2} accepts the C{'sha'} algorithm and returns a SHA hash of its arguments, excluding the entity hash for QOP other than C{'auth-int'}. """ self.test_MD5HashA2Auth(b"sha", sha1) def test_SHAHashA2AuthInt(self): """ L{calcHA2} accepts the C{'sha'} algorithm and returns a SHA hash of its arguments, including the entity hash for QOP of C{'auth-int'}. """ self.test_MD5HashA2AuthInt(b"sha", sha1) def test_MD5HashResponse(self, _algorithm=b"md5", _hash=md5): """ L{calcResponse} accepts the C{'md5'} algorithm and returns an MD5 hash of its parameters, excluding the nonce count, client nonce, and QoP value if the nonce count and client nonce are L{None} """ hashA1 = b"abc123" hashA2 = b"789xyz" nonce = b"lmnopq" response = hashA1 + b":" + nonce + b":" + hashA2 expected = hexlify(_hash(response).digest()) digest = calcResponse(hashA1, hashA2, _algorithm, nonce, None, None, None) self.assertEqual(expected, digest) def test_MD5SessionHashResponse(self): """ L{calcResponse} accepts the C{'md5-sess'} algorithm and returns an MD5 hash of its parameters, excluding the nonce count, client nonce, and QoP value if the nonce count and client nonce are L{None} """ self.test_MD5HashResponse(b"md5-sess") def test_SHAHashResponse(self): """ L{calcResponse} accepts the C{'sha'} algorithm and returns a SHA hash of its parameters, excluding the nonce count, client nonce, and QoP value if the nonce count and client nonce are L{None} """ self.test_MD5HashResponse(b"sha", sha1) def test_MD5HashResponseExtra(self, _algorithm=b"md5", _hash=md5): """ L{calcResponse} accepts the C{'md5'} algorithm and returns an MD5 hash of its parameters, including the nonce count, client nonce, and QoP value if they are specified. """ hashA1 = b"abc123" hashA2 = b"789xyz" nonce = b"lmnopq" nonceCount = b"00000004" clientNonce = b"abcxyz123" qop = b"auth" response = (hashA1 + b":" + nonce + b":" + nonceCount + b":" + clientNonce + b":" + qop + b":" + hashA2) expected = hexlify(_hash(response).digest()) digest = calcResponse(hashA1, hashA2, _algorithm, nonce, nonceCount, clientNonce, qop) self.assertEqual(expected, digest) def test_MD5SessionHashResponseExtra(self): """ L{calcResponse} accepts the C{'md5-sess'} algorithm and returns an MD5 hash of its parameters, including the nonce count, client nonce, and QoP value if they are specified. """ self.test_MD5HashResponseExtra(b"md5-sess") def test_SHAHashResponseExtra(self): """ L{calcResponse} accepts the C{'sha'} algorithm and returns a SHA hash of its parameters, including the nonce count, client nonce, and QoP value if they are specified. """ self.test_MD5HashResponseExtra(b"sha", sha1) def formatResponse(self, quotes=True, **kw): """ Format all given keyword arguments and their values suitably for use as the value of an HTTP header. @types quotes: C{bool} @param quotes: A flag indicating whether to quote the values of each field in the response. @param **kw: Keywords and C{bytes} values which will be treated as field name/value pairs to include in the result. @rtype: C{bytes} @return: The given fields formatted for use as an HTTP header value. """ if "username" not in kw: kw["username"] = self.username if "realm" not in kw: kw["realm"] = self.realm if "algorithm" not in kw: kw["algorithm"] = self.algorithm if "qop" not in kw: kw["qop"] = self.qop if "cnonce" not in kw: kw["cnonce"] = self.cnonce if "uri" not in kw: kw["uri"] = self.uri if quotes: quote = b'"' else: quote = b"" return b", ".join([ b"".join((networkString(k), b"=", quote, v, quote)) for (k, v) in kw.items() if v is not None ]) def getDigestResponse(self, challenge, ncount): """ Calculate the response for the given challenge """ nonce = challenge.get("nonce") algo = challenge.get("algorithm").lower() qop = challenge.get("qop") ha1 = calcHA1(algo, self.username, self.realm, self.password, nonce, self.cnonce) ha2 = calcHA2(algo, b"GET", self.uri, qop, None) expected = calcResponse(ha1, ha2, algo, nonce, ncount, self.cnonce, qop) return expected def test_response(self, quotes=True): """ L{DigestCredentialFactory.decode} accepts a digest challenge response and parses it into an L{IUsernameHashedPassword} provider. """ challenge = self.credentialFactory.getChallenge( self.clientAddress.host) nc = b"00000001" clientResponse = self.formatResponse( quotes=quotes, nonce=challenge["nonce"], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge["opaque"], ) creds = self.credentialFactory.decode(clientResponse, self.method, self.clientAddress.host) self.assertTrue(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + b"wrong")) def test_responseWithoutQuotes(self): """ L{DigestCredentialFactory.decode} accepts a digest challenge response which does not quote the values of its fields and parses it into an L{IUsernameHashedPassword} provider in the same way it would a response which included quoted field values. """ self.test_response(False) def test_responseWithCommaURI(self): """ L{DigestCredentialFactory.decode} accepts a digest challenge response which quotes the values of its fields and includes a C{b","} in the URI field. """ self.uri = b"/some,path/" self.test_response(True) def test_caseInsensitiveAlgorithm(self): """ The case of the algorithm value in the response is ignored when checking the credentials. """ self.algorithm = b"MD5" self.test_response() def test_md5DefaultAlgorithm(self): """ The algorithm defaults to MD5 if it is not supplied in the response. """ self.algorithm = None self.test_response() def test_responseWithoutClientIP(self): """ L{DigestCredentialFactory.decode} accepts a digest challenge response even if the client address it is passed is L{None}. """ challenge = self.credentialFactory.getChallenge(None) nc = b"00000001" clientResponse = self.formatResponse( nonce=challenge["nonce"], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge["opaque"], ) creds = self.credentialFactory.decode(clientResponse, self.method, None) self.assertTrue(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + b"wrong")) def test_multiResponse(self): """ L{DigestCredentialFactory.decode} handles multiple responses to a single challenge. """ challenge = self.credentialFactory.getChallenge( self.clientAddress.host) nc = b"00000001" clientResponse = self.formatResponse( nonce=challenge["nonce"], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge["opaque"], ) creds = self.credentialFactory.decode(clientResponse, self.method, self.clientAddress.host) self.assertTrue(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + b"wrong")) nc = b"00000002" clientResponse = self.formatResponse( nonce=challenge["nonce"], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge["opaque"], ) creds = self.credentialFactory.decode(clientResponse, self.method, self.clientAddress.host) self.assertTrue(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + b"wrong")) def test_failsWithDifferentMethod(self): """ L{DigestCredentialFactory.decode} returns an L{IUsernameHashedPassword} provider which rejects a correct password for the given user if the challenge response request is made using a different HTTP method than was used to request the initial challenge. """ challenge = self.credentialFactory.getChallenge( self.clientAddress.host) nc = b"00000001" clientResponse = self.formatResponse( nonce=challenge["nonce"], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge["opaque"], ) creds = self.credentialFactory.decode(clientResponse, b"POST", self.clientAddress.host) self.assertFalse(creds.checkPassword(self.password)) self.assertFalse(creds.checkPassword(self.password + b"wrong")) def test_noUsername(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response has no username field or if the username field is empty. """ # Check for no username e = self.assertRaises( LoginFailed, self.credentialFactory.decode, self.formatResponse(username=None), self.method, self.clientAddress.host, ) self.assertEqual(str(e), "Invalid response, no username given.") # Check for an empty username e = self.assertRaises( LoginFailed, self.credentialFactory.decode, self.formatResponse(username=b""), self.method, self.clientAddress.host, ) self.assertEqual(str(e), "Invalid response, no username given.") def test_noNonce(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response has no nonce. """ e = self.assertRaises( LoginFailed, self.credentialFactory.decode, self.formatResponse(opaque=b"abc123"), self.method, self.clientAddress.host, ) self.assertEqual(str(e), "Invalid response, no nonce given.") def test_noOpaque(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response has no opaque. """ e = self.assertRaises( LoginFailed, self.credentialFactory.decode, self.formatResponse(), self.method, self.clientAddress.host, ) self.assertEqual(str(e), "Invalid response, no opaque given.") def test_checkHash(self): """ L{DigestCredentialFactory.decode} returns an L{IUsernameDigestHash} provider which can verify a hash of the form 'username:realm:password'. """ challenge = self.credentialFactory.getChallenge( self.clientAddress.host) nc = b"00000001" clientResponse = self.formatResponse( nonce=challenge["nonce"], response=self.getDigestResponse(challenge, nc), nc=nc, opaque=challenge["opaque"], ) creds = self.credentialFactory.decode(clientResponse, self.method, self.clientAddress.host) self.assertTrue(verifyObject(IUsernameDigestHash, creds)) cleartext = self.username + b":" + self.realm + b":" + self.password hash = md5(cleartext) self.assertTrue(creds.checkHash(hexlify(hash.digest()))) hash.update(b"wrong") self.assertFalse(creds.checkHash(hexlify(hash.digest()))) def test_invalidOpaque(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the opaque value does not contain all the required parts. """ credentialFactory = FakeDigestCredentialFactory( self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, b"badOpaque", challenge["nonce"], self.clientAddress.host, ) self.assertEqual(str(exc), "Invalid response, invalid opaque value") badOpaque = b"foo-" + b64encode(b"nonce,clientip") exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badOpaque, challenge["nonce"], self.clientAddress.host, ) self.assertEqual(str(exc), "Invalid response, invalid opaque value") exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, b"", challenge["nonce"], self.clientAddress.host, ) self.assertEqual(str(exc), "Invalid response, invalid opaque value") badOpaque = b"foo-" + b64encode(b",".join( (challenge["nonce"], networkString( self.clientAddress.host), b"foobar"))) exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badOpaque, challenge["nonce"], self.clientAddress.host, ) self.assertEqual(str(exc), "Invalid response, invalid opaque/time values") def test_incompatibleNonce(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given nonce from the response does not match the nonce encoded in the opaque. """ credentialFactory = FakeDigestCredentialFactory( self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) badNonceOpaque = credentialFactory._generateOpaque( b"1234567890", self.clientAddress.host) exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badNonceOpaque, challenge["nonce"], self.clientAddress.host, ) self.assertEqual(str(exc), "Invalid response, incompatible opaque/nonce values") exc = self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badNonceOpaque, b"", self.clientAddress.host, ) self.assertEqual(str(exc), "Invalid response, incompatible opaque/nonce values") def test_incompatibleClientIP(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the request comes from a client IP other than what is encoded in the opaque. """ credentialFactory = FakeDigestCredentialFactory( self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) badAddress = "10.0.0.1" # Sanity check self.assertNotEqual(self.clientAddress.host, badAddress) badNonceOpaque = credentialFactory._generateOpaque( challenge["nonce"], badAddress) self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badNonceOpaque, challenge["nonce"], self.clientAddress.host, ) def test_oldNonce(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given opaque is older than C{DigestCredentialFactory.CHALLENGE_LIFETIME_SECS} """ credentialFactory = FakeDigestCredentialFactory( self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) key = b",".join( (challenge["nonce"], networkString(self.clientAddress.host), b"-137876876")) digest = hexlify(md5(key + credentialFactory.privateKey).digest()) ekey = b64encode(key) oldNonceOpaque = b"-".join((digest, ekey.strip(b"\n"))) self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, oldNonceOpaque, challenge["nonce"], self.clientAddress.host, ) def test_mismatchedOpaqueChecksum(self): """ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the opaque checksum fails verification. """ credentialFactory = FakeDigestCredentialFactory( self.algorithm, self.realm) challenge = credentialFactory.getChallenge(self.clientAddress.host) key = b",".join( (challenge["nonce"], networkString(self.clientAddress.host), b"0")) digest = hexlify(md5(key + b"this is not the right pkey").digest()) badChecksum = b"-".join((digest, b64encode(key))) self.assertRaises( LoginFailed, credentialFactory._verifyOpaque, badChecksum, challenge["nonce"], self.clientAddress.host, ) def test_incompatibleCalcHA1Options(self): """ L{calcHA1} raises L{TypeError} when any of the pszUsername, pszRealm, or pszPassword arguments are specified with the preHA1 keyword argument. """ arguments = ( (b"user", b"realm", b"password", b"preHA1"), (None, b"realm", None, b"preHA1"), (None, None, b"password", b"preHA1"), ) for pszUsername, pszRealm, pszPassword, preHA1 in arguments: self.assertRaises( TypeError, calcHA1, b"md5", pszUsername, pszRealm, pszPassword, b"nonce", b"cnonce", preHA1=preHA1, ) def test_noNewlineOpaque(self): """ L{DigestCredentialFactory._generateOpaque} returns a value without newlines, regardless of the length of the nonce. """ opaque = self.credentialFactory._generateOpaque( b"long nonce " * 10, None) self.assertNotIn(b"\n", opaque)