import
[web.mtrack] / inc / lib / Auth / OpenID / Association.php
1 <?php
2
3 /**
4  * This module contains code for dealing with associations between
5  * consumers and servers.
6  *
7  * PHP versions 4 and 5
8  *
9  * LICENSE: See the COPYING file included in this distribution.
10  *
11  * @package OpenID
12  * @author JanRain, Inc. <openid@janrain.com>
13  * @copyright 2005-2008 Janrain, Inc.
14  * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
15  */
16
17 /**
18  * @access private
19  */
20 require_once 'Auth/OpenID/CryptUtil.php';
21
22 /**
23  * @access private
24  */
25 require_once 'Auth/OpenID/KVForm.php';
26
27 /**
28  * @access private
29  */
30 require_once 'Auth/OpenID/HMAC.php';
31
32 /**
33  * This class represents an association between a server and a
34  * consumer.  In general, users of this library will never see
35  * instances of this object.  The only exception is if you implement a
36  * custom {@link Auth_OpenID_OpenIDStore}.
37  *
38  * If you do implement such a store, it will need to store the values
39  * of the handle, secret, issued, lifetime, and assoc_type instance
40  * variables.
41  *
42  * @package OpenID
43  */
44 class Auth_OpenID_Association {
45
46     /**
47      * This is a HMAC-SHA1 specific value.
48      *
49      * @access private
50      */
51     var $SIG_LENGTH = 20;
52
53     /**
54      * The ordering and name of keys as stored by serialize.
55      *
56      * @access private
57      */
58     var $assoc_keys = array(
59                             'version',
60                             'handle',
61                             'secret',
62                             'issued',
63                             'lifetime',
64                             'assoc_type'
65                             );
66
67     var $_macs = array(
68                        'HMAC-SHA1' => 'Auth_OpenID_HMACSHA1',
69                        'HMAC-SHA256' => 'Auth_OpenID_HMACSHA256'
70                        );
71
72     /**
73      * This is an alternate constructor (factory method) used by the
74      * OpenID consumer library to create associations.  OpenID store
75      * implementations shouldn't use this constructor.
76      *
77      * @access private
78      *
79      * @param integer $expires_in This is the amount of time this
80      * association is good for, measured in seconds since the
81      * association was issued.
82      *
83      * @param string $handle This is the handle the server gave this
84      * association.
85      *
86      * @param string secret This is the shared secret the server
87      * generated for this association.
88      *
89      * @param assoc_type This is the type of association this
90      * instance represents.  The only valid values of this field at
91      * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may
92      * be defined in the future.
93      *
94      * @return association An {@link Auth_OpenID_Association}
95      * instance.
96      */
97     function fromExpiresIn($expires_in, $handle, $secret, $assoc_type)
98     {
99         $issued = time();
100         $lifetime = $expires_in;
101         return new Auth_OpenID_Association($handle, $secret,
102                                            $issued, $lifetime, $assoc_type);
103     }
104
105     /**
106      * This is the standard constructor for creating an association.
107      * The library should create all of the necessary associations, so
108      * this constructor is not part of the external API.
109      *
110      * @access private
111      *
112      * @param string $handle This is the handle the server gave this
113      * association.
114      *
115      * @param string $secret This is the shared secret the server
116      * generated for this association.
117      *
118      * @param integer $issued This is the time this association was
119      * issued, in seconds since 00:00 GMT, January 1, 1970.  (ie, a
120      * unix timestamp)
121      *
122      * @param integer $lifetime This is the amount of time this
123      * association is good for, measured in seconds since the
124      * association was issued.
125      *
126      * @param string $assoc_type This is the type of association this
127      * instance represents.  The only valid values of this field at
128      * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may
129      * be defined in the future.
130      */
131     function Auth_OpenID_Association(
132         $handle, $secret, $issued, $lifetime, $assoc_type)
133     {
134         if (!in_array($assoc_type,
135                       Auth_OpenID_getSupportedAssociationTypes())) {
136             $fmt = 'Unsupported association type (%s)';
137             trigger_error(sprintf($fmt, $assoc_type), E_USER_ERROR);
138         }
139
140         $this->handle = $handle;
141         $this->secret = $secret;
142         $this->issued = $issued;
143         $this->lifetime = $lifetime;
144         $this->assoc_type = $assoc_type;
145     }
146
147     /**
148      * This returns the number of seconds this association is still
149      * valid for, or 0 if the association is no longer valid.
150      *
151      * @return integer $seconds The number of seconds this association
152      * is still valid for, or 0 if the association is no longer valid.
153      */
154     function getExpiresIn($now = null)
155     {
156         if ($now == null) {
157             $now = time();
158         }
159
160         return max(0, $this->issued + $this->lifetime - $now);
161     }
162
163     /**
164      * This checks to see if two {@link Auth_OpenID_Association}
165      * instances represent the same association.
166      *
167      * @return bool $result true if the two instances represent the
168      * same association, false otherwise.
169      */
170     function equal($other)
171     {
172         return ((gettype($this) == gettype($other))
173                 && ($this->handle == $other->handle)
174                 && ($this->secret == $other->secret)
175                 && ($this->issued == $other->issued)
176                 && ($this->lifetime == $other->lifetime)
177                 && ($this->assoc_type == $other->assoc_type));
178     }
179
180     /**
181      * Convert an association to KV form.
182      *
183      * @return string $result String in KV form suitable for
184      * deserialization by deserialize.
185      */
186     function serialize()
187     {
188         $data = array(
189                      'version' => '2',
190                      'handle' => $this->handle,
191                      'secret' => base64_encode($this->secret),
192                      'issued' => strval(intval($this->issued)),
193                      'lifetime' => strval(intval($this->lifetime)),
194                      'assoc_type' => $this->assoc_type
195                      );
196
197         assert(array_keys($data) == $this->assoc_keys);
198
199         return Auth_OpenID_KVForm::fromArray($data, $strict = true);
200     }
201
202     /**
203      * Parse an association as stored by serialize().  This is the
204      * inverse of serialize.
205      *
206      * @param string $assoc_s Association as serialized by serialize()
207      * @return Auth_OpenID_Association $result instance of this class
208      */
209     function deserialize($class_name, $assoc_s)
210     {
211         $pairs = Auth_OpenID_KVForm::toArray($assoc_s, $strict = true);
212         $keys = array();
213         $values = array();
214         foreach ($pairs as $key => $value) {
215             if (is_array($value)) {
216                 list($key, $value) = $value;
217             }
218             $keys[] = $key;
219             $values[] = $value;
220         }
221
222         $class_vars = get_class_vars($class_name);
223         $class_assoc_keys = $class_vars['assoc_keys'];
224
225         sort($keys);
226         sort($class_assoc_keys);
227
228         if ($keys != $class_assoc_keys) {
229             trigger_error('Unexpected key values: ' . var_export($keys, true),
230                           E_USER_WARNING);
231             return null;
232         }
233
234         $version = $pairs['version'];
235         $handle = $pairs['handle'];
236         $secret = $pairs['secret'];
237         $issued = $pairs['issued'];
238         $lifetime = $pairs['lifetime'];
239         $assoc_type = $pairs['assoc_type'];
240
241         if ($version != '2') {
242             trigger_error('Unknown version: ' . $version, E_USER_WARNING);
243             return null;
244         }
245
246         $issued = intval($issued);
247         $lifetime = intval($lifetime);
248         $secret = base64_decode($secret);
249
250         return new $class_name(
251             $handle, $secret, $issued, $lifetime, $assoc_type);
252     }
253
254     /**
255      * Generate a signature for a sequence of (key, value) pairs
256      *
257      * @access private
258      * @param array $pairs The pairs to sign, in order.  This is an
259      * array of two-tuples.
260      * @return string $signature The binary signature of this sequence
261      * of pairs
262      */
263     function sign($pairs)
264     {
265         $kv = Auth_OpenID_KVForm::fromArray($pairs);
266
267         /* Invalid association types should be caught at constructor */
268         $callback = $this->_macs[$this->assoc_type];
269
270         return call_user_func_array($callback, array($this->secret, $kv));
271     }
272
273     /**
274      * Generate a signature for some fields in a dictionary
275      *
276      * @access private
277      * @param array $fields The fields to sign, in order; this is an
278      * array of strings.
279      * @param array $data Dictionary of values to sign (an array of
280      * string => string pairs).
281      * @return string $signature The signature, base64 encoded
282      */
283     function signMessage($message)
284     {
285         if ($message->hasKey(Auth_OpenID_OPENID_NS, 'sig') ||
286             $message->hasKey(Auth_OpenID_OPENID_NS, 'signed')) {
287             // Already has a sig
288             return null;
289         }
290
291         $extant_handle = $message->getArg(Auth_OpenID_OPENID_NS,
292                                           'assoc_handle');
293
294         if ($extant_handle && ($extant_handle != $this->handle)) {
295             // raise ValueError("Message has a different association handle")
296             return null;
297         }
298
299         $signed_message = $message;
300         $signed_message->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle',
301                                 $this->handle);
302
303         $message_keys = array_keys($signed_message->toPostArgs());
304         $signed_list = array();
305         $signed_prefix = 'openid.';
306
307         foreach ($message_keys as $k) {
308             if (strpos($k, $signed_prefix) === 0) {
309                 $signed_list[] = substr($k, strlen($signed_prefix));
310             }
311         }
312
313         $signed_list[] = 'signed';
314         sort($signed_list);
315
316         $signed_message->setArg(Auth_OpenID_OPENID_NS, 'signed',
317                                 implode(',', $signed_list));
318         $sig = $this->getMessageSignature($signed_message);
319         $signed_message->setArg(Auth_OpenID_OPENID_NS, 'sig', $sig);
320         return $signed_message;
321     }
322
323     /**
324      * Given a {@link Auth_OpenID_Message}, return the key/value pairs
325      * to be signed according to the signed list in the message.  If
326      * the message lacks a signed list, return null.
327      *
328      * @access private
329      */
330     function _makePairs(&$message)
331     {
332         $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed');
333         if (!$signed || Auth_OpenID::isFailure($signed)) {
334             // raise ValueError('Message has no signed list: %s' % (message,))
335             return null;
336         }
337
338         $signed_list = explode(',', $signed);
339         $pairs = array();
340         $data = $message->toPostArgs();
341         foreach ($signed_list as $field) {
342             $pairs[] = array($field, Auth_OpenID::arrayGet($data,
343                                                            'openid.' .
344                                                            $field, ''));
345         }
346         return $pairs;
347     }
348
349     /**
350      * Given an {@link Auth_OpenID_Message}, return the signature for
351      * the signed list in the message.
352      *
353      * @access private
354      */
355     function getMessageSignature(&$message)
356     {
357         $pairs = $this->_makePairs($message);
358         return base64_encode($this->sign($pairs));
359     }
360
361     /**
362      * Confirm that the signature of these fields matches the
363      * signature contained in the data.
364      *
365      * @access private
366      */
367     function checkMessageSignature(&$message)
368     {
369         $sig = $message->getArg(Auth_OpenID_OPENID_NS,
370                                 'sig');
371
372         if (!$sig || Auth_OpenID::isFailure($sig)) {
373             return false;
374         }
375
376         $calculated_sig = $this->getMessageSignature($message);
377         return $calculated_sig == $sig;
378     }
379 }
380
381 function Auth_OpenID_getSecretSize($assoc_type)
382 {
383     if ($assoc_type == 'HMAC-SHA1') {
384         return 20;
385     } else if ($assoc_type == 'HMAC-SHA256') {
386         return 32;
387     } else {
388         return null;
389     }
390 }
391
392 function Auth_OpenID_getAllAssociationTypes()
393 {
394     return array('HMAC-SHA1', 'HMAC-SHA256');
395 }
396
397 function Auth_OpenID_getSupportedAssociationTypes()
398 {
399     $a = array('HMAC-SHA1');
400
401     if (Auth_OpenID_HMACSHA256_SUPPORTED) {
402         $a[] = 'HMAC-SHA256';
403     }
404
405     return $a;
406 }
407
408 function Auth_OpenID_getSessionTypes($assoc_type)
409 {
410     $assoc_to_session = array(
411        'HMAC-SHA1' => array('DH-SHA1', 'no-encryption'));
412
413     if (Auth_OpenID_HMACSHA256_SUPPORTED) {
414         $assoc_to_session['HMAC-SHA256'] =
415             array('DH-SHA256', 'no-encryption');
416     }
417
418     return Auth_OpenID::arrayGet($assoc_to_session, $assoc_type, array());
419 }
420
421 function Auth_OpenID_checkSessionType($assoc_type, $session_type)
422 {
423     if (!in_array($session_type,
424                   Auth_OpenID_getSessionTypes($assoc_type))) {
425         return false;
426     }
427
428     return true;
429 }
430
431 function Auth_OpenID_getDefaultAssociationOrder()
432 {
433     $order = array();
434
435     if (!Auth_OpenID_noMathSupport()) {
436         $order[] = array('HMAC-SHA1', 'DH-SHA1');
437
438         if (Auth_OpenID_HMACSHA256_SUPPORTED) {
439             $order[] = array('HMAC-SHA256', 'DH-SHA256');
440         }
441     }
442
443     $order[] = array('HMAC-SHA1', 'no-encryption');
444
445     if (Auth_OpenID_HMACSHA256_SUPPORTED) {
446         $order[] = array('HMAC-SHA256', 'no-encryption');
447     }
448
449     return $order;
450 }
451
452 function Auth_OpenID_getOnlyEncryptedOrder()
453 {
454     $result = array();
455
456     foreach (Auth_OpenID_getDefaultAssociationOrder() as $pair) {
457         list($assoc, $session) = $pair;
458
459         if ($session != 'no-encryption') {
460             if (Auth_OpenID_HMACSHA256_SUPPORTED &&
461                 ($assoc == 'HMAC-SHA256')) {
462                 $result[] = $pair;
463             } else if ($assoc != 'HMAC-SHA256') {
464                 $result[] = $pair;
465             }
466         }
467     }
468
469     return $result;
470 }
471
472 function &Auth_OpenID_getDefaultNegotiator()
473 {
474     $x = new Auth_OpenID_SessionNegotiator(
475                  Auth_OpenID_getDefaultAssociationOrder());
476     return $x;
477 }
478
479 function &Auth_OpenID_getEncryptedNegotiator()
480 {
481     $x = new Auth_OpenID_SessionNegotiator(
482                  Auth_OpenID_getOnlyEncryptedOrder());
483     return $x;
484 }
485
486 /**
487  * A session negotiator controls the allowed and preferred association
488  * types and association session types. Both the {@link
489  * Auth_OpenID_Consumer} and {@link Auth_OpenID_Server} use
490  * negotiators when creating associations.
491  *
492  * You can create and use negotiators if you:
493
494  * - Do not want to do Diffie-Hellman key exchange because you use
495  * transport-layer encryption (e.g. SSL)
496  *
497  * - Want to use only SHA-256 associations
498  *
499  * - Do not want to support plain-text associations over a non-secure
500  * channel
501  *
502  * It is up to you to set a policy for what kinds of associations to
503  * accept. By default, the library will make any kind of association
504  * that is allowed in the OpenID 2.0 specification.
505  *
506  * Use of negotiators in the library
507  * =================================
508  *
509  * When a consumer makes an association request, it calls {@link
510  * getAllowedType} to get the preferred association type and
511  * association session type.
512  *
513  * The server gets a request for a particular association/session type
514  * and calls {@link isAllowed} to determine if it should create an
515  * association. If it is supported, negotiation is complete. If it is
516  * not, the server calls {@link getAllowedType} to get an allowed
517  * association type to return to the consumer.
518  *
519  * If the consumer gets an error response indicating that the
520  * requested association/session type is not supported by the server
521  * that contains an assocation/session type to try, it calls {@link
522  * isAllowed} to determine if it should try again with the given
523  * combination of association/session type.
524  *
525  * @package OpenID
526  */
527 class Auth_OpenID_SessionNegotiator {
528     function Auth_OpenID_SessionNegotiator($allowed_types)
529     {
530         $this->allowed_types = array();
531         $this->setAllowedTypes($allowed_types);
532     }
533
534     /**
535      * Set the allowed association types, checking to make sure each
536      * combination is valid.
537      *
538      * @access private
539      */
540     function setAllowedTypes($allowed_types)
541     {
542         foreach ($allowed_types as $pair) {
543             list($assoc_type, $session_type) = $pair;
544             if (!Auth_OpenID_checkSessionType($assoc_type, $session_type)) {
545                 return false;
546             }
547         }
548
549         $this->allowed_types = $allowed_types;
550         return true;
551     }
552
553     /**
554      * Add an association type and session type to the allowed types
555      * list. The assocation/session pairs are tried in the order that
556      * they are added.
557      *
558      * @access private
559      */
560     function addAllowedType($assoc_type, $session_type = null)
561     {
562         if ($this->allowed_types === null) {
563             $this->allowed_types = array();
564         }
565
566         if ($session_type === null) {
567             $available = Auth_OpenID_getSessionTypes($assoc_type);
568
569             if (!$available) {
570                 return false;
571             }
572
573             foreach ($available as $session_type) {
574                 $this->addAllowedType($assoc_type, $session_type);
575             }
576         } else {
577             if (Auth_OpenID_checkSessionType($assoc_type, $session_type)) {
578                 $this->allowed_types[] = array($assoc_type, $session_type);
579             } else {
580                 return false;
581             }
582         }
583
584         return true;
585     }
586
587     // Is this combination of association type and session type allowed?
588     function isAllowed($assoc_type, $session_type)
589     {
590         $assoc_good = in_array(array($assoc_type, $session_type),
591                                $this->allowed_types);
592
593         $matches = in_array($session_type,
594                             Auth_OpenID_getSessionTypes($assoc_type));
595
596         return ($assoc_good && $matches);
597     }
598
599     /**
600      * Get a pair of assocation type and session type that are
601      * supported.
602      */
603     function getAllowedType()
604     {
605         if (!$this->allowed_types) {
606             return array(null, null);
607         }
608
609         return $this->allowed_types[0];
610     }
611 }
612
613 ?>