forked from GNUsocial/gnu-social
646 lines
19 KiB
PHP
646 lines
19 KiB
PHP
<?php
|
|
|
|
/**
|
|
* This module contains code for dealing with associations between
|
|
* consumers and servers.
|
|
*
|
|
* PHP versions 4 and 5
|
|
*
|
|
* LICENSE: See the COPYING file included in this distribution.
|
|
*
|
|
* @package OpenID
|
|
* @author JanRain, Inc. <openid@janrain.com>
|
|
* @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 $this->constantTimeCompare($calculated_sig, $sig);
|
|
}
|
|
|
|
/**
|
|
* String comparison function which will complete in a constant time
|
|
* for strings of any given matching length, to help prevent an attacker
|
|
* from distinguishing how much of a signature token they have guessed
|
|
* correctly.
|
|
*
|
|
* For this usage, it's assumed that the length of the string is known,
|
|
* so we may safely short-circuit on mismatched lengths which will be known
|
|
* to be invalid by the attacker.
|
|
*
|
|
* http://lists.openid.net/pipermail/openid-security/2010-July/001156.html
|
|
* http://rdist.root.org/2010/01/07/timing-independent-array-comparison/
|
|
*/
|
|
private function constantTimeCompare($a, $b)
|
|
{
|
|
$len = strlen($a);
|
|
if (strlen($b) !== $len) {
|
|
// Short-circuit on length mismatch; attackers will already know
|
|
// the correct target length so this is safe.
|
|
return false;
|
|
}
|
|
if ($len == 0) {
|
|
// 0-length valid input shouldn't really happen. :)
|
|
return true;
|
|
}
|
|
$result = 0;
|
|
for ($i = 0; $i < strlen($a); $i++) {
|
|
// We use scary bitwise operations to avoid logical short-circuits
|
|
// in lower-level code.
|
|
$result |= ord($a{$i}) ^ ord($b{$i});
|
|
}
|
|
return ($result == 0);
|
|
}
|
|
}
|
|
|
|
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];
|
|
}
|
|
}
|
|
|