root/trunk/twisted/cred/credentials.py

Revision 30752, 15.1 KB (checked in by exarkun, 15 months ago)

Rewrite the copyright headers to exclude date information.

Author: exarkun
Reviewer: glyph
Fixes: #4857

To avoid the need to perpetually update copyright dates in each file in Twisted,
remove the dates from most files and just leave them in the LICENSE file.

As a side effect, some files also have had a trailing newline added where it was
missing before.

Line 
1# -*- test-case-name: twisted.test.test_newcred-*-
2
3# Copyright (c) Twisted Matrix Laboratories.
4# See LICENSE for details.
5
6
7from zope.interface import implements, Interface
8
9import hmac, time, random
10from twisted.python.hashlib import md5
11from twisted.python.randbytes import secureRandom
12from twisted.cred._digest import calcResponse, calcHA1, calcHA2
13from twisted.cred import error
14
15class 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
25class 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
41class 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
71class 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
104class IAnonymous(ICredentials):
105    """
106    I am an explicitly anonymous request for access.
107    """
108
109
110
111class 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
172class 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
367class 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
401class 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
412class 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
423class Anonymous:
424    implements(IAnonymous)
425
426
427
428class 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
452class 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
462class 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
477class PluggableAuthenticationModules:
478    implements(IPluggableAuthenticationModules)
479
480    def __init__(self, username, pamConversion):
481        self.username = username
482        self.pamConversion = pamConversion
Note: See TracBrowser for help on using the browser.