123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555 |
- # -*- test-case-name: openid.test.test_association -*-
- """
- This module contains code for dealing with associations between
- consumers and servers. Associations contain a shared secret that is
- used to sign C{openid.mode=id_res} messages.
- Users of the library should not usually need to interact directly with
- associations. The L{store<openid.store>},
- L{server<openid.server.server>} and
- L{consumer<openid.consumer.consumer>} objects will create and manage
- the associations. The consumer and server code will make use of a
- C{L{SessionNegotiator}} when managing associations, which enables
- users to express a preference for what kind of associations should be
- allowed, and what kind of exchange should be done to establish the
- association.
- @var default_negotiator: A C{L{SessionNegotiator}} that allows all
- association types that are specified by the OpenID
- specification. It prefers to use HMAC-SHA1/DH-SHA1, if it's
- available. If HMAC-SHA256 is not supported by your Python runtime,
- HMAC-SHA256 and DH-SHA256 will not be available.
- @var encrypted_negotiator: A C{L{SessionNegotiator}} that
- does not support C{'no-encryption'} associations. It prefers
- HMAC-SHA1/DH-SHA1 association types if available.
- """
- __all__ = [
- 'default_negotiator',
- 'encrypted_negotiator',
- 'SessionNegotiator',
- 'Association',
- ]
- import time
- from openid import cryptutil
- from openid import kvform
- from openid import oidutil
- from openid.message import OPENID_NS
- all_association_types = [
- 'HMAC-SHA1',
- 'HMAC-SHA256',
- ]
- if hasattr(cryptutil, 'hmacSha256'):
- supported_association_types = list(all_association_types)
- default_association_order = [
- ('HMAC-SHA1', 'DH-SHA1'),
- ('HMAC-SHA1', 'no-encryption'),
- ('HMAC-SHA256', 'DH-SHA256'),
- ('HMAC-SHA256', 'no-encryption'),
- ]
- only_encrypted_association_order = [
- ('HMAC-SHA1', 'DH-SHA1'),
- ('HMAC-SHA256', 'DH-SHA256'),
- ]
- else:
- supported_association_types = ['HMAC-SHA1']
- default_association_order = [
- ('HMAC-SHA1', 'DH-SHA1'),
- ('HMAC-SHA1', 'no-encryption'),
- ]
- only_encrypted_association_order = [
- ('HMAC-SHA1', 'DH-SHA1'),
- ]
- def getSessionTypes(assoc_type):
- """Return the allowed session types for a given association type"""
- assoc_to_session = {
- 'HMAC-SHA1': ['DH-SHA1', 'no-encryption'],
- 'HMAC-SHA256': ['DH-SHA256', 'no-encryption'],
- }
- return assoc_to_session.get(assoc_type, [])
- def checkSessionType(assoc_type, session_type):
- """Check to make sure that this pair of assoc type and session
- type are allowed"""
- if session_type not in getSessionTypes(assoc_type):
- raise ValueError(
- 'Session type %r not valid for assocation type %r'
- % (session_type, assoc_type))
- class SessionNegotiator(object):
- """A session negotiator controls the allowed and preferred
- association types and association session types. Both the
- C{L{Consumer<openid.consumer.consumer.Consumer>}} and
- C{L{Server<openid.server.server.Server>}} use negotiators when
- creating associations.
- You can create and use negotiators if you:
- - Do not want to do Diffie-Hellman key exchange because you use
- transport-layer encryption (e.g. SSL)
- - Want to use only SHA-256 associations
- - Do not want to support plain-text associations over a non-secure
- channel
- It is up to you to set a policy for what kinds of associations to
- accept. By default, the library will make any kind of association
- that is allowed in the OpenID 2.0 specification.
- Use of negotiators in the library
- =================================
- When a consumer makes an association request, it calls
- C{L{getAllowedType}} to get the preferred association type and
- association session type.
- The server gets a request for a particular association/session
- type and calls C{L{isAllowed}} to determine if it should
- create an association. If it is supported, negotiation is
- complete. If it is not, the server calls C{L{getAllowedType}} to
- get an allowed association type to return to the consumer.
- If the consumer gets an error response indicating that the
- requested association/session type is not supported by the server
- that contains an assocation/session type to try, it calls
- C{L{isAllowed}} to determine if it should try again with the
- given combination of association/session type.
- @ivar allowed_types: A list of association/session types that are
- allowed by the server. The order of the pairs in this list
- determines preference. If an association/session type comes
- earlier in the list, the library is more likely to use that
- type.
- @type allowed_types: [(str, str)]
- """
- def __init__(self, allowed_types):
- self.setAllowedTypes(allowed_types)
- def copy(self):
- return self.__class__(list(self.allowed_types))
- def setAllowedTypes(self, allowed_types):
- """Set the allowed association types, checking to make sure
- each combination is valid."""
- for (assoc_type, session_type) in allowed_types:
- checkSessionType(assoc_type, session_type)
- self.allowed_types = allowed_types
- def addAllowedType(self, assoc_type, session_type=None):
- """Add an association type and session type to the allowed
- types list. The assocation/session pairs are tried in the
- order that they are added."""
- if self.allowed_types is None:
- self.allowed_types = []
- if session_type is None:
- available = getSessionTypes(assoc_type)
- if not available:
- raise ValueError('No session available for association type %r'
- % (assoc_type,))
- for session_type in getSessionTypes(assoc_type):
- self.addAllowedType(assoc_type, session_type)
- else:
- checkSessionType(assoc_type, session_type)
- self.allowed_types.append((assoc_type, session_type))
- def isAllowed(self, assoc_type, session_type):
- """Is this combination of association type and session type allowed?"""
- assoc_good = (assoc_type, session_type) in self.allowed_types
- matches = session_type in getSessionTypes(assoc_type)
- return assoc_good and matches
- def getAllowedType(self):
- """Get a pair of assocation type and session type that are
- supported"""
- try:
- return self.allowed_types[0]
- except IndexError:
- return (None, None)
- default_negotiator = SessionNegotiator(default_association_order)
- encrypted_negotiator = SessionNegotiator(only_encrypted_association_order)
- def getSecretSize(assoc_type):
- if assoc_type == 'HMAC-SHA1':
- return 20
- elif assoc_type == 'HMAC-SHA256':
- return 32
- else:
- raise ValueError('Unsupported association type: %r' % (assoc_type,))
- class Association(object):
- """
- This class represents an association between a server and a
- consumer. In general, users of this library will never see
- instances of this object. The only exception is if you implement
- a custom C{L{OpenIDStore<openid.store.interface.OpenIDStore>}}.
- If you do implement such a store, it will need to store the values
- of the C{L{handle}}, C{L{secret}}, C{L{issued}}, C{L{lifetime}}, and
- C{L{assoc_type}} instance variables.
- @ivar handle: This is the handle the server gave this association.
- @type handle: C{str}
- @ivar secret: This is the shared secret the server generated for
- this association.
- @type secret: C{str}
- @ivar issued: This is the time this association was issued, in
- seconds since 00:00 GMT, January 1, 1970. (ie, a unix
- timestamp)
- @type issued: C{int}
- @ivar lifetime: This is the amount of time this association is
- good for, measured in seconds since the association was
- issued.
- @type lifetime: C{int}
- @ivar assoc_type: This is the type of association this instance
- represents. The only valid value of this field at this time
- is C{'HMAC-SHA1'}, but new types may be defined in the future.
- @type assoc_type: C{str}
- @sort: __init__, fromExpiresIn, getExpiresIn, __eq__, __ne__,
- handle, secret, issued, lifetime, assoc_type
- """
- # The ordering and name of keys as stored by serialize
- assoc_keys = [
- 'version',
- 'handle',
- 'secret',
- 'issued',
- 'lifetime',
- 'assoc_type',
- ]
- _macs = {
- 'HMAC-SHA1': cryptutil.hmacSha1,
- 'HMAC-SHA256': cryptutil.hmacSha256,
- }
- def fromExpiresIn(cls, expires_in, handle, secret, assoc_type):
- """
- This is an alternate constructor used by the OpenID consumer
- library to create associations. C{L{OpenIDStore
- <openid.store.interface.OpenIDStore>}} implementations
- shouldn't use this constructor.
- @param expires_in: This is the amount of time this association
- is good for, measured in seconds since the association was
- issued.
- @type expires_in: C{int}
- @param handle: This is the handle the server gave this
- association.
- @type handle: C{str}
- @param secret: This is the shared secret the server generated
- for this association.
- @type secret: C{str}
- @param assoc_type: This is the type of association this
- instance represents. The only valid value of this field
- at this time is C{'HMAC-SHA1'}, but new types may be
- defined in the future.
- @type assoc_type: C{str}
- """
- issued = int(time.time())
- lifetime = expires_in
- return cls(handle, secret, issued, lifetime, assoc_type)
- fromExpiresIn = classmethod(fromExpiresIn)
- def __init__(self, handle, secret, issued, lifetime, assoc_type):
- """
- This is the standard constructor for creating an association.
- @param handle: This is the handle the server gave this
- association.
- @type handle: C{str}
- @param secret: This is the shared secret the server generated
- for this association.
- @type secret: C{str}
- @param issued: This is the time this association was issued,
- in seconds since 00:00 GMT, January 1, 1970. (ie, a unix
- timestamp)
- @type issued: C{int}
- @param lifetime: This is the amount of time this association
- is good for, measured in seconds since the association was
- issued.
- @type lifetime: C{int}
- @param assoc_type: This is the type of association this
- instance represents. The only valid value of this field
- at this time is C{'HMAC-SHA1'}, but new types may be
- defined in the future.
- @type assoc_type: C{str}
- """
- if assoc_type not in all_association_types:
- fmt = '%r is not a supported association type'
- raise ValueError(fmt % (assoc_type,))
- # secret_size = getSecretSize(assoc_type)
- # if len(secret) != secret_size:
- # fmt = 'Wrong size secret (%s bytes) for association type %s'
- # raise ValueError(fmt % (len(secret), assoc_type))
- self.handle = handle
- self.secret = secret
- self.issued = issued
- self.lifetime = lifetime
- self.assoc_type = assoc_type
- def getExpiresIn(self, now=None):
- """
- This returns the number of seconds this association is still
- valid for, or C{0} if the association is no longer valid.
- @return: The number of seconds this association is still valid
- for, or C{0} if the association is no longer valid.
- @rtype: C{int}
- """
- if now is None:
- now = int(time.time())
- return max(0, self.issued + self.lifetime - now)
- expiresIn = property(getExpiresIn)
- def __eq__(self, other):
- """
- This checks to see if two C{L{Association}} instances
- represent the same association.
- @return: C{True} if the two instances represent the same
- association, C{False} otherwise.
- @rtype: C{bool}
- """
- return type(self) is type(other) and self.__dict__ == other.__dict__
- def __ne__(self, other):
- """
- This checks to see if two C{L{Association}} instances
- represent different associations.
- @return: C{True} if the two instances represent different
- associations, C{False} otherwise.
- @rtype: C{bool}
- """
- return not (self == other)
- def serialize(self):
- """
- Convert an association to KV form.
- @return: String in KV form suitable for deserialization by
- deserialize.
- @rtype: str
- """
- data = {
- 'version':'2',
- 'handle':self.handle,
- 'secret':oidutil.toBase64(self.secret),
- 'issued':str(int(self.issued)),
- 'lifetime':str(int(self.lifetime)),
- 'assoc_type':self.assoc_type
- }
- assert len(data) == len(self.assoc_keys)
- pairs = []
- for field_name in self.assoc_keys:
- pairs.append((field_name, data[field_name]))
- return kvform.seqToKV(pairs, strict=True)
- def deserialize(cls, assoc_s):
- """
- Parse an association as stored by serialize().
- inverse of serialize
- @param assoc_s: Association as serialized by serialize()
- @type assoc_s: str
- @return: instance of this class
- """
- pairs = kvform.kvToSeq(assoc_s, strict=True)
- keys = []
- values = []
- for k, v in pairs:
- keys.append(k)
- values.append(v)
- if keys != cls.assoc_keys:
- raise ValueError('Unexpected key values: %r', keys)
- version, handle, secret, issued, lifetime, assoc_type = values
- if version != '2':
- raise ValueError('Unknown version: %r' % version)
- issued = int(issued)
- lifetime = int(lifetime)
- secret = oidutil.fromBase64(secret)
- return cls(handle, secret, issued, lifetime, assoc_type)
- deserialize = classmethod(deserialize)
- def sign(self, pairs):
- """
- Generate a signature for a sequence of (key, value) pairs
- @param pairs: The pairs to sign, in order
- @type pairs: sequence of (str, str)
- @return: The binary signature of this sequence of pairs
- @rtype: str
- """
- kv = kvform.seqToKV(pairs)
- try:
- mac = self._macs[self.assoc_type]
- except KeyError:
- raise ValueError(
- 'Unknown association type: %r' % (self.assoc_type,))
- return mac(self.secret, kv)
- def getMessageSignature(self, message):
- """Return the signature of a message.
- If I am not a sign-all association, the message must have a
- signed list.
- @return: the signature, base64 encoded
- @rtype: str
- @raises ValueError: If there is no signed list and I am not a sign-all
- type of association.
- """
- pairs = self._makePairs(message)
- return oidutil.toBase64(self.sign(pairs))
- def signMessage(self, message):
- """Add a signature (and a signed list) to a message.
- @return: a new Message object with a signature
- @rtype: L{openid.message.Message}
- """
- if (message.hasKey(OPENID_NS, 'sig') or
- message.hasKey(OPENID_NS, 'signed')):
- raise ValueError('Message already has signed list or signature')
- extant_handle = message.getArg(OPENID_NS, 'assoc_handle')
- if extant_handle and extant_handle != self.handle:
- raise ValueError("Message has a different association handle")
- signed_message = message.copy()
- signed_message.setArg(OPENID_NS, 'assoc_handle', self.handle)
- message_keys = signed_message.toPostArgs().keys()
- signed_list = [k[7:] for k in message_keys
- if k.startswith('openid.')]
- signed_list.append('signed')
- signed_list.sort()
- signed_message.setArg(OPENID_NS, 'signed', ','.join(signed_list))
- sig = self.getMessageSignature(signed_message)
- signed_message.setArg(OPENID_NS, 'sig', sig)
- return signed_message
- def checkMessageSignature(self, message):
- """Given a message with a signature, calculate a new signature
- and return whether it matches the signature in the message.
- @raises ValueError: if the message has no signature or no signature
- can be calculated for it.
- """
- message_sig = message.getArg(OPENID_NS, 'sig')
- if not message_sig:
- raise ValueError("%s has no sig." % (message,))
- calculated_sig = self.getMessageSignature(message)
- return calculated_sig == message_sig
- def _makePairs(self, message):
- signed = message.getArg(OPENID_NS, 'signed')
- if not signed:
- raise ValueError('Message has no signed list: %s' % (message,))
- signed_list = signed.split(',')
- pairs = []
- data = message.toPostArgs()
- for field in signed_list:
- pairs.append((field, data.get('openid.' + field, '')))
- return pairs
- def __repr__(self):
- return "<%s.%s %s %s>" % (
- self.__class__.__module__,
- self.__class__.__name__,
- self.assoc_type,
- self.handle)
|