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 */
20require_once 'Auth/OpenID/CryptUtil.php';
21
22/**
23 * @access private
24 */
25require_once 'Auth/OpenID/KVForm.php';
26
27/**
28 * @access private
29 */
30require_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 */
44class 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    static 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(), true)) {
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    static 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 Auth_OpenID_CryptUtil::constEq($calculated_sig, $sig);
378    }
379}
380
381function 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
392function Auth_OpenID_getAllAssociationTypes()
393{
394    return array('HMAC-SHA1', 'HMAC-SHA256');
395}
396
397function 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
408function 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
421function 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
431function 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
452function 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
472function Auth_OpenID_getDefaultNegotiator()
473{
474    return new Auth_OpenID_SessionNegotiator(
475                 Auth_OpenID_getDefaultAssociationOrder());
476}
477
478function Auth_OpenID_getEncryptedNegotiator()
479{
480    return new Auth_OpenID_SessionNegotiator(
481                 Auth_OpenID_getOnlyEncryptedOrder());
482}
483
484/**
485 * A session negotiator controls the allowed and preferred association
486 * types and association session types. Both the {@link
487 * Auth_OpenID_Consumer} and {@link Auth_OpenID_Server} use
488 * negotiators when creating associations.
489 *
490 * You can create and use negotiators if you:
491
492 * - Do not want to do Diffie-Hellman key exchange because you use
493 * transport-layer encryption (e.g. SSL)
494 *
495 * - Want to use only SHA-256 associations
496 *
497 * - Do not want to support plain-text associations over a non-secure
498 * channel
499 *
500 * It is up to you to set a policy for what kinds of associations to
501 * accept. By default, the library will make any kind of association
502 * that is allowed in the OpenID 2.0 specification.
503 *
504 * Use of negotiators in the library
505 * =================================
506 *
507 * When a consumer makes an association request, it calls {@link
508 * getAllowedType} to get the preferred association type and
509 * association session type.
510 *
511 * The server gets a request for a particular association/session type
512 * and calls {@link isAllowed} to determine if it should create an
513 * association. If it is supported, negotiation is complete. If it is
514 * not, the server calls {@link getAllowedType} to get an allowed
515 * association type to return to the consumer.
516 *
517 * If the consumer gets an error response indicating that the
518 * requested association/session type is not supported by the server
519 * that contains an assocation/session type to try, it calls {@link
520 * isAllowed} to determine if it should try again with the given
521 * combination of association/session type.
522 *
523 * @package OpenID
524 */
525class Auth_OpenID_SessionNegotiator {
526    function Auth_OpenID_SessionNegotiator($allowed_types)
527    {
528        $this->allowed_types = array();
529        $this->setAllowedTypes($allowed_types);
530    }
531
532    /**
533     * Set the allowed association types, checking to make sure each
534     * combination is valid.
535     *
536     * @access private
537     */
538    function setAllowedTypes($allowed_types)
539    {
540        foreach ($allowed_types as $pair) {
541            list($assoc_type, $session_type) = $pair;
542            if (!Auth_OpenID_checkSessionType($assoc_type, $session_type)) {
543                return false;
544            }
545        }
546
547        $this->allowed_types = $allowed_types;
548        return true;
549    }
550
551    /**
552     * Add an association type and session type to the allowed types
553     * list. The assocation/session pairs are tried in the order that
554     * they are added.
555     *
556     * @access private
557     */
558    function addAllowedType($assoc_type, $session_type = null)
559    {
560        if ($this->allowed_types === null) {
561            $this->allowed_types = array();
562        }
563
564        if ($session_type === null) {
565            $available = Auth_OpenID_getSessionTypes($assoc_type);
566
567            if (!$available) {
568                return false;
569            }
570
571            foreach ($available as $session_type) {
572                $this->addAllowedType($assoc_type, $session_type);
573            }
574        } else {
575            if (Auth_OpenID_checkSessionType($assoc_type, $session_type)) {
576                $this->allowed_types[] = array($assoc_type, $session_type);
577            } else {
578                return false;
579            }
580        }
581
582        return true;
583    }
584
585    // Is this combination of association type and session type allowed?
586    function isAllowed($assoc_type, $session_type)
587    {
588        $assoc_good = in_array(array($assoc_type, $session_type),
589                               $this->allowed_types);
590
591        $matches = in_array($session_type,
592                            Auth_OpenID_getSessionTypes($assoc_type));
593
594        return ($assoc_good && $matches);
595    }
596
597    /**
598     * Get a pair of assocation type and session type that are
599     * supported.
600     */
601    function getAllowedType()
602    {
603        if (!$this->allowed_types) {
604            return array(null, null);
605        }
606
607        return $this->allowed_types[0];
608    }
609}
610
611