1
2 """
3 This module contains code for dealing with associations between
4 consumers and servers. Associations contain a shared secret that is
5 used to sign C{openid.mode=id_res} messages.
6
7 Users of the library should not usually need to interact directly with
8 associations. The L{store<openid.store>},
9 L{server<openid.server.server>} and
10 L{consumer<openid.consumer.consumer>} objects will create and manage
11 the associations. The consumer and server code will make use of a
12 C{L{SessionNegotiator}} when managing associations, which enables
13 users to express a preference for what kind of associations should be
14 allowed, and what kind of exchange should be done to establish the
15 association.
16
17 @var default_negotiator: A C{L{SessionNegotiator}} that allows all
18 association types that are specified by the OpenID
19 specification. It prefers to use HMAC-SHA1/DH-SHA1, if it's
20 available. If HMAC-SHA256 is not supported by your Python runtime,
21 HMAC-SHA256 and DH-SHA256 will not be available.
22
23 @var encrypted_negotiator: A C{L{SessionNegotiator}} that
24 does not support C{'no-encryption'} associations. It prefers
25 HMAC-SHA1/DH-SHA1 association types if available.
26 """
27
28 __all__ = [
29 'default_negotiator',
30 'encrypted_negotiator',
31 'SessionNegotiator',
32 'Association',
33 ]
34
35 import time
36
37 from openid import cryptutil
38 from openid import kvform
39 from openid import oidutil
40 from openid.message import OPENID_NS
41
42 all_association_types = [
43 'HMAC-SHA1',
44 'HMAC-SHA256',
45 ]
46
47 if hasattr(cryptutil, 'hmacSha256'):
48 supported_association_types = list(all_association_types)
49
50 default_association_order = [
51 ('HMAC-SHA1', 'DH-SHA1'),
52 ('HMAC-SHA1', 'no-encryption'),
53 ('HMAC-SHA256', 'DH-SHA256'),
54 ('HMAC-SHA256', 'no-encryption'),
55 ]
56
57 only_encrypted_association_order = [
58 ('HMAC-SHA1', 'DH-SHA1'),
59 ('HMAC-SHA256', 'DH-SHA256'),
60 ]
61 else:
62 supported_association_types = ['HMAC-SHA1']
63
64 default_association_order = [
65 ('HMAC-SHA1', 'DH-SHA1'),
66 ('HMAC-SHA1', 'no-encryption'),
67 ]
68
69 only_encrypted_association_order = [
70 ('HMAC-SHA1', 'DH-SHA1'),
71 ]
72
74 """Return the allowed session types for a given association type"""
75 assoc_to_session = {
76 'HMAC-SHA1': ['DH-SHA1', 'no-encryption'],
77 'HMAC-SHA256': ['DH-SHA256', 'no-encryption'],
78 }
79 return assoc_to_session.get(assoc_type, [])
80
82 """Check to make sure that this pair of assoc type and session
83 type are allowed"""
84 if session_type not in getSessionTypes(assoc_type):
85 raise ValueError(
86 'Session type %r not valid for assocation type %r'
87 % (session_type, assoc_type))
88
90 """A session negotiator controls the allowed and preferred
91 association types and association session types. Both the
92 C{L{Consumer<openid.consumer.consumer.Consumer>}} and
93 C{L{Server<openid.server.server.Server>}} use negotiators when
94 creating associations.
95
96 You can create and use negotiators if you:
97
98 - Do not want to do Diffie-Hellman key exchange because you use
99 transport-layer encryption (e.g. SSL)
100
101 - Want to use only SHA-256 associations
102
103 - Do not want to support plain-text associations over a non-secure
104 channel
105
106 It is up to you to set a policy for what kinds of associations to
107 accept. By default, the library will make any kind of association
108 that is allowed in the OpenID 2.0 specification.
109
110 Use of negotiators in the library
111 =================================
112
113 When a consumer makes an association request, it calls
114 C{L{getAllowedType}} to get the preferred association type and
115 association session type.
116
117 The server gets a request for a particular association/session
118 type and calls C{L{isAllowed}} to determine if it should
119 create an association. If it is supported, negotiation is
120 complete. If it is not, the server calls C{L{getAllowedType}} to
121 get an allowed association type to return to the consumer.
122
123 If the consumer gets an error response indicating that the
124 requested association/session type is not supported by the server
125 that contains an assocation/session type to try, it calls
126 C{L{isAllowed}} to determine if it should try again with the
127 given combination of association/session type.
128
129 @ivar allowed_types: A list of association/session types that are
130 allowed by the server. The order of the pairs in this list
131 determines preference. If an association/session type comes
132 earlier in the list, the library is more likely to use that
133 type.
134 @type allowed_types: [(str, str)]
135 """
136
139
141 return self.__class__(list(self.allowed_types))
142
144 """Set the allowed association types, checking to make sure
145 each combination is valid."""
146 for (assoc_type, session_type) in allowed_types:
147 checkSessionType(assoc_type, session_type)
148
149 self.allowed_types = allowed_types
150
152 """Add an association type and session type to the allowed
153 types list. The assocation/session pairs are tried in the
154 order that they are added."""
155 if self.allowed_types is None:
156 self.allowed_types = []
157
158 if session_type is None:
159 available = getSessionTypes(assoc_type)
160
161 if not available:
162 raise ValueError('No session available for association type %r'
163 % (assoc_type,))
164
165 for session_type in getSessionTypes(assoc_type):
166 self.addAllowedType(assoc_type, session_type)
167 else:
168 checkSessionType(assoc_type, session_type)
169 self.allowed_types.append((assoc_type, session_type))
170
171
172 - def isAllowed(self, assoc_type, session_type):
173 """Is this combination of association type and session type allowed?"""
174 assoc_good = (assoc_type, session_type) in self.allowed_types
175 matches = session_type in getSessionTypes(assoc_type)
176 return assoc_good and matches
177
179 """Get a pair of assocation type and session type that are
180 supported"""
181 try:
182 return self.allowed_types[0]
183 except IndexError:
184 return (None, None)
185
186 default_negotiator = SessionNegotiator(default_association_order)
187 encrypted_negotiator = SessionNegotiator(only_encrypted_association_order)
188
190 if assoc_type == 'HMAC-SHA1':
191 return 20
192 elif assoc_type == 'HMAC-SHA256':
193 return 32
194 else:
195 raise ValueError('Unsupported association type: %r' % (assoc_type,))
196
198 """
199 This class represents an association between a server and a
200 consumer. In general, users of this library will never see
201 instances of this object. The only exception is if you implement
202 a custom C{L{OpenIDStore<openid.store.interface.OpenIDStore>}}.
203
204 If you do implement such a store, it will need to store the values
205 of the C{L{handle}}, C{L{secret}}, C{L{issued}}, C{L{lifetime}}, and
206 C{L{assoc_type}} instance variables.
207
208 @ivar handle: This is the handle the server gave this association.
209
210 @type handle: C{str}
211
212
213 @ivar secret: This is the shared secret the server generated for
214 this association.
215
216 @type secret: C{str}
217
218
219 @ivar issued: This is the time this association was issued, in
220 seconds since 00:00 GMT, January 1, 1970. (ie, a unix
221 timestamp)
222
223 @type issued: C{int}
224
225
226 @ivar lifetime: This is the amount of time this association is
227 good for, measured in seconds since the association was
228 issued.
229
230 @type lifetime: C{int}
231
232
233 @ivar assoc_type: This is the type of association this instance
234 represents. The only valid value of this field at this time
235 is C{'HMAC-SHA1'}, but new types may be defined in the future.
236
237 @type assoc_type: C{str}
238
239
240 @sort: __init__, fromExpiresIn, getExpiresIn, __eq__, __ne__,
241 handle, secret, issued, lifetime, assoc_type
242 """
243
244
245 assoc_keys = [
246 'version',
247 'handle',
248 'secret',
249 'issued',
250 'lifetime',
251 'assoc_type',
252 ]
253
254
255 _macs = {
256 'HMAC-SHA1': cryptutil.hmacSha1,
257 'HMAC-SHA256': cryptutil.hmacSha256,
258 }
259
260
262 """
263 This is an alternate constructor used by the OpenID consumer
264 library to create associations. C{L{OpenIDStore
265 <openid.store.interface.OpenIDStore>}} implementations
266 shouldn't use this constructor.
267
268
269 @param expires_in: This is the amount of time this association
270 is good for, measured in seconds since the association was
271 issued.
272
273 @type expires_in: C{int}
274
275
276 @param handle: This is the handle the server gave this
277 association.
278
279 @type handle: C{str}
280
281
282 @param secret: This is the shared secret the server generated
283 for this association.
284
285 @type secret: C{str}
286
287
288 @param assoc_type: This is the type of association this
289 instance represents. The only valid value of this field
290 at this time is C{'HMAC-SHA1'}, but new types may be
291 defined in the future.
292
293 @type assoc_type: C{str}
294 """
295 issued = int(time.time())
296 lifetime = expires_in
297 return cls(handle, secret, issued, lifetime, assoc_type)
298
299 fromExpiresIn = classmethod(fromExpiresIn)
300
301 - def __init__(self, handle, secret, issued, lifetime, assoc_type):
302 """
303 This is the standard constructor for creating an association.
304
305
306 @param handle: This is the handle the server gave this
307 association.
308
309 @type handle: C{str}
310
311
312 @param secret: This is the shared secret the server generated
313 for this association.
314
315 @type secret: C{str}
316
317
318 @param issued: This is the time this association was issued,
319 in seconds since 00:00 GMT, January 1, 1970. (ie, a unix
320 timestamp)
321
322 @type issued: C{int}
323
324
325 @param lifetime: This is the amount of time this association
326 is good for, measured in seconds since the association was
327 issued.
328
329 @type lifetime: C{int}
330
331
332 @param assoc_type: This is the type of association this
333 instance represents. The only valid value of this field
334 at this time is C{'HMAC-SHA1'}, but new types may be
335 defined in the future.
336
337 @type assoc_type: C{str}
338 """
339 if assoc_type not in all_association_types:
340 fmt = '%r is not a supported association type'
341 raise ValueError(fmt % (assoc_type,))
342
343
344
345
346
347
348 self.handle = handle
349 self.secret = secret
350 self.issued = issued
351 self.lifetime = lifetime
352 self.assoc_type = assoc_type
353
355 """
356 This returns the number of seconds this association is still
357 valid for, or C{0} if the association is no longer valid.
358
359
360 @return: The number of seconds this association is still valid
361 for, or C{0} if the association is no longer valid.
362
363 @rtype: C{int}
364 """
365 if now is None:
366 now = int(time.time())
367
368 return max(0, self.issued + self.lifetime - now)
369
370 expiresIn = property(getExpiresIn)
371
373 """
374 This checks to see if two C{L{Association}} instances
375 represent the same association.
376
377
378 @return: C{True} if the two instances represent the same
379 association, C{False} otherwise.
380
381 @rtype: C{bool}
382 """
383 return type(self) is type(other) and self.__dict__ == other.__dict__
384
386 """
387 This checks to see if two C{L{Association}} instances
388 represent different associations.
389
390
391 @return: C{True} if the two instances represent different
392 associations, C{False} otherwise.
393
394 @rtype: C{bool}
395 """
396 return not (self == other)
397
399 """
400 Convert an association to KV form.
401
402 @return: String in KV form suitable for deserialization by
403 deserialize.
404
405 @rtype: str
406 """
407 data = {
408 'version':'2',
409 'handle':self.handle,
410 'secret':oidutil.toBase64(self.secret),
411 'issued':str(int(self.issued)),
412 'lifetime':str(int(self.lifetime)),
413 'assoc_type':self.assoc_type
414 }
415
416 assert len(data) == len(self.assoc_keys)
417 pairs = []
418 for field_name in self.assoc_keys:
419 pairs.append((field_name, data[field_name]))
420
421 return kvform.seqToKV(pairs, strict=True)
422
424 """
425 Parse an association as stored by serialize().
426
427 inverse of serialize
428
429
430 @param assoc_s: Association as serialized by serialize()
431
432 @type assoc_s: str
433
434
435 @return: instance of this class
436 """
437 pairs = kvform.kvToSeq(assoc_s, strict=True)
438 keys = []
439 values = []
440 for k, v in pairs:
441 keys.append(k)
442 values.append(v)
443
444 if keys != cls.assoc_keys:
445 raise ValueError('Unexpected key values: %r', keys)
446
447 version, handle, secret, issued, lifetime, assoc_type = values
448 if version != '2':
449 raise ValueError('Unknown version: %r' % version)
450 issued = int(issued)
451 lifetime = int(lifetime)
452 secret = oidutil.fromBase64(secret)
453 return cls(handle, secret, issued, lifetime, assoc_type)
454
455 deserialize = classmethod(deserialize)
456
457 - def sign(self, pairs):
458 """
459 Generate a signature for a sequence of (key, value) pairs
460
461
462 @param pairs: The pairs to sign, in order
463
464 @type pairs: sequence of (str, str)
465
466
467 @return: The binary signature of this sequence of pairs
468
469 @rtype: str
470 """
471 kv = kvform.seqToKV(pairs)
472
473 try:
474 mac = self._macs[self.assoc_type]
475 except KeyError:
476 raise ValueError(
477 'Unknown association type: %r' % (self.assoc_type,))
478
479 return mac(self.secret, kv)
480
481
483 """Return the signature of a message.
484
485 If I am not a sign-all association, the message must have a
486 signed list.
487
488 @return: the signature, base64 encoded
489
490 @rtype: str
491
492 @raises ValueError: If there is no signed list and I am not a sign-all
493 type of association.
494 """
495 pairs = self._makePairs(message)
496 return oidutil.toBase64(self.sign(pairs))
497
499 """Add a signature (and a signed list) to a message.
500
501 @return: a new Message object with a signature
502 @rtype: L{openid.message.Message}
503 """
504 if (message.hasKey(OPENID_NS, 'sig') or
505 message.hasKey(OPENID_NS, 'signed')):
506 raise ValueError('Message already has signed list or signature')
507
508 extant_handle = message.getArg(OPENID_NS, 'assoc_handle')
509 if extant_handle and extant_handle != self.handle:
510 raise ValueError("Message has a different association handle")
511
512 signed_message = message.copy()
513 signed_message.setArg(OPENID_NS, 'assoc_handle', self.handle)
514 message_keys = signed_message.toPostArgs().keys()
515 signed_list = [k[7:] for k in message_keys
516 if k.startswith('openid.')]
517 signed_list.append('signed')
518 signed_list.sort()
519 signed_message.setArg(OPENID_NS, 'signed', ','.join(signed_list))
520 sig = self.getMessageSignature(signed_message)
521 signed_message.setArg(OPENID_NS, 'sig', sig)
522 return signed_message
523
525 """Given a message with a signature, calculate a new signature
526 and return whether it matches the signature in the message.
527
528 @raises ValueError: if the message has no signature or no signature
529 can be calculated for it.
530 """
531 message_sig = message.getArg(OPENID_NS, 'sig')
532 if not message_sig:
533 raise ValueError("%s has no sig." % (message,))
534 calculated_sig = self.getMessageSignature(message)
535 return cryptutil.const_eq(calculated_sig, message_sig)
536
537
539 signed = message.getArg(OPENID_NS, 'signed')
540 if not signed:
541 raise ValueError('Message has no signed list: %s' % (message,))
542
543 signed_list = signed.split(',')
544 pairs = []
545 data = message.toPostArgs()
546 for field in signed_list:
547 pairs.append((field, data.get('openid.' + field, '')))
548 return pairs
549
551 return "<%s.%s %s %s>" % (
552 self.__class__.__module__,
553 self.__class__.__name__,
554 self.assoc_type,
555 self.handle)
556