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