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