* @copyright 2005-2008 Janrain, Inc. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache */ /** * @access private */ require_once 'Auth/OpenID/CryptUtil.php'; /** * @access private */ require_once 'Auth/OpenID/KVForm.php'; /** * @access private */ require_once 'Auth/OpenID/HMAC.php'; /** * This class represents an association between a server and a * consumer. In general, users of this library will never see * instances of this object. The only exception is if you implement a * custom {@link Auth_OpenID_OpenIDStore}. * * If you do implement such a store, it will need to store the values * of the handle, secret, issued, lifetime, and assoc_type instance * variables. * * @package OpenID */ class Auth_OpenID_Association { /** * This is a HMAC-SHA1 specific value. * * @access private */ var $SIG_LENGTH = 20; /** * The ordering and name of keys as stored by serialize. * * @access private */ var $assoc_keys = array( 'version', 'handle', 'secret', 'issued', 'lifetime', 'assoc_type' ); var $_macs = array( 'HMAC-SHA1' => 'Auth_OpenID_HMACSHA1', 'HMAC-SHA256' => 'Auth_OpenID_HMACSHA256' ); /** * This is an alternate constructor (factory method) used by the * OpenID consumer library to create associations. OpenID store * implementations shouldn't use this constructor. * * @access private * * @param integer $expires_in This is the amount of time this * association is good for, measured in seconds since the * association was issued. * * @param string $handle This is the handle the server gave this * association. * * @param string secret This is the shared secret the server * generated for this association. * * @param assoc_type This is the type of association this * instance represents. The only valid values of this field at * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may * be defined in the future. * * @return association An {@link Auth_OpenID_Association} * instance. */ static function fromExpiresIn($expires_in, $handle, $secret, $assoc_type) { $issued = time(); $lifetime = $expires_in; return new Auth_OpenID_Association($handle, $secret, $issued, $lifetime, $assoc_type); } /** * This is the standard constructor for creating an association. * The library should create all of the necessary associations, so * this constructor is not part of the external API. * * @access private * * @param string $handle This is the handle the server gave this * association. * * @param string $secret This is the shared secret the server * generated for this association. * * @param integer $issued This is the time this association was * issued, in seconds since 00:00 GMT, January 1, 1970. (ie, a * unix timestamp) * * @param integer $lifetime This is the amount of time this * association is good for, measured in seconds since the * association was issued. * * @param string $assoc_type This is the type of association this * instance represents. The only valid values of this field at * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may * be defined in the future. */ function Auth_OpenID_Association( $handle, $secret, $issued, $lifetime, $assoc_type) { if (!in_array($assoc_type, Auth_OpenID_getSupportedAssociationTypes(), true)) { $fmt = 'Unsupported association type (%s)'; trigger_error(sprintf($fmt, $assoc_type), E_USER_ERROR); } $this->handle = $handle; $this->secret = $secret; $this->issued = $issued; $this->lifetime = $lifetime; $this->assoc_type = $assoc_type; } /** * This returns the number of seconds this association is still * valid for, or 0 if the association is no longer valid. * * @return integer $seconds The number of seconds this association * is still valid for, or 0 if the association is no longer valid. */ function getExpiresIn($now = null) { if ($now == null) { $now = time(); } return max(0, $this->issued + $this->lifetime - $now); } /** * This checks to see if two {@link Auth_OpenID_Association} * instances represent the same association. * * @return bool $result true if the two instances represent the * same association, false otherwise. */ function equal($other) { return ((gettype($this) == gettype($other)) && ($this->handle == $other->handle) && ($this->secret == $other->secret) && ($this->issued == $other->issued) && ($this->lifetime == $other->lifetime) && ($this->assoc_type == $other->assoc_type)); } /** * Convert an association to KV form. * * @return string $result String in KV form suitable for * deserialization by deserialize. */ function serialize() { $data = array( 'version' => '2', 'handle' => $this->handle, 'secret' => base64_encode($this->secret), 'issued' => strval(intval($this->issued)), 'lifetime' => strval(intval($this->lifetime)), 'assoc_type' => $this->assoc_type ); assert(array_keys($data) == $this->assoc_keys); return Auth_OpenID_KVForm::fromArray($data, $strict = true); } /** * Parse an association as stored by serialize(). This is the * inverse of serialize. * * @param string $assoc_s Association as serialized by serialize() * @return Auth_OpenID_Association $result instance of this class */ static function deserialize($class_name, $assoc_s) { $pairs = Auth_OpenID_KVForm::toArray($assoc_s, $strict = true); $keys = array(); $values = array(); foreach ($pairs as $key => $value) { if (is_array($value)) { list($key, $value) = $value; } $keys[] = $key; $values[] = $value; } $class_vars = get_class_vars($class_name); $class_assoc_keys = $class_vars['assoc_keys']; sort($keys); sort($class_assoc_keys); if ($keys != $class_assoc_keys) { trigger_error('Unexpected key values: ' . var_export($keys, true), E_USER_WARNING); return null; } $version = $pairs['version']; $handle = $pairs['handle']; $secret = $pairs['secret']; $issued = $pairs['issued']; $lifetime = $pairs['lifetime']; $assoc_type = $pairs['assoc_type']; if ($version != '2') { trigger_error('Unknown version: ' . $version, E_USER_WARNING); return null; } $issued = intval($issued); $lifetime = intval($lifetime); $secret = base64_decode($secret); return new $class_name( $handle, $secret, $issued, $lifetime, $assoc_type); } /** * Generate a signature for a sequence of (key, value) pairs * * @access private * @param array $pairs The pairs to sign, in order. This is an * array of two-tuples. * @return string $signature The binary signature of this sequence * of pairs */ function sign($pairs) { $kv = Auth_OpenID_KVForm::fromArray($pairs); /* Invalid association types should be caught at constructor */ $callback = $this->_macs[$this->assoc_type]; return call_user_func_array($callback, array($this->secret, $kv)); } /** * Generate a signature for some fields in a dictionary * * @access private * @param array $fields The fields to sign, in order; this is an * array of strings. * @param array $data Dictionary of values to sign (an array of * string => string pairs). * @return string $signature The signature, base64 encoded */ function signMessage($message) { if ($message->hasKey(Auth_OpenID_OPENID_NS, 'sig') || $message->hasKey(Auth_OpenID_OPENID_NS, 'signed')) { // Already has a sig return null; } $extant_handle = $message->getArg(Auth_OpenID_OPENID_NS, 'assoc_handle'); if ($extant_handle && ($extant_handle != $this->handle)) { // raise ValueError("Message has a different association handle") return null; } $signed_message = $message; $signed_message->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle', $this->handle); $message_keys = array_keys($signed_message->toPostArgs()); $signed_list = array(); $signed_prefix = 'openid.'; foreach ($message_keys as $k) { if (strpos($k, $signed_prefix) === 0) { $signed_list[] = substr($k, strlen($signed_prefix)); } } $signed_list[] = 'signed'; sort($signed_list); $signed_message->setArg(Auth_OpenID_OPENID_NS, 'signed', implode(',', $signed_list)); $sig = $this->getMessageSignature($signed_message); $signed_message->setArg(Auth_OpenID_OPENID_NS, 'sig', $sig); return $signed_message; } /** * Given a {@link Auth_OpenID_Message}, return the key/value pairs * to be signed according to the signed list in the message. If * the message lacks a signed list, return null. * * @access private */ function _makePairs($message) { $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed'); if (!$signed || Auth_OpenID::isFailure($signed)) { // raise ValueError('Message has no signed list: %s' % (message,)) return null; } $signed_list = explode(',', $signed); $pairs = array(); $data = $message->toPostArgs(); foreach ($signed_list as $field) { $pairs[] = array($field, Auth_OpenID::arrayGet($data, 'openid.' . $field, '')); } return $pairs; } /** * Given an {@link Auth_OpenID_Message}, return the signature for * the signed list in the message. * * @access private */ function getMessageSignature($message) { $pairs = $this->_makePairs($message); return base64_encode($this->sign($pairs)); } /** * Confirm that the signature of these fields matches the * signature contained in the data. * * @access private */ function checkMessageSignature($message) { $sig = $message->getArg(Auth_OpenID_OPENID_NS, 'sig'); if (!$sig || Auth_OpenID::isFailure($sig)) { return false; } $calculated_sig = $this->getMessageSignature($message); return Auth_OpenID_CryptUtil::constEq($calculated_sig, $sig); } } function Auth_OpenID_getSecretSize($assoc_type) { if ($assoc_type == 'HMAC-SHA1') { return 20; } else if ($assoc_type == 'HMAC-SHA256') { return 32; } else { return null; } } function Auth_OpenID_getAllAssociationTypes() { return array('HMAC-SHA1', 'HMAC-SHA256'); } function Auth_OpenID_getSupportedAssociationTypes() { $a = array('HMAC-SHA1'); if (Auth_OpenID_HMACSHA256_SUPPORTED) { $a[] = 'HMAC-SHA256'; } return $a; } function Auth_OpenID_getSessionTypes($assoc_type) { $assoc_to_session = array( 'HMAC-SHA1' => array('DH-SHA1', 'no-encryption')); if (Auth_OpenID_HMACSHA256_SUPPORTED) { $assoc_to_session['HMAC-SHA256'] = array('DH-SHA256', 'no-encryption'); } return Auth_OpenID::arrayGet($assoc_to_session, $assoc_type, array()); } function Auth_OpenID_checkSessionType($assoc_type, $session_type) { if (!in_array($session_type, Auth_OpenID_getSessionTypes($assoc_type))) { return false; } return true; } function Auth_OpenID_getDefaultAssociationOrder() { $order = array(); if (!Auth_OpenID_noMathSupport()) { $order[] = array('HMAC-SHA1', 'DH-SHA1'); if (Auth_OpenID_HMACSHA256_SUPPORTED) { $order[] = array('HMAC-SHA256', 'DH-SHA256'); } } $order[] = array('HMAC-SHA1', 'no-encryption'); if (Auth_OpenID_HMACSHA256_SUPPORTED) { $order[] = array('HMAC-SHA256', 'no-encryption'); } return $order; } function Auth_OpenID_getOnlyEncryptedOrder() { $result = array(); foreach (Auth_OpenID_getDefaultAssociationOrder() as $pair) { list($assoc, $session) = $pair; if ($session != 'no-encryption') { if (Auth_OpenID_HMACSHA256_SUPPORTED && ($assoc == 'HMAC-SHA256')) { $result[] = $pair; } else if ($assoc != 'HMAC-SHA256') { $result[] = $pair; } } } return $result; } function Auth_OpenID_getDefaultNegotiator() { return new Auth_OpenID_SessionNegotiator( Auth_OpenID_getDefaultAssociationOrder()); } function Auth_OpenID_getEncryptedNegotiator() { return new Auth_OpenID_SessionNegotiator( Auth_OpenID_getOnlyEncryptedOrder()); } /** * A session negotiator controls the allowed and preferred association * types and association session types. Both the {@link * Auth_OpenID_Consumer} and {@link Auth_OpenID_Server} use * negotiators when creating associations. * * You can create and use negotiators if you: * - Do not want to do Diffie-Hellman key exchange because you use * transport-layer encryption (e.g. SSL) * * - Want to use only SHA-256 associations * * - Do not want to support plain-text associations over a non-secure * channel * * It is up to you to set a policy for what kinds of associations to * accept. By default, the library will make any kind of association * that is allowed in the OpenID 2.0 specification. * * Use of negotiators in the library * ================================= * * When a consumer makes an association request, it calls {@link * getAllowedType} to get the preferred association type and * association session type. * * The server gets a request for a particular association/session type * and calls {@link isAllowed} to determine if it should create an * association. If it is supported, negotiation is complete. If it is * not, the server calls {@link getAllowedType} to get an allowed * association type to return to the consumer. * * If the consumer gets an error response indicating that the * requested association/session type is not supported by the server * that contains an assocation/session type to try, it calls {@link * isAllowed} to determine if it should try again with the given * combination of association/session type. * * @package OpenID */ class Auth_OpenID_SessionNegotiator { function Auth_OpenID_SessionNegotiator($allowed_types) { $this->allowed_types = array(); $this->setAllowedTypes($allowed_types); } /** * Set the allowed association types, checking to make sure each * combination is valid. * * @access private */ function setAllowedTypes($allowed_types) { foreach ($allowed_types as $pair) { list($assoc_type, $session_type) = $pair; if (!Auth_OpenID_checkSessionType($assoc_type, $session_type)) { return false; } } $this->allowed_types = $allowed_types; return true; } /** * Add an association type and session type to the allowed types * list. The assocation/session pairs are tried in the order that * they are added. * * @access private */ function addAllowedType($assoc_type, $session_type = null) { if ($this->allowed_types === null) { $this->allowed_types = array(); } if ($session_type === null) { $available = Auth_OpenID_getSessionTypes($assoc_type); if (!$available) { return false; } foreach ($available as $session_type) { $this->addAllowedType($assoc_type, $session_type); } } else { if (Auth_OpenID_checkSessionType($assoc_type, $session_type)) { $this->allowed_types[] = array($assoc_type, $session_type); } else { return false; } } return true; } // Is this combination of association type and session type allowed? function isAllowed($assoc_type, $session_type) { $assoc_good = in_array(array($assoc_type, $session_type), $this->allowed_types); $matches = in_array($session_type, Auth_OpenID_getSessionTypes($assoc_type)); return ($assoc_good && $matches); } /** * Get a pair of assocation type and session type that are * supported. */ function getAllowedType() { if (!$this->allowed_types) { return array(null, null); } return $this->allowed_types[0]; } }