Ticket #5495: ticket-5495-digestcredentials-nonce-verification.diff

File ticket-5495-digestcredentials-nonce-verification.diff, 7.5 KB (added by JohnDoeee, 4 years ago)
  • twisted/test/test_digestauth.py

     
    2626    def __init__(self, *args, **kwargs):
    2727        super(FakeDigestCredentialFactory, self).__init__(*args, **kwargs)
    2828        self.privateKey = "0"
     29        self.fakeTime = 0
    2930
    30 
    31     def _generateNonce(self):
    32         """
    33         Generate a static nonce
    34         """
    35         return '178288758716122392881254770685'
    36 
    37 
    3831    def _getTime(self):
    3932        """
    4033        Return a stable time
    4134        """
    42         return 0
     35        return self.fakeTime
    4336
    4437
    4538
     
    355348        self.assertFalse(creds.checkPassword(self.password + 'wrong'))
    356349
    357350
    358     def test_multiResponse(self):
    359         """
    360         L{DigestCredentialFactory.decode} handles multiple responses to a
    361         single challenge.
    362         """
    363         challenge = self.credentialFactory.getChallenge(self.clientAddress.host)
    364 
    365         nc = "00000001"
    366         clientResponse = self.formatResponse(
    367             nonce=challenge['nonce'],
    368             response=self.getDigestResponse(challenge, nc),
    369             nc=nc,
    370             opaque=challenge['opaque'])
    371 
    372         creds = self.credentialFactory.decode(clientResponse, self.method,
    373                                               self.clientAddress.host)
    374         self.assertTrue(creds.checkPassword(self.password))
    375         self.assertFalse(creds.checkPassword(self.password + 'wrong'))
    376 
    377         nc = "00000002"
    378         clientResponse = self.formatResponse(
    379             nonce=challenge['nonce'],
    380             response=self.getDigestResponse(challenge, nc),
    381             nc=nc,
    382             opaque=challenge['opaque'])
    383 
    384         creds = self.credentialFactory.decode(clientResponse, self.method,
    385                                               self.clientAddress.host)
    386         self.assertTrue(creds.checkPassword(self.password))
    387         self.assertFalse(creds.checkPassword(self.password + 'wrong'))
    388 
    389 
    390351    def test_failsWithDifferentMethod(self):
    391352        """
    392353        L{DigestCredentialFactory.decode} returns an L{IUsernameHashedPassword}
     
    669630        opaque = self.credentialFactory._generateOpaque(
    670631            "long nonce " * 10, None)
    671632        self.assertNotIn('\n', opaque)
     633   
     634   
     635    def test_reusedNonce(self):
     636        """
     637        L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given
     638        same nonce twice
     639        """
     640        credentialFactory = FakeDigestCredentialFactory(self.algorithm,
     641                                                        self.realm)
     642        challenge = credentialFactory.getChallenge(self.clientAddress.host)
     643
     644        key = '%s,%s,%s' % (challenge['nonce'],
     645                            self.clientAddress.host,
     646                            '0')
     647        digest = md5(key + credentialFactory.privateKey).hexdigest()
     648        ekey = b64encode(key)
     649
     650        nonceOpaque = '%s-%s' % (digest, ekey.strip('\n'))
     651       
     652        self.assertEqual(credentialFactory._verifyOpaque(
     653                nonceOpaque,
     654                challenge['nonce'],
     655                self.clientAddress.host),
     656            True)
     657
     658        self.assertRaises(
     659            LoginFailed,
     660            credentialFactory._verifyOpaque,
     661            nonceOpaque,
     662            challenge['nonce'],
     663            self.clientAddress.host)
     664   
     665   
     666    def test_nonceTimeoutCleanup(self):
     667        """
     668        L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given
     669        same nonce twice
     670        """
     671        credentialFactory = FakeDigestCredentialFactory(self.algorithm,
     672                                                        self.realm)
     673        challenge = credentialFactory.getChallenge(self.clientAddress.host)
     674
     675        key = '%s,%s,%s' % (challenge['nonce'],
     676                            self.clientAddress.host,
     677                            str(credentialFactory.fakeTime))
     678        digest = md5(key + credentialFactory.privateKey).hexdigest()
     679        ekey = b64encode(key)
     680
     681        nonceOpaque = '%s-%s' % (digest, ekey.strip('\n'))
     682       
     683        self.assertEqual(credentialFactory._verifyOpaque(
     684                nonceOpaque,
     685                challenge['nonce'],
     686                self.clientAddress.host),
     687            True)
     688
     689        credentialFactory.fakeTime = 10000
     690        timeoutChallenge = credentialFactory.getChallenge(self.clientAddress.host)
     691        key = '%s,%s,%s' % (timeoutChallenge['nonce'],
     692                            self.clientAddress.host,
     693                            str(credentialFactory.fakeTime))
     694        digest = md5(key + credentialFactory.privateKey).hexdigest()
     695        ekey = b64encode(key)
     696
     697        nonceOpaque = '%s-%s' % (digest, ekey.strip('\n'))
     698       
     699        self.assertEqual(credentialFactory._verifyOpaque(
     700                nonceOpaque,
     701                timeoutChallenge['nonce'],
     702                self.clientAddress.host),
     703            True)
     704       
     705        # setting the clock back, using the forwarded time earlier as the
     706        # "cleanup mechanism"
     707        credentialFactory.fakeTime = 0
     708        key = '%s,%s,%s' % (challenge['nonce'],
     709                            self.clientAddress.host,
     710                            str(credentialFactory.fakeTime))
     711        digest = md5(key + credentialFactory.privateKey).hexdigest()
     712        ekey = b64encode(key)
     713
     714        nonceOpaque = '%s-%s' % (digest, ekey.strip('\n'))
     715       
     716        self.assertEqual(credentialFactory._verifyOpaque(
     717                nonceOpaque,
     718                challenge['nonce'],
     719                self.clientAddress.host),
     720            True)
     721 No newline at end of file
  • twisted/cred/credentials.py

     
    77from zope.interface import implements, Interface
    88
    99import hmac, time, random
     10from collections import deque
    1011from twisted.python.hashlib import md5
    1112from twisted.python.randbytes import secureRandom
    1213from twisted.cred._digest import calcResponse, calcHA1, calcHA2
     
    197198        self.algorithm = algorithm
    198199        self.authenticationRealm = authenticationRealm
    199200        self.privateKey = secureRandom(12)
     201        self.nonces = set()
     202        self.nonceTimestamps = deque()
    200203
    201204
    202205    def getChallenge(self, address):
     
    226229
    227230        @rtype: C{str}
    228231        """
    229         return secureRandom(12).encode('hex')
     232       
     233        # clean up old nonces
     234        while self.nonceTimestamps and int(self._getTime()) - self.nonceTimestamps[0][1] > self.CHALLENGE_LIFETIME_SECS:
     235            nonce, age = self.nonceTimestamps.popleft()
     236            self.nonces.remove(nonce)
     237       
     238        # keep track of nonce age
     239        nonce = secureRandom(12).encode('hex')
     240        return nonce
    230241
    231242
    232243    def _getTime(self):
     
    285296        if len(keyParts) != 3:
    286297            raise error.LoginFailed('Invalid response, invalid opaque value')
    287298
     299        if nonce in self.nonces:
     300            raise error.LoginFailed(
     301                'Invalid response, nonce already used')
     302       
    288303        if keyParts[0] != nonce:
    289304            raise error.LoginFailed(
    290305                'Invalid response, incompatible opaque/nonce values')
     
    310325        if digest != opaqueParts[0]:
    311326            raise error.LoginFailed('Invalid response, invalid opaque value')
    312327
     328        self.nonces.add(nonce)
     329        self.nonceTimestamps.append((nonce, int(self._getTime())))
     330
    313331        return True
    314332
    315333