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