~ntt-pf-lab/nova/monkey_patch_notification

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/test/test_digestauth.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (c) 2008-2009 Twisted Matrix Laboratories.
 
2
# See LICENSE for details.
 
3
 
 
4
"""
 
5
Tests for L{twisted.cred._digest} and the associated bits in
 
6
L{twisted.cred.credentials}.
 
7
"""
 
8
 
 
9
from zope.interface.verify import verifyObject
 
10
from twisted.trial.unittest import TestCase
 
11
from twisted.python.hashlib import md5, sha1
 
12
from twisted.internet.address import IPv4Address
 
13
from twisted.cred.error import LoginFailed
 
14
from twisted.cred.credentials import calcHA1, calcHA2, IUsernameDigestHash
 
15
from twisted.cred.credentials import calcResponse, DigestCredentialFactory
 
16
 
 
17
def b64encode(s):
 
18
    return s.encode('base64').strip()
 
19
 
 
20
 
 
21
class FakeDigestCredentialFactory(DigestCredentialFactory):
 
22
    """
 
23
    A Fake Digest Credential Factory that generates a predictable
 
24
    nonce and opaque
 
25
    """
 
26
    def __init__(self, *args, **kwargs):
 
27
        super(FakeDigestCredentialFactory, self).__init__(*args, **kwargs)
 
28
        self.privateKey = "0"
 
29
 
 
30
 
 
31
    def _generateNonce(self):
 
32
        """
 
33
        Generate a static nonce
 
34
        """
 
35
        return '178288758716122392881254770685'
 
36
 
 
37
 
 
38
    def _getTime(self):
 
39
        """
 
40
        Return a stable time
 
41
        """
 
42
        return 0
 
43
 
 
44
 
 
45
 
 
46
class DigestAuthTests(TestCase):
 
47
    """
 
48
    L{TestCase} mixin class which defines a number of tests for
 
49
    L{DigestCredentialFactory}.  Because this mixin defines C{setUp}, it
 
50
    must be inherited before L{TestCase}.
 
51
    """
 
52
    def setUp(self):
 
53
        """
 
54
        Create a DigestCredentialFactory for testing
 
55
        """
 
56
        self.username = "foobar"
 
57
        self.password = "bazquux"
 
58
        self.realm = "test realm"
 
59
        self.algorithm = "md5"
 
60
        self.cnonce = "29fc54aa1641c6fa0e151419361c8f23"
 
61
        self.qop = "auth"
 
62
        self.uri = "/write/"
 
63
        self.clientAddress = IPv4Address('TCP', '10.2.3.4', 43125)
 
64
        self.method = 'GET'
 
65
        self.credentialFactory = DigestCredentialFactory(
 
66
            self.algorithm, self.realm)
 
67
 
 
68
 
 
69
    def test_MD5HashA1(self, _algorithm='md5', _hash=md5):
 
70
        """
 
71
        L{calcHA1} accepts the C{'md5'} algorithm and returns an MD5 hash of
 
72
        its parameters, excluding the nonce and cnonce.
 
73
        """
 
74
        nonce = 'abc123xyz'
 
75
        hashA1 = calcHA1(_algorithm, self.username, self.realm, self.password,
 
76
                         nonce, self.cnonce)
 
77
        a1 = '%s:%s:%s' % (self.username, self.realm, self.password)
 
78
        expected = _hash(a1).hexdigest()
 
79
        self.assertEqual(hashA1, expected)
 
80
 
 
81
 
 
82
    def test_MD5SessionHashA1(self):
 
83
        """
 
84
        L{calcHA1} accepts the C{'md5-sess'} algorithm and returns an MD5 hash
 
85
        of its parameters, including the nonce and cnonce.
 
86
        """
 
87
        nonce = 'xyz321abc'
 
88
        hashA1 = calcHA1('md5-sess', self.username, self.realm, self.password,
 
89
                         nonce, self.cnonce)
 
90
        a1 = '%s:%s:%s' % (self.username, self.realm, self.password)
 
91
        ha1 = md5(a1).digest()
 
92
        a1 = '%s:%s:%s' % (ha1, nonce, self.cnonce)
 
93
        expected = md5(a1).hexdigest()
 
94
        self.assertEqual(hashA1, expected)
 
95
 
 
96
 
 
97
    def test_SHAHashA1(self):
 
98
        """
 
99
        L{calcHA1} accepts the C{'sha'} algorithm and returns a SHA hash of its
 
100
        parameters, excluding the nonce and cnonce.
 
101
        """
 
102
        self.test_MD5HashA1('sha', sha1)
 
103
 
 
104
 
 
105
    def test_MD5HashA2Auth(self, _algorithm='md5', _hash=md5):
 
106
        """
 
107
        L{calcHA2} accepts the C{'md5'} algorithm and returns an MD5 hash of
 
108
        its arguments, excluding the entity hash for QOP other than
 
109
        C{'auth-int'}.
 
110
        """
 
111
        method = 'GET'
 
112
        hashA2 = calcHA2(_algorithm, method, self.uri, 'auth', None)
 
113
        a2 = '%s:%s' % (method, self.uri)
 
114
        expected = _hash(a2).hexdigest()
 
115
        self.assertEqual(hashA2, expected)
 
116
 
 
117
 
 
118
    def test_MD5HashA2AuthInt(self, _algorithm='md5', _hash=md5):
 
119
        """
 
120
        L{calcHA2} accepts the C{'md5'} algorithm and returns an MD5 hash of
 
121
        its arguments, including the entity hash for QOP of C{'auth-int'}.
 
122
        """
 
123
        method = 'GET'
 
124
        hentity = 'foobarbaz'
 
125
        hashA2 = calcHA2(_algorithm, method, self.uri, 'auth-int', hentity)
 
126
        a2 = '%s:%s:%s' % (method, self.uri, hentity)
 
127
        expected = _hash(a2).hexdigest()
 
128
        self.assertEqual(hashA2, expected)
 
129
 
 
130
 
 
131
    def test_MD5SessHashA2Auth(self):
 
132
        """
 
133
        L{calcHA2} accepts the C{'md5-sess'} algorithm and QOP of C{'auth'} and
 
134
        returns the same value as it does for the C{'md5'} algorithm.
 
135
        """
 
136
        self.test_MD5HashA2Auth('md5-sess')
 
137
 
 
138
 
 
139
    def test_MD5SessHashA2AuthInt(self):
 
140
        """
 
141
        L{calcHA2} accepts the C{'md5-sess'} algorithm and QOP of C{'auth-int'}
 
142
        and returns the same value as it does for the C{'md5'} algorithm.
 
143
        """
 
144
        self.test_MD5HashA2AuthInt('md5-sess')
 
145
 
 
146
 
 
147
    def test_SHAHashA2Auth(self):
 
148
        """
 
149
        L{calcHA2} accepts the C{'sha'} algorithm and returns a SHA hash of
 
150
        its arguments, excluding the entity hash for QOP other than
 
151
        C{'auth-int'}.
 
152
        """
 
153
        self.test_MD5HashA2Auth('sha', sha1)
 
154
 
 
155
 
 
156
    def test_SHAHashA2AuthInt(self):
 
157
        """
 
158
        L{calcHA2} accepts the C{'sha'} algorithm and returns a SHA hash of
 
159
        its arguments, including the entity hash for QOP of C{'auth-int'}.
 
160
        """
 
161
        self.test_MD5HashA2AuthInt('sha', sha1)
 
162
 
 
163
 
 
164
    def test_MD5HashResponse(self, _algorithm='md5', _hash=md5):
 
165
        """
 
166
        L{calcResponse} accepts the C{'md5'} algorithm and returns an MD5 hash
 
167
        of its parameters, excluding the nonce count, client nonce, and QoP
 
168
        value if the nonce count and client nonce are C{None}
 
169
        """
 
170
        hashA1 = 'abc123'
 
171
        hashA2 = '789xyz'
 
172
        nonce = 'lmnopq'
 
173
 
 
174
        response = '%s:%s:%s' % (hashA1, nonce, hashA2)
 
175
        expected = _hash(response).hexdigest()
 
176
 
 
177
        digest = calcResponse(hashA1, hashA2, _algorithm, nonce, None, None,
 
178
                              None)
 
179
        self.assertEqual(expected, digest)
 
180
 
 
181
 
 
182
    def test_MD5SessionHashResponse(self):
 
183
        """
 
184
        L{calcResponse} accepts the C{'md5-sess'} algorithm and returns an MD5
 
185
        hash of its parameters, excluding the nonce count, client nonce, and
 
186
        QoP value if the nonce count and client nonce are C{None}
 
187
        """
 
188
        self.test_MD5HashResponse('md5-sess')
 
189
 
 
190
 
 
191
    def test_SHAHashResponse(self):
 
192
        """
 
193
        L{calcResponse} accepts the C{'sha'} algorithm and returns a SHA hash
 
194
        of its parameters, excluding the nonce count, client nonce, and QoP
 
195
        value if the nonce count and client nonce are C{None}
 
196
        """
 
197
        self.test_MD5HashResponse('sha', sha1)
 
198
 
 
199
 
 
200
    def test_MD5HashResponseExtra(self, _algorithm='md5', _hash=md5):
 
201
        """
 
202
        L{calcResponse} accepts the C{'md5'} algorithm and returns an MD5 hash
 
203
        of its parameters, including the nonce count, client nonce, and QoP
 
204
        value if they are specified.
 
205
        """
 
206
        hashA1 = 'abc123'
 
207
        hashA2 = '789xyz'
 
208
        nonce = 'lmnopq'
 
209
        nonceCount = '00000004'
 
210
        clientNonce = 'abcxyz123'
 
211
        qop = 'auth'
 
212
 
 
213
        response = '%s:%s:%s:%s:%s:%s' % (
 
214
            hashA1, nonce, nonceCount, clientNonce, qop, hashA2)
 
215
        expected = _hash(response).hexdigest()
 
216
 
 
217
        digest = calcResponse(
 
218
            hashA1, hashA2, _algorithm, nonce, nonceCount, clientNonce, qop)
 
219
        self.assertEqual(expected, digest)
 
220
 
 
221
 
 
222
    def test_MD5SessionHashResponseExtra(self):
 
223
        """
 
224
        L{calcResponse} accepts the C{'md5-sess'} algorithm and returns an MD5
 
225
        hash of its parameters, including the nonce count, client nonce, and
 
226
        QoP value if they are specified.
 
227
        """
 
228
        self.test_MD5HashResponseExtra('md5-sess')
 
229
 
 
230
 
 
231
    def test_SHAHashResponseExtra(self):
 
232
        """
 
233
        L{calcResponse} accepts the C{'sha'} algorithm and returns a SHA hash
 
234
        of its parameters, including the nonce count, client nonce, and QoP
 
235
        value if they are specified.
 
236
        """
 
237
        self.test_MD5HashResponseExtra('sha', sha1)
 
238
 
 
239
 
 
240
    def formatResponse(self, quotes=True, **kw):
 
241
        """
 
242
        Format all given keyword arguments and their values suitably for use as
 
243
        the value of an HTTP header.
 
244
 
 
245
        @types quotes: C{bool}
 
246
        @param quotes: A flag indicating whether to quote the values of each
 
247
            field in the response.
 
248
 
 
249
        @param **kw: Keywords and C{str} values which will be treated as field
 
250
            name/value pairs to include in the result.
 
251
 
 
252
        @rtype: C{str}
 
253
        @return: The given fields formatted for use as an HTTP header value.
 
254
        """
 
255
        if 'username' not in kw:
 
256
            kw['username'] = self.username
 
257
        if 'realm' not in kw:
 
258
            kw['realm'] = self.realm
 
259
        if 'algorithm' not in kw:
 
260
            kw['algorithm'] = self.algorithm
 
261
        if 'qop' not in kw:
 
262
            kw['qop'] = self.qop
 
263
        if 'cnonce' not in kw:
 
264
            kw['cnonce'] = self.cnonce
 
265
        if 'uri' not in kw:
 
266
            kw['uri'] = self.uri
 
267
        if quotes:
 
268
            quote = '"'
 
269
        else:
 
270
            quote = ''
 
271
        return ', '.join([
 
272
                '%s=%s%s%s' % (k, quote, v, quote)
 
273
                for (k, v)
 
274
                in kw.iteritems()
 
275
                if v is not None])
 
276
 
 
277
 
 
278
    def getDigestResponse(self, challenge, ncount):
 
279
        """
 
280
        Calculate the response for the given challenge
 
281
        """
 
282
        nonce = challenge.get('nonce')
 
283
        algo = challenge.get('algorithm').lower()
 
284
        qop = challenge.get('qop')
 
285
 
 
286
        ha1 = calcHA1(
 
287
            algo, self.username, self.realm, self.password, nonce, self.cnonce)
 
288
        ha2 = calcHA2(algo, "GET", self.uri, qop, None)
 
289
        expected = calcResponse(ha1, ha2, algo, nonce, ncount, self.cnonce, qop)
 
290
        return expected
 
291
 
 
292
 
 
293
    def test_response(self, quotes=True):
 
294
        """
 
295
        L{DigestCredentialFactory.decode} accepts a digest challenge response
 
296
        and parses it into an L{IUsernameHashedPassword} provider.
 
297
        """
 
298
        challenge = self.credentialFactory.getChallenge(self.clientAddress.host)
 
299
 
 
300
        nc = "00000001"
 
301
        clientResponse = self.formatResponse(
 
302
            quotes=quotes,
 
303
            nonce=challenge['nonce'],
 
304
            response=self.getDigestResponse(challenge, nc),
 
305
            nc=nc,
 
306
            opaque=challenge['opaque'])
 
307
        creds = self.credentialFactory.decode(
 
308
            clientResponse, self.method, self.clientAddress.host)
 
309
        self.assertTrue(creds.checkPassword(self.password))
 
310
        self.assertFalse(creds.checkPassword(self.password + 'wrong'))
 
311
 
 
312
 
 
313
    def test_responseWithoutQuotes(self):
 
314
        """
 
315
        L{DigestCredentialFactory.decode} accepts a digest challenge response
 
316
        which does not quote the values of its fields and parses it into an
 
317
        L{IUsernameHashedPassword} provider in the same way it would a
 
318
        response which included quoted field values.
 
319
        """
 
320
        self.test_response(False)
 
321
 
 
322
 
 
323
    def test_caseInsensitiveAlgorithm(self):
 
324
        """
 
325
        The case of the algorithm value in the response is ignored when
 
326
        checking the credentials.
 
327
        """
 
328
        self.algorithm = 'MD5'
 
329
        self.test_response()
 
330
 
 
331
 
 
332
    def test_md5DefaultAlgorithm(self):
 
333
        """
 
334
        The algorithm defaults to MD5 if it is not supplied in the response.
 
335
        """
 
336
        self.algorithm = None
 
337
        self.test_response()
 
338
 
 
339
 
 
340
    def test_responseWithoutClientIP(self):
 
341
        """
 
342
        L{DigestCredentialFactory.decode} accepts a digest challenge response
 
343
        even if the client address it is passed is C{None}.
 
344
        """
 
345
        challenge = self.credentialFactory.getChallenge(None)
 
346
 
 
347
        nc = "00000001"
 
348
        clientResponse = self.formatResponse(
 
349
            nonce=challenge['nonce'],
 
350
            response=self.getDigestResponse(challenge, nc),
 
351
            nc=nc,
 
352
            opaque=challenge['opaque'])
 
353
        creds = self.credentialFactory.decode(clientResponse, self.method, None)
 
354
        self.assertTrue(creds.checkPassword(self.password))
 
355
        self.assertFalse(creds.checkPassword(self.password + 'wrong'))
 
356
 
 
357
 
 
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
 
 
390
    def test_failsWithDifferentMethod(self):
 
391
        """
 
392
        L{DigestCredentialFactory.decode} returns an L{IUsernameHashedPassword}
 
393
        provider which rejects a correct password for the given user if the
 
394
        challenge response request is made using a different HTTP method than
 
395
        was used to request the initial challenge.
 
396
        """
 
397
        challenge = self.credentialFactory.getChallenge(self.clientAddress.host)
 
398
 
 
399
        nc = "00000001"
 
400
        clientResponse = self.formatResponse(
 
401
            nonce=challenge['nonce'],
 
402
            response=self.getDigestResponse(challenge, nc),
 
403
            nc=nc,
 
404
            opaque=challenge['opaque'])
 
405
        creds = self.credentialFactory.decode(clientResponse, 'POST',
 
406
                                              self.clientAddress.host)
 
407
        self.assertFalse(creds.checkPassword(self.password))
 
408
        self.assertFalse(creds.checkPassword(self.password + 'wrong'))
 
409
 
 
410
 
 
411
    def test_noUsername(self):
 
412
        """
 
413
        L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response
 
414
        has no username field or if the username field is empty.
 
415
        """
 
416
        # Check for no username
 
417
        e = self.assertRaises(
 
418
            LoginFailed,
 
419
            self.credentialFactory.decode,
 
420
            self.formatResponse(username=None),
 
421
            self.method, self.clientAddress.host)
 
422
        self.assertEqual(str(e), "Invalid response, no username given.")
 
423
 
 
424
        # Check for an empty username
 
425
        e = self.assertRaises(
 
426
            LoginFailed,
 
427
            self.credentialFactory.decode,
 
428
            self.formatResponse(username=""),
 
429
            self.method, self.clientAddress.host)
 
430
        self.assertEqual(str(e), "Invalid response, no username given.")
 
431
 
 
432
 
 
433
    def test_noNonce(self):
 
434
        """
 
435
        L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response
 
436
        has no nonce.
 
437
        """
 
438
        e = self.assertRaises(
 
439
            LoginFailed,
 
440
            self.credentialFactory.decode,
 
441
            self.formatResponse(opaque="abc123"),
 
442
            self.method, self.clientAddress.host)
 
443
        self.assertEqual(str(e), "Invalid response, no nonce given.")
 
444
 
 
445
 
 
446
    def test_noOpaque(self):
 
447
        """
 
448
        L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response
 
449
        has no opaque.
 
450
        """
 
451
        e = self.assertRaises(
 
452
            LoginFailed,
 
453
            self.credentialFactory.decode,
 
454
            self.formatResponse(),
 
455
            self.method, self.clientAddress.host)
 
456
        self.assertEqual(str(e), "Invalid response, no opaque given.")
 
457
 
 
458
 
 
459
    def test_checkHash(self):
 
460
        """
 
461
        L{DigestCredentialFactory.decode} returns an L{IUsernameDigestHash}
 
462
        provider which can verify a hash of the form 'username:realm:password'.
 
463
        """
 
464
        challenge = self.credentialFactory.getChallenge(self.clientAddress.host)
 
465
 
 
466
        nc = "00000001"
 
467
        clientResponse = self.formatResponse(
 
468
            nonce=challenge['nonce'],
 
469
            response=self.getDigestResponse(challenge, nc),
 
470
            nc=nc,
 
471
            opaque=challenge['opaque'])
 
472
 
 
473
        creds = self.credentialFactory.decode(clientResponse, self.method,
 
474
                                              self.clientAddress.host)
 
475
        self.assertTrue(verifyObject(IUsernameDigestHash, creds))
 
476
 
 
477
        cleartext = '%s:%s:%s' % (self.username, self.realm, self.password)
 
478
        hash = md5(cleartext)
 
479
        self.assertTrue(creds.checkHash(hash.hexdigest()))
 
480
        hash.update('wrong')
 
481
        self.assertFalse(creds.checkHash(hash.hexdigest()))
 
482
 
 
483
 
 
484
    def test_invalidOpaque(self):
 
485
        """
 
486
        L{DigestCredentialFactory.decode} raises L{LoginFailed} when the opaque
 
487
        value does not contain all the required parts.
 
488
        """
 
489
        credentialFactory = FakeDigestCredentialFactory(self.algorithm,
 
490
                                                        self.realm)
 
491
        challenge = credentialFactory.getChallenge(self.clientAddress.host)
 
492
 
 
493
        exc = self.assertRaises(
 
494
            LoginFailed,
 
495
            credentialFactory._verifyOpaque,
 
496
            'badOpaque',
 
497
            challenge['nonce'],
 
498
            self.clientAddress.host)
 
499
        self.assertEqual(str(exc), 'Invalid response, invalid opaque value')
 
500
 
 
501
        badOpaque = 'foo-' + b64encode('nonce,clientip')
 
502
 
 
503
        exc = self.assertRaises(
 
504
            LoginFailed,
 
505
            credentialFactory._verifyOpaque,
 
506
            badOpaque,
 
507
            challenge['nonce'],
 
508
            self.clientAddress.host)
 
509
        self.assertEqual(str(exc), 'Invalid response, invalid opaque value')
 
510
 
 
511
        exc = self.assertRaises(
 
512
            LoginFailed,
 
513
            credentialFactory._verifyOpaque,
 
514
            '',
 
515
            challenge['nonce'],
 
516
            self.clientAddress.host)
 
517
        self.assertEqual(str(exc), 'Invalid response, invalid opaque value')
 
518
 
 
519
        badOpaque = (
 
520
            'foo-' + b64encode('%s,%s,foobar' % (
 
521
                    challenge['nonce'],
 
522
                    self.clientAddress.host)))
 
523
        exc = self.assertRaises(
 
524
            LoginFailed,
 
525
            credentialFactory._verifyOpaque,
 
526
            badOpaque,
 
527
            challenge['nonce'],
 
528
            self.clientAddress.host)
 
529
        self.assertEqual(
 
530
            str(exc), 'Invalid response, invalid opaque/time values')
 
531
 
 
532
 
 
533
    def test_incompatibleNonce(self):
 
534
        """
 
535
        L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given
 
536
        nonce from the response does not match the nonce encoded in the opaque.
 
537
        """
 
538
        credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm)
 
539
        challenge = credentialFactory.getChallenge(self.clientAddress.host)
 
540
 
 
541
        badNonceOpaque = credentialFactory._generateOpaque(
 
542
            '1234567890',
 
543
            self.clientAddress.host)
 
544
 
 
545
        exc = self.assertRaises(
 
546
            LoginFailed,
 
547
            credentialFactory._verifyOpaque,
 
548
            badNonceOpaque,
 
549
            challenge['nonce'],
 
550
            self.clientAddress.host)
 
551
        self.assertEqual(
 
552
            str(exc),
 
553
            'Invalid response, incompatible opaque/nonce values')
 
554
 
 
555
        exc = self.assertRaises(
 
556
            LoginFailed,
 
557
            credentialFactory._verifyOpaque,
 
558
            badNonceOpaque,
 
559
            '',
 
560
            self.clientAddress.host)
 
561
        self.assertEqual(
 
562
            str(exc),
 
563
            'Invalid response, incompatible opaque/nonce values')
 
564
 
 
565
 
 
566
    def test_incompatibleClientIP(self):
 
567
        """
 
568
        L{DigestCredentialFactory.decode} raises L{LoginFailed} when the
 
569
        request comes from a client IP other than what is encoded in the
 
570
        opaque.
 
571
        """
 
572
        credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm)
 
573
        challenge = credentialFactory.getChallenge(self.clientAddress.host)
 
574
 
 
575
        badAddress = '10.0.0.1'
 
576
        # Sanity check
 
577
        self.assertNotEqual(self.clientAddress.host, badAddress)
 
578
 
 
579
        badNonceOpaque = credentialFactory._generateOpaque(
 
580
            challenge['nonce'], badAddress)
 
581
 
 
582
        self.assertRaises(
 
583
            LoginFailed,
 
584
            credentialFactory._verifyOpaque,
 
585
            badNonceOpaque,
 
586
            challenge['nonce'],
 
587
            self.clientAddress.host)
 
588
 
 
589
 
 
590
    def test_oldNonce(self):
 
591
        """
 
592
        L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given
 
593
        opaque is older than C{DigestCredentialFactory.CHALLENGE_LIFETIME_SECS}
 
594
        """
 
595
        credentialFactory = FakeDigestCredentialFactory(self.algorithm,
 
596
                                                        self.realm)
 
597
        challenge = credentialFactory.getChallenge(self.clientAddress.host)
 
598
 
 
599
        key = '%s,%s,%s' % (challenge['nonce'],
 
600
                            self.clientAddress.host,
 
601
                            '-137876876')
 
602
        digest = md5(key + credentialFactory.privateKey).hexdigest()
 
603
        ekey = b64encode(key)
 
604
 
 
605
        oldNonceOpaque = '%s-%s' % (digest, ekey.strip('\n'))
 
606
 
 
607
        self.assertRaises(
 
608
            LoginFailed,
 
609
            credentialFactory._verifyOpaque,
 
610
            oldNonceOpaque,
 
611
            challenge['nonce'],
 
612
            self.clientAddress.host)
 
613
 
 
614
 
 
615
    def test_mismatchedOpaqueChecksum(self):
 
616
        """
 
617
        L{DigestCredentialFactory.decode} raises L{LoginFailed} when the opaque
 
618
        checksum fails verification.
 
619
        """
 
620
        credentialFactory = FakeDigestCredentialFactory(self.algorithm,
 
621
                                                        self.realm)
 
622
        challenge = credentialFactory.getChallenge(self.clientAddress.host)
 
623
 
 
624
        key = '%s,%s,%s' % (challenge['nonce'],
 
625
                            self.clientAddress.host,
 
626
                            '0')
 
627
 
 
628
        digest = md5(key + 'this is not the right pkey').hexdigest()
 
629
        badChecksum = '%s-%s' % (digest, b64encode(key))
 
630
 
 
631
        self.assertRaises(
 
632
            LoginFailed,
 
633
            credentialFactory._verifyOpaque,
 
634
            badChecksum,
 
635
            challenge['nonce'],
 
636
            self.clientAddress.host)
 
637
 
 
638
 
 
639
    def test_incompatibleCalcHA1Options(self):
 
640
        """
 
641
        L{calcHA1} raises L{TypeError} when any of the pszUsername, pszRealm,
 
642
        or pszPassword arguments are specified with the preHA1 keyword
 
643
        argument.
 
644
        """
 
645
        arguments = (
 
646
            ("user", "realm", "password", "preHA1"),
 
647
            (None, "realm", None, "preHA1"),
 
648
            (None, None, "password", "preHA1"),
 
649
            )
 
650
 
 
651
        for pszUsername, pszRealm, pszPassword, preHA1 in arguments:
 
652
            self.assertRaises(
 
653
                TypeError,
 
654
                calcHA1,
 
655
                "md5",
 
656
                pszUsername,
 
657
                pszRealm,
 
658
                pszPassword,
 
659
                "nonce",
 
660
                "cnonce",
 
661
                preHA1=preHA1)
 
662
 
 
663
 
 
664
    def test_noNewlineOpaque(self):
 
665
        """
 
666
        L{DigestCredentialFactory._generateOpaque} returns a value without
 
667
        newlines, regardless of the length of the nonce.
 
668
        """
 
669
        opaque = self.credentialFactory._generateOpaque(
 
670
            "long nonce " * 10, None)
 
671
        self.assertNotIn('\n', opaque)