0
# Association Management
1
2
Handles shared secrets between consumers and servers including creation, serialization, message signing, and expiration management. Associations enable efficient message signing without requiring the consumer to contact the server for each verification.
3
4
## Capabilities
5
6
### Association Objects
7
8
Represents shared secrets between OpenID consumers and servers with cryptographic operations.
9
10
```python { .api }
11
class Association:
12
"""Shared secret association between consumer and server."""
13
14
def __init__(self, handle, secret, issued, lifetime, assoc_type):
15
"""
16
Initialize association with cryptographic parameters.
17
18
Parameters:
19
- handle: str, unique association handle
20
- secret: bytes, shared secret for signing
21
- issued: int, timestamp when association was created
22
- lifetime: int, association lifetime in seconds
23
- assoc_type: str, association type ('HMAC-SHA1' or 'HMAC-SHA256')
24
"""
25
26
@classmethod
27
def fromExpiresIn(cls, expires_in, handle, secret, assoc_type):
28
"""
29
Create association from expiration time.
30
31
Parameters:
32
- expires_in: int, seconds until expiration
33
- handle: str, association handle
34
- secret: bytes, shared secret
35
- assoc_type: str, association type
36
37
Returns:
38
Association object
39
"""
40
41
def serialize(self):
42
"""
43
Serialize association to string format for storage.
44
45
Returns:
46
str, serialized association data
47
"""
48
49
@classmethod
50
def deserialize(cls, assoc_s):
51
"""
52
Deserialize association from string format.
53
54
Parameters:
55
- assoc_s: str, serialized association data
56
57
Returns:
58
Association object
59
60
Raises:
61
ValueError: if deserialization fails
62
"""
63
64
def sign(self, pairs):
65
"""
66
Sign list of key-value pairs.
67
68
Parameters:
69
- pairs: list, [(key, value), ...] tuples to sign
70
71
Returns:
72
str, base64-encoded signature
73
"""
74
75
def getMessageSignature(self, message):
76
"""
77
Generate signature for OpenID message.
78
79
Parameters:
80
- message: Message object to sign
81
82
Returns:
83
str, base64-encoded message signature
84
"""
85
86
def signMessage(self, message):
87
"""
88
Add signature to OpenID message.
89
90
Parameters:
91
- message: Message object to sign (modified in-place)
92
"""
93
94
def checkMessageSignature(self, message):
95
"""
96
Verify message signature against this association.
97
98
Parameters:
99
- message: Message object to verify
100
101
Returns:
102
bool, True if signature is valid
103
"""
104
105
@property
106
def expiresIn(self):
107
"""
108
Get seconds until association expires.
109
110
Returns:
111
int, seconds until expiration (0 if expired)
112
"""
113
```
114
115
### Session Negotiation
116
117
Manages allowed association and session types for establishing shared secrets.
118
119
```python { .api }
120
class SessionNegotiator:
121
"""Manages allowed association and session type combinations."""
122
123
def __init__(self, allowed_types):
124
"""
125
Initialize negotiator with allowed type combinations.
126
127
Parameters:
128
- allowed_types: list, [(assoc_type, session_type), ...] tuples
129
"""
130
131
def copy(self):
132
"""
133
Create copy of this negotiator.
134
135
Returns:
136
SessionNegotiator, copy of this negotiator
137
"""
138
139
def setAllowedTypes(self, allowed_types):
140
"""
141
Set allowed association and session type combinations.
142
143
Parameters:
144
- allowed_types: list, [(assoc_type, session_type), ...] tuples
145
"""
146
147
def addAllowedType(self, assoc_type, session_type=None):
148
"""
149
Add allowed association and session type combination.
150
151
Parameters:
152
- assoc_type: str, association type ('HMAC-SHA1' or 'HMAC-SHA256')
153
- session_type: str, session type (None for all compatible types)
154
"""
155
156
def isAllowed(self, assoc_type, session_type):
157
"""
158
Check if association and session type combination is allowed.
159
160
Parameters:
161
- assoc_type: str, association type
162
- session_type: str, session type
163
164
Returns:
165
bool, True if combination is allowed
166
"""
167
168
def getAllowedType(self):
169
"""
170
Get preferred allowed association and session type combination.
171
172
Returns:
173
tuple, (assoc_type, session_type) or None if no types allowed
174
"""
175
```
176
177
### Association Utilities
178
179
Utility functions for working with association types and session compatibility.
180
181
```python { .api }
182
def getSessionTypes(assoc_type):
183
"""
184
Get compatible session types for association type.
185
186
Parameters:
187
- assoc_type: str, association type
188
189
Returns:
190
list, compatible session type names
191
"""
192
193
def checkSessionType(assoc_type, session_type):
194
"""
195
Validate that session type is compatible with association type.
196
197
Parameters:
198
- assoc_type: str, association type
199
- session_type: str, session type
200
201
Raises:
202
ValueError: if combination is invalid
203
"""
204
205
def getSecretSize(assoc_type):
206
"""
207
Get required secret size for association type.
208
209
Parameters:
210
- assoc_type: str, association type
211
212
Returns:
213
int, required secret size in bytes
214
"""
215
```
216
217
### Pre-configured Negotiators
218
219
Pre-configured session negotiators for common use cases.
220
221
```python { .api }
222
# Default negotiator allowing all standard combinations
223
default_negotiator = SessionNegotiator([
224
('HMAC-SHA1', 'DH-SHA1'),
225
('HMAC-SHA1', 'no-encryption'),
226
('HMAC-SHA256', 'DH-SHA256'),
227
('HMAC-SHA256', 'no-encryption')
228
])
229
230
# Encrypted-only negotiator (no plain-text sessions)
231
encrypted_negotiator = SessionNegotiator([
232
('HMAC-SHA1', 'DH-SHA1'),
233
('HMAC-SHA256', 'DH-SHA256')
234
])
235
```
236
237
## Usage Examples
238
239
### Basic Association Usage
240
241
```python
242
from openid.association import Association
243
import time
244
245
# Create association
246
handle = 'association_handle_123'
247
secret = b'shared_secret_bytes'
248
issued = int(time.time())
249
lifetime = 3600 # 1 hour
250
assoc_type = 'HMAC-SHA1'
251
252
association = Association(handle, secret, issued, lifetime, assoc_type)
253
254
# Check expiration
255
if association.expiresIn > 0:
256
print(f"Association expires in {association.expiresIn} seconds")
257
else:
258
print("Association has expired")
259
260
# Serialize for storage
261
serialized = association.serialize()
262
print(f"Serialized: {serialized}")
263
264
# Deserialize from storage
265
restored = Association.deserialize(serialized)
266
print(f"Restored handle: {restored.handle}")
267
```
268
269
### Message Signing
270
271
```python
272
from openid.message import Message
273
from openid.association import Association
274
275
# Create message
276
message = Message()
277
message.setArg('openid.ns', 'http://specs.openid.net/auth/2.0')
278
message.setArg('openid.mode', 'id_res')
279
message.setArg('openid.identity', 'https://user.example.com')
280
message.setArg('openid.return_to', 'https://consumer.example.com/return')
281
282
# Sign message with association
283
association.signMessage(message)
284
285
# Verify signature
286
is_valid = association.checkMessageSignature(message)
287
print(f"Signature valid: {is_valid}")
288
289
# Manual signing of key-value pairs
290
pairs = [
291
('openid.mode', 'id_res'),
292
('openid.identity', 'https://user.example.com'),
293
('openid.return_to', 'https://consumer.example.com/return')
294
]
295
signature = association.sign(pairs)
296
print(f"Manual signature: {signature}")
297
```
298
299
### Session Negotiator Usage
300
301
```python
302
from openid.association import SessionNegotiator
303
304
# Create custom negotiator
305
negotiator = SessionNegotiator([
306
('HMAC-SHA256', 'DH-SHA256'), # Prefer SHA256 with DH
307
('HMAC-SHA1', 'DH-SHA1') # Fallback to SHA1 with DH
308
])
309
310
# Check if combination is allowed
311
if negotiator.isAllowed('HMAC-SHA256', 'DH-SHA256'):
312
print("SHA256 with DH is allowed")
313
314
# Get preferred type
315
preferred = negotiator.getAllowedType()
316
if preferred:
317
assoc_type, session_type = preferred
318
print(f"Preferred: {assoc_type} with {session_type}")
319
320
# Add new allowed type
321
negotiator.addAllowedType('HMAC-SHA256', 'no-encryption')
322
323
# Use pre-configured negotiators
324
from openid.association import default_negotiator, encrypted_negotiator
325
326
# Default allows plain-text sessions
327
if default_negotiator.isAllowed('HMAC-SHA1', 'no-encryption'):
328
print("Plain-text sessions allowed in default negotiator")
329
330
# Encrypted-only does not allow plain-text
331
if not encrypted_negotiator.isAllowed('HMAC-SHA1', 'no-encryption'):
332
print("Plain-text sessions not allowed in encrypted negotiator")
333
```
334
335
### Consumer Association Management
336
337
```python
338
from openid.consumer import consumer
339
from openid.association import encrypted_negotiator
340
341
# Create consumer with encrypted-only associations
342
openid_consumer = consumer.Consumer({}, store)
343
openid_consumer.setAssociationPreference([
344
('HMAC-SHA256', 'DH-SHA256'),
345
('HMAC-SHA1', 'DH-SHA1')
346
])
347
348
# Start authentication (will use preferred association types)
349
auth_request = openid_consumer.begin(user_url)
350
```
351
352
### Server Association Creation
353
354
```python
355
from openid.server.server import Signatory
356
from openid.association import getSecretSize
357
import os
358
359
# Create signatory for association management
360
signatory = Signatory(store)
361
362
# Create association with specific type
363
assoc_type = 'HMAC-SHA256'
364
secret_size = getSecretSize(assoc_type)
365
secret = os.urandom(secret_size)
366
367
association = signatory.createAssociation(
368
dumb=False, # Smart mode association
369
assoc_type=assoc_type
370
)
371
372
print(f"Created association: {association.handle}")
373
print(f"Expires in: {association.expiresIn} seconds")
374
375
# Store association
376
store.storeAssociation('https://consumer.example.com', association)
377
378
# Later, retrieve for verification
379
retrieved = store.getAssociation('https://consumer.example.com', association.handle)
380
if retrieved:
381
print(f"Retrieved association: {retrieved.handle}")
382
```
383
384
### Association Validation
385
386
```python
387
from openid.association import checkSessionType, getSessionTypes
388
389
# Validate session type compatibility
390
try:
391
checkSessionType('HMAC-SHA256', 'DH-SHA256')
392
print("Valid combination")
393
except ValueError as e:
394
print(f"Invalid combination: {e}")
395
396
# Get compatible session types
397
session_types = getSessionTypes('HMAC-SHA1')
398
print(f"Compatible session types for HMAC-SHA1: {session_types}")
399
400
# Check secret size requirements
401
secret_size_sha1 = getSecretSize('HMAC-SHA1')
402
secret_size_sha256 = getSecretSize('HMAC-SHA256')
403
print(f"HMAC-SHA1 requires {secret_size_sha1} byte secret")
404
print(f"HMAC-SHA256 requires {secret_size_sha256} byte secret")
405
```
406
407
## Types
408
409
```python { .api }
410
# Association types
411
all_association_types = ['HMAC-SHA1', 'HMAC-SHA256']
412
413
# Session types
414
SESSION_TYPE_NO_ENCRYPTION = 'no-encryption'
415
SESSION_TYPE_DH_SHA1 = 'DH-SHA1'
416
SESSION_TYPE_DH_SHA256 = 'DH-SHA256'
417
418
# Secret sizes (in bytes)
419
SECRET_SIZE = {
420
'HMAC-SHA1': 20,
421
'HMAC-SHA256': 32
422
}
423
424
# Default association lifetime
425
DEFAULT_LIFETIME = 14 * 24 * 60 * 60 # 14 days in seconds
426
427
# Association handle format
428
ASSOCIATION_HANDLE_LENGTH = 255 # Maximum length
429
430
# Serialization format version
431
ASSOCIATION_SERIALIZATION_VERSION = 2
432
```