~0x44/nova/bug838466

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/cred/credentials.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
# -*- test-case-name: twisted.test.test_newcred-*-
 
2
 
 
3
# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
 
4
# See LICENSE for details.
 
5
 
 
6
 
 
7
from zope.interface import implements, Interface
 
8
 
 
9
import hmac, time, random
 
10
from twisted.python.hashlib import md5
 
11
from twisted.python.randbytes import secureRandom
 
12
from twisted.cred._digest import calcResponse, calcHA1, calcHA2
 
13
from twisted.cred import error
 
14
 
 
15
class ICredentials(Interface):
 
16
    """
 
17
    I check credentials.
 
18
 
 
19
    Implementors _must_ specify which sub-interfaces of ICredentials
 
20
    to which it conforms, using zope.interface.implements().
 
21
    """
 
22
 
 
23
 
 
24
 
 
25
class IUsernameDigestHash(ICredentials):
 
26
    """
 
27
    This credential is used when a CredentialChecker has access to the hash
 
28
    of the username:realm:password as in an Apache .htdigest file.
 
29
    """
 
30
    def checkHash(digestHash):
 
31
        """
 
32
        @param digestHash: The hashed username:realm:password to check against.
 
33
 
 
34
        @return: C{True} if the credentials represented by this object match
 
35
            the given hash, C{False} if they do not, or a L{Deferred} which
 
36
            will be called back with one of these values.
 
37
        """
 
38
 
 
39
 
 
40
 
 
41
class IUsernameHashedPassword(ICredentials):
 
42
    """
 
43
    I encapsulate a username and a hashed password.
 
44
 
 
45
    This credential is used when a hashed password is received from the
 
46
    party requesting authentication.  CredentialCheckers which check this
 
47
    kind of credential must store the passwords in plaintext (or as
 
48
    password-equivalent hashes) form so that they can be hashed in a manner
 
49
    appropriate for the particular credentials class.
 
50
 
 
51
    @type username: C{str}
 
52
    @ivar username: The username associated with these credentials.
 
53
    """
 
54
 
 
55
    def checkPassword(password):
 
56
        """
 
57
        Validate these credentials against the correct password.
 
58
 
 
59
        @type password: C{str}
 
60
        @param password: The correct, plaintext password against which to
 
61
        check.
 
62
 
 
63
        @rtype: C{bool} or L{Deferred}
 
64
        @return: C{True} if the credentials represented by this object match the
 
65
            given password, C{False} if they do not, or a L{Deferred} which will
 
66
            be called back with one of these values.
 
67
        """
 
68
 
 
69
 
 
70
 
 
71
class IUsernamePassword(ICredentials):
 
72
    """
 
73
    I encapsulate a username and a plaintext password.
 
74
 
 
75
    This encapsulates the case where the password received over the network
 
76
    has been hashed with the identity function (That is, not at all).  The
 
77
    CredentialsChecker may store the password in whatever format it desires,
 
78
    it need only transform the stored password in a similar way before
 
79
    performing the comparison.
 
80
 
 
81
    @type username: C{str}
 
82
    @ivar username: The username associated with these credentials.
 
83
 
 
84
    @type password: C{str}
 
85
    @ivar password: The password associated with these credentials.
 
86
    """
 
87
 
 
88
    def checkPassword(password):
 
89
        """
 
90
        Validate these credentials against the correct password.
 
91
 
 
92
        @type password: C{str}
 
93
        @param password: The correct, plaintext password against which to
 
94
        check.
 
95
 
 
96
        @rtype: C{bool} or L{Deferred}
 
97
        @return: C{True} if the credentials represented by this object match the
 
98
            given password, C{False} if they do not, or a L{Deferred} which will
 
99
            be called back with one of these values.
 
100
        """
 
101
 
 
102
 
 
103
 
 
104
class IAnonymous(ICredentials):
 
105
    """
 
106
    I am an explicitly anonymous request for access.
 
107
    """
 
108
 
 
109
 
 
110
 
 
111
class DigestedCredentials(object):
 
112
    """
 
113
    Yet Another Simple HTTP Digest authentication scheme.
 
114
    """
 
115
    implements(IUsernameHashedPassword, IUsernameDigestHash)
 
116
 
 
117
    def __init__(self, username, method, realm, fields):
 
118
        self.username = username
 
119
        self.method = method
 
120
        self.realm = realm
 
121
        self.fields = fields
 
122
 
 
123
 
 
124
    def checkPassword(self, password):
 
125
        """
 
126
        Verify that the credentials represented by this object agree with the
 
127
        given plaintext C{password} by hashing C{password} in the same way the
 
128
        response hash represented by this object was generated and comparing
 
129
        the results.
 
130
        """
 
131
        response = self.fields.get('response')
 
132
        uri = self.fields.get('uri')
 
133
        nonce = self.fields.get('nonce')
 
134
        cnonce = self.fields.get('cnonce')
 
135
        nc = self.fields.get('nc')
 
136
        algo = self.fields.get('algorithm', 'md5').lower()
 
137
        qop = self.fields.get('qop', 'auth')
 
138
 
 
139
        expected = calcResponse(
 
140
            calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
 
141
            calcHA2(algo, self.method, uri, qop, None),
 
142
            algo, nonce, nc, cnonce, qop)
 
143
 
 
144
        return expected == response
 
145
 
 
146
 
 
147
    def checkHash(self, digestHash):
 
148
        """
 
149
        Verify that the credentials represented by this object agree with the
 
150
        credentials represented by the I{H(A1)} given in C{digestHash}.
 
151
 
 
152
        @param digestHash: A precomputed H(A1) value based on the username,
 
153
            realm, and password associate with this credentials object.
 
154
        """
 
155
        response = self.fields.get('response')
 
156
        uri = self.fields.get('uri')
 
157
        nonce = self.fields.get('nonce')
 
158
        cnonce = self.fields.get('cnonce')
 
159
        nc = self.fields.get('nc')
 
160
        algo = self.fields.get('algorithm', 'md5').lower()
 
161
        qop = self.fields.get('qop', 'auth')
 
162
 
 
163
        expected = calcResponse(
 
164
            calcHA1(algo, None, None, None, nonce, cnonce, preHA1=digestHash),
 
165
            calcHA2(algo, self.method, uri, qop, None),
 
166
            algo, nonce, nc, cnonce, qop)
 
167
 
 
168
        return expected == response
 
169
 
 
170
 
 
171
 
 
172
class DigestCredentialFactory(object):
 
173
    """
 
174
    Support for RFC2617 HTTP Digest Authentication
 
175
 
 
176
    @cvar CHALLENGE_LIFETIME_SECS: The number of seconds for which an
 
177
        opaque should be valid.
 
178
 
 
179
    @type privateKey: C{str}
 
180
    @ivar privateKey: A random string used for generating the secure opaque.
 
181
 
 
182
    @type algorithm: C{str}
 
183
    @param algorithm: Case insensitive string specifying the hash algorithm to
 
184
        use.  Must be either C{'md5'} or C{'sha'}.  C{'md5-sess'} is B{not}
 
185
        supported.
 
186
 
 
187
    @type authenticationRealm: C{str}
 
188
    @param authenticationRealm: case sensitive string that specifies the realm
 
189
        portion of the challenge
 
190
    """
 
191
 
 
192
    CHALLENGE_LIFETIME_SECS = 15 * 60    # 15 minutes
 
193
 
 
194
    scheme = "digest"
 
195
 
 
196
    def __init__(self, algorithm, authenticationRealm):
 
197
        self.algorithm = algorithm
 
198
        self.authenticationRealm = authenticationRealm
 
199
        self.privateKey = secureRandom(12)
 
200
 
 
201
 
 
202
    def getChallenge(self, address):
 
203
        """
 
204
        Generate the challenge for use in the WWW-Authenticate header.
 
205
 
 
206
        @param address: The client address to which this challenge is being
 
207
        sent.
 
208
 
 
209
        @return: The C{dict} that can be used to generate a WWW-Authenticate
 
210
            header.
 
211
        """
 
212
        c = self._generateNonce()
 
213
        o = self._generateOpaque(c, address)
 
214
 
 
215
        return {'nonce': c,
 
216
                'opaque': o,
 
217
                'qop': 'auth',
 
218
                'algorithm': self.algorithm,
 
219
                'realm': self.authenticationRealm}
 
220
 
 
221
 
 
222
    def _generateNonce(self):
 
223
        """
 
224
        Create a random value suitable for use as the nonce parameter of a
 
225
        WWW-Authenticate challenge.
 
226
 
 
227
        @rtype: C{str}
 
228
        """
 
229
        return secureRandom(12).encode('hex')
 
230
 
 
231
 
 
232
    def _getTime(self):
 
233
        """
 
234
        Parameterize the time based seed used in C{_generateOpaque}
 
235
        so we can deterministically unittest it's behavior.
 
236
        """
 
237
        return time.time()
 
238
 
 
239
 
 
240
    def _generateOpaque(self, nonce, clientip):
 
241
        """
 
242
        Generate an opaque to be returned to the client.  This is a unique
 
243
        string that can be returned to us and verified.
 
244
        """
 
245
        # Now, what we do is encode the nonce, client ip and a timestamp in the
 
246
        # opaque value with a suitable digest.
 
247
        now = str(int(self._getTime()))
 
248
        if clientip is None:
 
249
            clientip = ''
 
250
        key = "%s,%s,%s" % (nonce, clientip, now)
 
251
        digest = md5(key + self.privateKey).hexdigest()
 
252
        ekey = key.encode('base64')
 
253
        return "%s-%s" % (digest, ekey.replace('\n', ''))
 
254
 
 
255
 
 
256
    def _verifyOpaque(self, opaque, nonce, clientip):
 
257
        """
 
258
        Given the opaque and nonce from the request, as well as the client IP
 
259
        that made the request, verify that the opaque was generated by us.
 
260
        And that it's not too old.
 
261
 
 
262
        @param opaque: The opaque value from the Digest response
 
263
        @param nonce: The nonce value from the Digest response
 
264
        @param clientip: The remote IP address of the client making the request
 
265
            or C{None} if the request was submitted over a channel where this
 
266
            does not make sense.
 
267
 
 
268
        @return: C{True} if the opaque was successfully verified.
 
269
 
 
270
        @raise error.LoginFailed: if C{opaque} could not be parsed or
 
271
            contained the wrong values.
 
272
        """
 
273
        # First split the digest from the key
 
274
        opaqueParts = opaque.split('-')
 
275
        if len(opaqueParts) != 2:
 
276
            raise error.LoginFailed('Invalid response, invalid opaque value')
 
277
 
 
278
        if clientip is None:
 
279
            clientip = ''
 
280
 
 
281
        # Verify the key
 
282
        key = opaqueParts[1].decode('base64')
 
283
        keyParts = key.split(',')
 
284
 
 
285
        if len(keyParts) != 3:
 
286
            raise error.LoginFailed('Invalid response, invalid opaque value')
 
287
 
 
288
        if keyParts[0] != nonce:
 
289
            raise error.LoginFailed(
 
290
                'Invalid response, incompatible opaque/nonce values')
 
291
 
 
292
        if keyParts[1] != clientip:
 
293
            raise error.LoginFailed(
 
294
                'Invalid response, incompatible opaque/client values')
 
295
 
 
296
        try:
 
297
            when = int(keyParts[2])
 
298
        except ValueError:
 
299
            raise error.LoginFailed(
 
300
                'Invalid response, invalid opaque/time values')
 
301
 
 
302
        if (int(self._getTime()) - when >
 
303
            DigestCredentialFactory.CHALLENGE_LIFETIME_SECS):
 
304
 
 
305
            raise error.LoginFailed(
 
306
                'Invalid response, incompatible opaque/nonce too old')
 
307
 
 
308
        # Verify the digest
 
309
        digest = md5(key + self.privateKey).hexdigest()
 
310
        if digest != opaqueParts[0]:
 
311
            raise error.LoginFailed('Invalid response, invalid opaque value')
 
312
 
 
313
        return True
 
314
 
 
315
 
 
316
    def decode(self, response, method, host):
 
317
        """
 
318
        Decode the given response and attempt to generate a
 
319
        L{DigestedCredentials} from it.
 
320
 
 
321
        @type response: C{str}
 
322
        @param response: A string of comma seperated key=value pairs
 
323
 
 
324
        @type method: C{str}
 
325
        @param method: The action requested to which this response is addressed
 
326
        (GET, POST, INVITE, OPTIONS, etc).
 
327
 
 
328
        @type host: C{str}
 
329
        @param host: The address the request was sent from.
 
330
 
 
331
        @raise error.LoginFailed: If the response does not contain a username,
 
332
            a nonce, an opaque, or if the opaque is invalid.
 
333
 
 
334
        @return: L{DigestedCredentials}
 
335
        """
 
336
        def unq(s):
 
337
            if s[0] == s[-1] == '"':
 
338
                return s[1:-1]
 
339
            return s
 
340
        response = ' '.join(response.splitlines())
 
341
        parts = response.split(',')
 
342
 
 
343
        auth = {}
 
344
 
 
345
        for (k, v) in [p.split('=', 1) for p in parts]:
 
346
            auth[k.strip()] = unq(v.strip())
 
347
 
 
348
        username = auth.get('username')
 
349
        if not username:
 
350
            raise error.LoginFailed('Invalid response, no username given.')
 
351
 
 
352
        if 'opaque' not in auth:
 
353
            raise error.LoginFailed('Invalid response, no opaque given.')
 
354
 
 
355
        if 'nonce' not in auth:
 
356
            raise error.LoginFailed('Invalid response, no nonce given.')
 
357
 
 
358
        # Now verify the nonce/opaque values for this client
 
359
        if self._verifyOpaque(auth.get('opaque'), auth.get('nonce'), host):
 
360
            return DigestedCredentials(username,
 
361
                                       method,
 
362
                                       self.authenticationRealm,
 
363
                                       auth)
 
364
 
 
365
 
 
366
 
 
367
class CramMD5Credentials:
 
368
    implements(IUsernameHashedPassword)
 
369
 
 
370
    challenge = ''
 
371
    response = ''
 
372
 
 
373
    def __init__(self, host=None):
 
374
        self.host = host
 
375
 
 
376
    def getChallenge(self):
 
377
        if self.challenge:
 
378
            return self.challenge
 
379
        # The data encoded in the first ready response contains an
 
380
        # presumptively arbitrary string of random digits, a timestamp, and
 
381
        # the fully-qualified primary host name of the server.  The syntax of
 
382
        # the unencoded form must correspond to that of an RFC 822 'msg-id'
 
383
        # [RFC822] as described in [POP3].
 
384
        #   -- RFC 2195
 
385
        r = random.randrange(0x7fffffff)
 
386
        t = time.time()
 
387
        self.challenge = '<%d.%d@%s>' % (r, t, self.host)
 
388
        return self.challenge
 
389
 
 
390
    def setResponse(self, response):
 
391
        self.username, self.response = response.split(None, 1)
 
392
 
 
393
    def moreChallenges(self):
 
394
        return False
 
395
 
 
396
    def checkPassword(self, password):
 
397
        verify = hmac.HMAC(password, self.challenge).hexdigest()
 
398
        return verify == self.response
 
399
 
 
400
 
 
401
class UsernameHashedPassword:
 
402
    implements(IUsernameHashedPassword)
 
403
 
 
404
    def __init__(self, username, hashed):
 
405
        self.username = username
 
406
        self.hashed = hashed
 
407
 
 
408
    def checkPassword(self, password):
 
409
        return self.hashed == password
 
410
 
 
411
 
 
412
class UsernamePassword:
 
413
    implements(IUsernamePassword)
 
414
 
 
415
    def __init__(self, username, password):
 
416
        self.username = username
 
417
        self.password = password
 
418
 
 
419
    def checkPassword(self, password):
 
420
        return self.password == password
 
421
 
 
422
 
 
423
class Anonymous:
 
424
    implements(IAnonymous)
 
425
 
 
426
 
 
427
 
 
428
class ISSHPrivateKey(ICredentials):
 
429
    """
 
430
    L{ISSHPrivateKey} credentials encapsulate an SSH public key to be checked
 
431
    against a user's private key.
 
432
 
 
433
    @ivar username: The username associated with these credentials.
 
434
    @type username: C{str}
 
435
 
 
436
    @ivar algName: The algorithm name for the blob.
 
437
    @type algName: C{str}
 
438
 
 
439
    @ivar blob: The public key blob as sent by the client.
 
440
    @type blob: C{str}
 
441
 
 
442
    @ivar sigData: The data the signature was made from.
 
443
    @type sigData: C{str}
 
444
 
 
445
    @ivar signature: The signed data.  This is checked to verify that the user
 
446
        owns the private key.
 
447
    @type signature: C{str} or C{NoneType}
 
448
    """
 
449
 
 
450
 
 
451
 
 
452
class SSHPrivateKey:
 
453
    implements(ISSHPrivateKey)
 
454
    def __init__(self, username, algName, blob, sigData, signature):
 
455
        self.username = username
 
456
        self.algName = algName
 
457
        self.blob = blob
 
458
        self.sigData = sigData
 
459
        self.signature = signature
 
460
 
 
461
 
 
462
class IPluggableAuthenticationModules(ICredentials):
 
463
    """I encapsulate the authentication of a user via PAM (Pluggable
 
464
    Authentication Modules.  I use PyPAM (available from
 
465
    http://www.tummy.com/Software/PyPam/index.html).
 
466
 
 
467
    @ivar username: The username for the user being logged in.
 
468
 
 
469
    @ivar pamConversion: A function that is called with a list of tuples
 
470
    (message, messageType).  See the PAM documentation
 
471
    for the meaning of messageType.  The function
 
472
    returns a Deferred which will fire with a list
 
473
    of (response, 0), one for each message.  The 0 is
 
474
    currently unused, but is required by the PAM library.
 
475
    """
 
476
 
 
477
class PluggableAuthenticationModules:
 
478
    implements(IPluggableAuthenticationModules)
 
479
 
 
480
    def __init__(self, username, pamConversion):
 
481
        self.username = username
 
482
        self.pamConversion = pamConversion
 
483