1 """
2 This module contains code for dealing with associations between
3 consumers and servers.
4 """
5
6 import time
7
8 from openid import cryptutil
9 from openid import kvform
10 from openid import oidutil
11
13 """
14 This class represents an association between a server and a
15 consumer. In general, users of this library will never see
16 instances of this object. The only exception is if you implement
17 a custom C{L{OpenIDStore<openid.store.interface.OpenIDStore>}}.
18
19 If you do implement such a store, it will need to store the values
20 of the C{L{handle}}, C{L{secret}}, C{L{issued}}, C{L{lifetime}}, and
21 C{L{assoc_type}} instance variables.
22
23 @ivar handle: This is the handle the server gave this association.
24
25 @type handle: C{str}
26
27
28 @ivar secret: This is the shared secret the server generated for
29 this association.
30
31 @type secret: C{str}
32
33
34 @ivar issued: This is the time this association was issued, in
35 seconds since 00:00 GMT, January 1, 1970. (ie, a unix
36 timestamp)
37
38 @type issued: C{int}
39
40
41 @ivar lifetime: This is the amount of time this association is
42 good for, measured in seconds since the association was
43 issued.
44
45 @type lifetime: C{int}
46
47
48 @ivar assoc_type: This is the type of association this instance
49 represents. The only valid value of this field at this time
50 is C{'HMAC-SHA1'}, but new types may be defined in the future.
51
52 @type assoc_type: C{str}
53
54
55 @sort: __init__, fromExpiresIn, getExpiresIn, __eq__, __ne__,
56 handle, secret, issued, lifetime, assoc_type
57 """
58
59
60 SIG_LENGTH = 20
61
62
63 assoc_keys = [
64 'version',
65 'handle',
66 'secret',
67 'issued',
68 'lifetime',
69 'assoc_type',
70 ]
71
73 """
74 This is an alternate constructor used by the OpenID consumer
75 library to create associations. C{L{OpenIDStore
76 <openid.store.interface.OpenIDStore>}} implementations
77 shouldn't use this constructor.
78
79
80 @param expires_in: This is the amount of time this association
81 is good for, measured in seconds since the association was
82 issued.
83
84 @type expires_in: C{int}
85
86
87 @param handle: This is the handle the server gave this
88 association.
89
90 @type handle: C{str}
91
92
93 @param secret: This is the shared secret the server generated
94 for this association.
95
96 @type secret: C{str}
97
98
99 @param assoc_type: This is the type of association this
100 instance represents. The only valid value of this field
101 at this time is C{'HMAC-SHA1'}, but new types may be
102 defined in the future.
103
104 @type assoc_type: C{str}
105 """
106 issued = int(time.time())
107 lifetime = expires_in
108 return cls(handle, secret, issued, lifetime, assoc_type)
109
110 fromExpiresIn = classmethod(fromExpiresIn)
111
112 - def __init__(self, handle, secret, issued, lifetime, assoc_type):
113 """
114 This is the standard constructor for creating an association.
115
116
117 @param handle: This is the handle the server gave this
118 association.
119
120 @type handle: C{str}
121
122
123 @param secret: This is the shared secret the server generated
124 for this association.
125
126 @type secret: C{str}
127
128
129 @param issued: This is the time this association was issued,
130 in seconds since 00:00 GMT, January 1, 1970. (ie, a unix
131 timestamp)
132
133 @type issued: C{int}
134
135
136 @param lifetime: This is the amount of time this association
137 is good for, measured in seconds since the association was
138 issued.
139
140 @type lifetime: C{int}
141
142
143 @param assoc_type: This is the type of association this
144 instance represents. The only valid value of this field
145 at this time is C{'HMAC-SHA1'}, but new types may be
146 defined in the future.
147
148 @type assoc_type: C{str}
149 """
150 if assoc_type != 'HMAC-SHA1':
151 fmt = 'HMAC-SHA1 is the only supported association type (got %r)'
152 raise ValueError(fmt % (assoc_type,))
153
154 self.handle = handle
155 self.secret = secret
156 self.issued = issued
157 self.lifetime = lifetime
158 self.assoc_type = assoc_type
159
161 """
162 This returns the number of seconds this association is still
163 valid for, or C{0} if the association is no longer valid.
164
165
166 @return: The number of seconds this association is still valid
167 for, or C{0} if the association is no longer valid.
168
169 @rtype: C{int}
170 """
171 if now is None:
172 now = int(time.time())
173
174 return max(0, self.issued + self.lifetime - now)
175
176 expiresIn = property(getExpiresIn)
177
179 """
180 This checks to see if two C{L{Association}} instances
181 represent the same association.
182
183
184 @return: C{True} if the two instances represent the same
185 association, C{False} otherwise.
186
187 @rtype: C{bool}
188 """
189 return type(self) is type(other) and self.__dict__ == other.__dict__
190
192 """
193 This checks to see if two C{L{Association}} instances
194 represent different associations.
195
196
197 @return: C{True} if the two instances represent different
198 associations, C{False} otherwise.
199
200 @rtype: C{bool}
201 """
202 return not (self == other)
203
205 """
206 Convert an association to KV form.
207
208 @return: String in KV form suitable for deserialization by
209 deserialize.
210
211 @rtype: str
212 """
213 data = {
214 'version':'2',
215 'handle':self.handle,
216 'secret':oidutil.toBase64(self.secret),
217 'issued':str(int(self.issued)),
218 'lifetime':str(int(self.lifetime)),
219 'assoc_type':self.assoc_type
220 }
221
222 assert len(data) == len(self.assoc_keys)
223 pairs = []
224 for field_name in self.assoc_keys:
225 pairs.append((field_name, data[field_name]))
226
227 return kvform.seqToKV(pairs, strict=True)
228
230 """
231 Parse an association as stored by serialize().
232
233 inverse of serialize
234
235
236 @param assoc_s: Association as serialized by serialize()
237
238 @type assoc_s: str
239
240
241 @return: instance of this class
242 """
243 pairs = kvform.kvToSeq(assoc_s, strict=True)
244 keys = []
245 values = []
246 for k, v in pairs:
247 keys.append(k)
248 values.append(v)
249
250 if keys != cls.assoc_keys:
251 raise ValueError('Unexpected key values: %r', keys)
252
253 version, handle, secret, issued, lifetime, assoc_type = values
254 if version != '2':
255 raise ValueError('Unknown version: %r' % version)
256 issued = int(issued)
257 lifetime = int(lifetime)
258 secret = oidutil.fromBase64(secret)
259 return cls(handle, secret, issued, lifetime, assoc_type)
260
261 deserialize = classmethod(deserialize)
262
263 - def sign(self, pairs):
264 """
265 Generate a signature for a sequence of (key, value) pairs
266
267
268 @param pairs: The pairs to sign, in order
269
270 @type pairs: sequence of (str, str)
271
272
273 @return: The binary signature of this sequence of pairs
274
275 @rtype: str
276 """
277 assert self.assoc_type == 'HMAC-SHA1'
278 kv = kvform.seqToKV(pairs)
279 return cryptutil.hmacSha1(self.secret, kv)
280
281 - def signDict(self, fields, data, prefix='openid.'):
282 """
283 Generate a signature for some fields in a dictionary
284
285
286 @param fields: The fields to sign, in order
287
288 @type fields: sequence of str
289
290
291 @param data: Dictionary of values to sign
292
293 @type data: {str:str}
294
295
296 @return: the signature, base64 encoded
297
298 @rtype: str
299 """
300 pairs = []
301 for field in fields:
302 pairs.append((field, data.get(prefix + field, '')))
303
304 return oidutil.toBase64(self.sign(pairs))
305
307 sig = self.signDict(fields, data, prefix)
308 signed = ','.join(fields)
309 data[prefix + 'sig'] = sig
310 data[prefix + 'signed'] = signed
311
313 try:
314 signed = data[prefix + 'signed']
315 fields = signed.split(',')
316 expected_sig = self.signDict(fields, data, prefix)
317 request_sig = data[prefix + 'sig']
318 except KeyError:
319 return False
320
321 return request_sig == expected_sig
322