Package openid :: Module association
[frames] | no frames]

Source Code for Module openid.association

  1  # -*- test-case-name: openid.test.test_association -*- 
  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   
73 -def getSessionTypes(assoc_type):
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
81 -def checkSessionType(assoc_type, session_type):
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
89 -class SessionNegotiator(object):
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
137 - def __init__(self, allowed_types):
138 self.setAllowedTypes(allowed_types)
139
140 - def copy(self):
141 return self.__class__(list(self.allowed_types))
142
143 - def setAllowedTypes(self, allowed_types):
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
151 - def addAllowedType(self, assoc_type, session_type=None):
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
178 - def getAllowedType(self):
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
189 -def getSecretSize(assoc_type):
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
197 -class Association(object):
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 # The ordering and name of keys as stored by serialize 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
261 - def fromExpiresIn(cls, expires_in, handle, secret, assoc_type):
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 # secret_size = getSecretSize(assoc_type) 344 # if len(secret) != secret_size: 345 # fmt = 'Wrong size secret (%s bytes) for association type %s' 346 # raise ValueError(fmt % (len(secret), assoc_type)) 347 348 self.handle = handle 349 self.secret = secret 350 self.issued = issued 351 self.lifetime = lifetime 352 self.assoc_type = assoc_type
353
354 - def getExpiresIn(self, now=None):
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
372 - def __eq__(self, other):
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
385 - def __ne__(self, other):
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
398 - def serialize(self):
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
423 - def deserialize(cls, assoc_s):
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
482 - def getMessageSignature(self, message):
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
498 - def signMessage(self, message):
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
524 - def checkMessageSignature(self, message):
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
538 - def _makePairs(self, message):
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
550 - def __repr__(self):
551 return "<%s.%s %s %s>" % ( 552 self.__class__.__module__, 553 self.__class__.__name__, 554 self.assoc_type, 555 self.handle)
556