~certify-web-dev/twisted/certify-staging

« back to all changes in this revision

Viewing changes to twisted/cred/credentials.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2010-01-02 19:38:17 UTC
  • mfrom: (2.2.4 sid)
  • Revision ID: james.westby@ubuntu.com-20100102193817-jphp464ppwh7dulg
Tags: 9.0.0-1
* python-twisted: Depend on the python-twisted-* 9.0 packages.
* python-twisted: Depend on python-zope.interface only. Closes: #557781.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# -*- test-case-name: twisted.test.test_newcred-*-
2
2
 
3
 
# Copyright (c) 2001-2008 Twisted Matrix Laboratories.
 
3
# Copyright (c) 2001-2009 Twisted Matrix Laboratories.
4
4
# See LICENSE for details.
5
5
 
6
6
 
7
7
from zope.interface import implements, Interface
8
8
 
9
 
import hmac
10
 
import time
11
 
import random
12
 
 
13
 
 
 
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
14
 
15
15
class ICredentials(Interface):
16
16
    """
22
22
 
23
23
 
24
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
 
25
41
class IUsernameHashedPassword(ICredentials):
26
42
    """
27
43
    I encapsulate a username and a hashed password.
84
100
 
85
101
 
86
102
 
 
103
class DigestedCredentials(object):
 
104
    """
 
105
    Yet Another Simple HTTP Digest authentication scheme.
 
106
    """
 
107
    implements(IUsernameHashedPassword, IUsernameDigestHash)
 
108
 
 
109
    def __init__(self, username, method, realm, fields):
 
110
        self.username = username
 
111
        self.method = method
 
112
        self.realm = realm
 
113
        self.fields = fields
 
114
 
 
115
 
 
116
    def checkPassword(self, password):
 
117
        """
 
118
        Verify that the credentials represented by this object agree with the
 
119
        given plaintext C{password} by hashing C{password} in the same way the
 
120
        response hash represented by this object was generated and comparing
 
121
        the results.
 
122
        """
 
123
        response = self.fields.get('response')
 
124
        uri = self.fields.get('uri')
 
125
        nonce = self.fields.get('nonce')
 
126
        cnonce = self.fields.get('cnonce')
 
127
        nc = self.fields.get('nc')
 
128
        algo = self.fields.get('algorithm', 'md5').lower()
 
129
        qop = self.fields.get('qop', 'auth')
 
130
 
 
131
        expected = calcResponse(
 
132
            calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
 
133
            calcHA2(algo, self.method, uri, qop, None),
 
134
            algo, nonce, nc, cnonce, qop)
 
135
 
 
136
        return expected == response
 
137
 
 
138
 
 
139
    def checkHash(self, digestHash):
 
140
        """
 
141
        Verify that the credentials represented by this object agree with the
 
142
        credentials represented by the I{H(A1)} given in C{digestHash}.
 
143
 
 
144
        @param digestHash: A precomputed H(A1) value based on the username,
 
145
            realm, and password associate with this credentials object.
 
146
        """
 
147
        response = self.fields.get('response')
 
148
        uri = self.fields.get('uri')
 
149
        nonce = self.fields.get('nonce')
 
150
        cnonce = self.fields.get('cnonce')
 
151
        nc = self.fields.get('nc')
 
152
        algo = self.fields.get('algorithm', 'md5').lower()
 
153
        qop = self.fields.get('qop', 'auth')
 
154
 
 
155
        expected = calcResponse(
 
156
            calcHA1(algo, None, None, None, nonce, cnonce, preHA1=digestHash),
 
157
            calcHA2(algo, self.method, uri, qop, None),
 
158
            algo, nonce, nc, cnonce, qop)
 
159
 
 
160
        return expected == response
 
161
 
 
162
 
 
163
 
 
164
class DigestCredentialFactory(object):
 
165
    """
 
166
    Support for RFC2617 HTTP Digest Authentication
 
167
 
 
168
    @cvar CHALLENGE_LIFETIME_SECS: The number of seconds for which an
 
169
        opaque should be valid.
 
170
 
 
171
    @type privateKey: C{str}
 
172
    @ivar privateKey: A random string used for generating the secure opaque.
 
173
 
 
174
    @type algorithm: C{str}
 
175
    @param algorithm: Case insensitive string specifying the hash algorithm to
 
176
        use.  Must be either C{'md5'} or C{'sha'}.  C{'md5-sess'} is B{not}
 
177
        supported.
 
178
 
 
179
    @type authenticationRealm: C{str}
 
180
    @param authenticationRealm: case sensitive string that specifies the realm
 
181
        portion of the challenge
 
182
    """
 
183
 
 
184
    CHALLENGE_LIFETIME_SECS = 15 * 60    # 15 minutes
 
185
 
 
186
    scheme = "digest"
 
187
 
 
188
    def __init__(self, algorithm, authenticationRealm):
 
189
        self.algorithm = algorithm
 
190
        self.authenticationRealm = authenticationRealm
 
191
        self.privateKey = secureRandom(12)
 
192
 
 
193
 
 
194
    def getChallenge(self, address):
 
195
        """
 
196
        Generate the challenge for use in the WWW-Authenticate header.
 
197
 
 
198
        @param address: The client address to which this challenge is being
 
199
        sent.
 
200
 
 
201
        @return: The C{dict} that can be used to generate a WWW-Authenticate
 
202
            header.
 
203
        """
 
204
        c = self._generateNonce()
 
205
        o = self._generateOpaque(c, address)
 
206
 
 
207
        return {'nonce': c,
 
208
                'opaque': o,
 
209
                'qop': 'auth',
 
210
                'algorithm': self.algorithm,
 
211
                'realm': self.authenticationRealm}
 
212
 
 
213
 
 
214
    def _generateNonce(self):
 
215
        """
 
216
        Create a random value suitable for use as the nonce parameter of a
 
217
        WWW-Authenticate challenge.
 
218
 
 
219
        @rtype: C{str}
 
220
        """
 
221
        return secureRandom(12).encode('hex')
 
222
 
 
223
 
 
224
    def _getTime(self):
 
225
        """
 
226
        Parameterize the time based seed used in C{_generateOpaque}
 
227
        so we can deterministically unittest it's behavior.
 
228
        """
 
229
        return time.time()
 
230
 
 
231
 
 
232
    def _generateOpaque(self, nonce, clientip):
 
233
        """
 
234
        Generate an opaque to be returned to the client.  This is a unique
 
235
        string that can be returned to us and verified.
 
236
        """
 
237
        # Now, what we do is encode the nonce, client ip and a timestamp in the
 
238
        # opaque value with a suitable digest.
 
239
        now = str(int(self._getTime()))
 
240
        if clientip is None:
 
241
            clientip = ''
 
242
        key = "%s,%s,%s" % (nonce, clientip, now)
 
243
        digest = md5(key + self.privateKey).hexdigest()
 
244
        ekey = key.encode('base64')
 
245
        return "%s-%s" % (digest, ekey.strip('\n'))
 
246
 
 
247
 
 
248
    def _verifyOpaque(self, opaque, nonce, clientip):
 
249
        """
 
250
        Given the opaque and nonce from the request, as well as the client IP
 
251
        that made the request, verify that the opaque was generated by us.
 
252
        And that it's not too old.
 
253
 
 
254
        @param opaque: The opaque value from the Digest response
 
255
        @param nonce: The nonce value from the Digest response
 
256
        @param clientip: The remote IP address of the client making the request
 
257
            or C{None} if the request was submitted over a channel where this
 
258
            does not make sense.
 
259
 
 
260
        @return: C{True} if the opaque was successfully verified.
 
261
 
 
262
        @raise error.LoginFailed: if C{opaque} could not be parsed or
 
263
            contained the wrong values.
 
264
        """
 
265
        # First split the digest from the key
 
266
        opaqueParts = opaque.split('-')
 
267
        if len(opaqueParts) != 2:
 
268
            raise error.LoginFailed('Invalid response, invalid opaque value')
 
269
 
 
270
        if clientip is None:
 
271
            clientip = ''
 
272
 
 
273
        # Verify the key
 
274
        key = opaqueParts[1].decode('base64')
 
275
        keyParts = key.split(',')
 
276
 
 
277
        if len(keyParts) != 3:
 
278
            raise error.LoginFailed('Invalid response, invalid opaque value')
 
279
 
 
280
        if keyParts[0] != nonce:
 
281
            raise error.LoginFailed(
 
282
                'Invalid response, incompatible opaque/nonce values')
 
283
 
 
284
        if keyParts[1] != clientip:
 
285
            raise error.LoginFailed(
 
286
                'Invalid response, incompatible opaque/client values')
 
287
 
 
288
        try:
 
289
            when = int(keyParts[2])
 
290
        except ValueError:
 
291
            raise error.LoginFailed(
 
292
                'Invalid response, invalid opaque/time values')
 
293
 
 
294
        if (int(self._getTime()) - when >
 
295
            DigestCredentialFactory.CHALLENGE_LIFETIME_SECS):
 
296
 
 
297
            raise error.LoginFailed(
 
298
                'Invalid response, incompatible opaque/nonce too old')
 
299
 
 
300
        # Verify the digest
 
301
        digest = md5(key + self.privateKey).hexdigest()
 
302
        if digest != opaqueParts[0]:
 
303
            raise error.LoginFailed('Invalid response, invalid opaque value')
 
304
 
 
305
        return True
 
306
 
 
307
 
 
308
    def decode(self, response, method, host):
 
309
        """
 
310
        Decode the given response and attempt to generate a
 
311
        L{DigestedCredentials} from it.
 
312
 
 
313
        @type response: C{str}
 
314
        @param response: A string of comma seperated key=value pairs
 
315
 
 
316
        @type method: C{str}
 
317
        @param method: The action requested to which this response is addressed
 
318
        (GET, POST, INVITE, OPTIONS, etc).
 
319
 
 
320
        @type host: C{str}
 
321
        @param host: The address the request was sent from.
 
322
 
 
323
        @raise error.LoginFailed: If the response does not contain a username,
 
324
            a nonce, an opaque, or if the opaque is invalid.
 
325
 
 
326
        @return: L{DigestedCredentials}
 
327
        """
 
328
        def unq(s):
 
329
            if s[0] == s[-1] == '"':
 
330
                return s[1:-1]
 
331
            return s
 
332
        response = ' '.join(response.splitlines())
 
333
        parts = response.split(',')
 
334
 
 
335
        auth = {}
 
336
 
 
337
        for (k, v) in [p.split('=', 1) for p in parts]:
 
338
            auth[k.strip()] = unq(v.strip())
 
339
 
 
340
        username = auth.get('username')
 
341
        if not username:
 
342
            raise error.LoginFailed('Invalid response, no username given.')
 
343
 
 
344
        if 'opaque' not in auth:
 
345
            raise error.LoginFailed('Invalid response, no opaque given.')
 
346
 
 
347
        if 'nonce' not in auth:
 
348
            raise error.LoginFailed('Invalid response, no nonce given.')
 
349
 
 
350
        # Now verify the nonce/opaque values for this client
 
351
        if self._verifyOpaque(auth.get('opaque'), auth.get('nonce'), host):
 
352
            return DigestedCredentials(username,
 
353
                                       method,
 
354
                                       self.authenticationRealm,
 
355
                                       auth)
 
356
 
 
357
 
 
358
 
87
359
class CramMD5Credentials:
88
360
    implements(IUsernameHashedPassword)
89
361
 
147
419
 
148
420
class ISSHPrivateKey(ICredentials):
149
421
    """
150
 
    I encapsulate an SSH public key to be checked against a users private
151
 
    key.
 
422
    L{ISSHPrivateKey} credentials encapsulate an SSH public key to be checked
 
423
    against a user's private key.
152
424
 
153
 
    @ivar username: Duh?
 
425
    @ivar username: The username associated with these credentials.
 
426
    @type username: C{str}
154
427
 
155
428
    @ivar algName: The algorithm name for the blob.
 
429
    @type algName: C{str}
156
430
 
157
431
    @ivar blob: The public key blob as sent by the client.
 
432
    @type blob: C{str}
158
433
 
159
434
    @ivar sigData: The data the signature was made from.
 
435
    @type sigData: C{str}
160
436
 
161
437
    @ivar signature: The signed data.  This is checked to verify that the user
162
 
    owns the private key.
 
438
        owns the private key.
 
439
    @type signature: C{str} or C{NoneType}
163
440
    """
164
441
 
165
442