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

File ticket-5495-digestcredentials-nonce-verification.diff, 7.5 KB (added by JohnDoeee, 3 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