Association.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. <?php
  2. /**
  3. * This module contains code for dealing with associations between
  4. * consumers and servers.
  5. *
  6. * PHP versions 4 and 5
  7. *
  8. * LICENSE: See the COPYING file included in this distribution.
  9. *
  10. * @package OpenID
  11. * @author JanRain, Inc. <openid@janrain.com>
  12. * @copyright 2005-2008 Janrain, Inc.
  13. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
  14. */
  15. /**
  16. * @access private
  17. */
  18. require_once 'Auth/OpenID/CryptUtil.php';
  19. /**
  20. * @access private
  21. */
  22. require_once 'Auth/OpenID/KVForm.php';
  23. /**
  24. * @access private
  25. */
  26. require_once 'Auth/OpenID/HMAC.php';
  27. /**
  28. * This class represents an association between a server and a
  29. * consumer. In general, users of this library will never see
  30. * instances of this object. The only exception is if you implement a
  31. * custom {@link Auth_OpenID_OpenIDStore}.
  32. *
  33. * If you do implement such a store, it will need to store the values
  34. * of the handle, secret, issued, lifetime, and assoc_type instance
  35. * variables.
  36. *
  37. * @package OpenID
  38. */
  39. class Auth_OpenID_Association {
  40. /**
  41. * This is a HMAC-SHA1 specific value.
  42. *
  43. * @access private
  44. */
  45. var $SIG_LENGTH = 20;
  46. /**
  47. * The ordering and name of keys as stored by serialize.
  48. *
  49. * @access private
  50. */
  51. var $assoc_keys = array(
  52. 'version',
  53. 'handle',
  54. 'secret',
  55. 'issued',
  56. 'lifetime',
  57. 'assoc_type'
  58. );
  59. var $_macs = array(
  60. 'HMAC-SHA1' => 'Auth_OpenID_HMACSHA1',
  61. 'HMAC-SHA256' => 'Auth_OpenID_HMACSHA256'
  62. );
  63. /**
  64. * This is an alternate constructor (factory method) used by the
  65. * OpenID consumer library to create associations. OpenID store
  66. * implementations shouldn't use this constructor.
  67. *
  68. * @access private
  69. *
  70. * @param integer $expires_in This is the amount of time this
  71. * association is good for, measured in seconds since the
  72. * association was issued.
  73. *
  74. * @param string $handle This is the handle the server gave this
  75. * association.
  76. *
  77. * @param string secret This is the shared secret the server
  78. * generated for this association.
  79. *
  80. * @param assoc_type This is the type of association this
  81. * instance represents. The only valid values of this field at
  82. * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may
  83. * be defined in the future.
  84. *
  85. * @return association An {@link Auth_OpenID_Association}
  86. * instance.
  87. */
  88. static function fromExpiresIn($expires_in, $handle, $secret, $assoc_type)
  89. {
  90. $issued = time();
  91. $lifetime = $expires_in;
  92. return new Auth_OpenID_Association($handle, $secret,
  93. $issued, $lifetime, $assoc_type);
  94. }
  95. /**
  96. * This is the standard constructor for creating an association.
  97. * The library should create all of the necessary associations, so
  98. * this constructor is not part of the external API.
  99. *
  100. * @access private
  101. *
  102. * @param string $handle This is the handle the server gave this
  103. * association.
  104. *
  105. * @param string $secret This is the shared secret the server
  106. * generated for this association.
  107. *
  108. * @param integer $issued This is the time this association was
  109. * issued, in seconds since 00:00 GMT, January 1, 1970. (ie, a
  110. * unix timestamp)
  111. *
  112. * @param integer $lifetime This is the amount of time this
  113. * association is good for, measured in seconds since the
  114. * association was issued.
  115. *
  116. * @param string $assoc_type This is the type of association this
  117. * instance represents. The only valid values of this field at
  118. * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may
  119. * be defined in the future.
  120. */
  121. function Auth_OpenID_Association(
  122. $handle, $secret, $issued, $lifetime, $assoc_type)
  123. {
  124. if (!in_array($assoc_type,
  125. Auth_OpenID_getSupportedAssociationTypes(), true)) {
  126. $fmt = 'Unsupported association type (%s)';
  127. trigger_error(sprintf($fmt, $assoc_type), E_USER_ERROR);
  128. }
  129. $this->handle = $handle;
  130. $this->secret = $secret;
  131. $this->issued = $issued;
  132. $this->lifetime = $lifetime;
  133. $this->assoc_type = $assoc_type;
  134. }
  135. /**
  136. * This returns the number of seconds this association is still
  137. * valid for, or 0 if the association is no longer valid.
  138. *
  139. * @return integer $seconds The number of seconds this association
  140. * is still valid for, or 0 if the association is no longer valid.
  141. */
  142. function getExpiresIn($now = null)
  143. {
  144. if ($now == null) {
  145. $now = time();
  146. }
  147. return max(0, $this->issued + $this->lifetime - $now);
  148. }
  149. /**
  150. * This checks to see if two {@link Auth_OpenID_Association}
  151. * instances represent the same association.
  152. *
  153. * @return bool $result true if the two instances represent the
  154. * same association, false otherwise.
  155. */
  156. function equal($other)
  157. {
  158. return ((gettype($this) == gettype($other))
  159. && ($this->handle == $other->handle)
  160. && ($this->secret == $other->secret)
  161. && ($this->issued == $other->issued)
  162. && ($this->lifetime == $other->lifetime)
  163. && ($this->assoc_type == $other->assoc_type));
  164. }
  165. /**
  166. * Convert an association to KV form.
  167. *
  168. * @return string $result String in KV form suitable for
  169. * deserialization by deserialize.
  170. */
  171. function serialize()
  172. {
  173. $data = array(
  174. 'version' => '2',
  175. 'handle' => $this->handle,
  176. 'secret' => base64_encode($this->secret),
  177. 'issued' => strval(intval($this->issued)),
  178. 'lifetime' => strval(intval($this->lifetime)),
  179. 'assoc_type' => $this->assoc_type
  180. );
  181. assert(array_keys($data) == $this->assoc_keys);
  182. return Auth_OpenID_KVForm::fromArray($data, $strict = true);
  183. }
  184. /**
  185. * Parse an association as stored by serialize(). This is the
  186. * inverse of serialize.
  187. *
  188. * @param string $assoc_s Association as serialized by serialize()
  189. * @return Auth_OpenID_Association $result instance of this class
  190. */
  191. static function deserialize($class_name, $assoc_s)
  192. {
  193. $pairs = Auth_OpenID_KVForm::toArray($assoc_s, $strict = true);
  194. $keys = array();
  195. $values = array();
  196. foreach ($pairs as $key => $value) {
  197. if (is_array($value)) {
  198. list($key, $value) = $value;
  199. }
  200. $keys[] = $key;
  201. $values[] = $value;
  202. }
  203. $class_vars = get_class_vars($class_name);
  204. $class_assoc_keys = $class_vars['assoc_keys'];
  205. sort($keys);
  206. sort($class_assoc_keys);
  207. if ($keys != $class_assoc_keys) {
  208. trigger_error('Unexpected key values: ' . var_export($keys, true),
  209. E_USER_WARNING);
  210. return null;
  211. }
  212. $version = $pairs['version'];
  213. $handle = $pairs['handle'];
  214. $secret = $pairs['secret'];
  215. $issued = $pairs['issued'];
  216. $lifetime = $pairs['lifetime'];
  217. $assoc_type = $pairs['assoc_type'];
  218. if ($version != '2') {
  219. trigger_error('Unknown version: ' . $version, E_USER_WARNING);
  220. return null;
  221. }
  222. $issued = intval($issued);
  223. $lifetime = intval($lifetime);
  224. $secret = base64_decode($secret);
  225. return new $class_name(
  226. $handle, $secret, $issued, $lifetime, $assoc_type);
  227. }
  228. /**
  229. * Generate a signature for a sequence of (key, value) pairs
  230. *
  231. * @access private
  232. * @param array $pairs The pairs to sign, in order. This is an
  233. * array of two-tuples.
  234. * @return string $signature The binary signature of this sequence
  235. * of pairs
  236. */
  237. function sign($pairs)
  238. {
  239. $kv = Auth_OpenID_KVForm::fromArray($pairs);
  240. /* Invalid association types should be caught at constructor */
  241. $callback = $this->_macs[$this->assoc_type];
  242. return call_user_func_array($callback, array($this->secret, $kv));
  243. }
  244. /**
  245. * Generate a signature for some fields in a dictionary
  246. *
  247. * @access private
  248. * @param array $fields The fields to sign, in order; this is an
  249. * array of strings.
  250. * @param array $data Dictionary of values to sign (an array of
  251. * string => string pairs).
  252. * @return string $signature The signature, base64 encoded
  253. */
  254. function signMessage($message)
  255. {
  256. if ($message->hasKey(Auth_OpenID_OPENID_NS, 'sig') ||
  257. $message->hasKey(Auth_OpenID_OPENID_NS, 'signed')) {
  258. // Already has a sig
  259. return null;
  260. }
  261. $extant_handle = $message->getArg(Auth_OpenID_OPENID_NS,
  262. 'assoc_handle');
  263. if ($extant_handle && ($extant_handle != $this->handle)) {
  264. // raise ValueError("Message has a different association handle")
  265. return null;
  266. }
  267. $signed_message = $message;
  268. $signed_message->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle',
  269. $this->handle);
  270. $message_keys = array_keys($signed_message->toPostArgs());
  271. $signed_list = array();
  272. $signed_prefix = 'openid.';
  273. foreach ($message_keys as $k) {
  274. if (strpos($k, $signed_prefix) === 0) {
  275. $signed_list[] = substr($k, strlen($signed_prefix));
  276. }
  277. }
  278. $signed_list[] = 'signed';
  279. sort($signed_list);
  280. $signed_message->setArg(Auth_OpenID_OPENID_NS, 'signed',
  281. implode(',', $signed_list));
  282. $sig = $this->getMessageSignature($signed_message);
  283. $signed_message->setArg(Auth_OpenID_OPENID_NS, 'sig', $sig);
  284. return $signed_message;
  285. }
  286. /**
  287. * Given a {@link Auth_OpenID_Message}, return the key/value pairs
  288. * to be signed according to the signed list in the message. If
  289. * the message lacks a signed list, return null.
  290. *
  291. * @access private
  292. */
  293. function _makePairs($message)
  294. {
  295. $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed');
  296. if (!$signed || Auth_OpenID::isFailure($signed)) {
  297. // raise ValueError('Message has no signed list: %s' % (message,))
  298. return null;
  299. }
  300. $signed_list = explode(',', $signed);
  301. $pairs = array();
  302. $data = $message->toPostArgs();
  303. foreach ($signed_list as $field) {
  304. $pairs[] = array($field, Auth_OpenID::arrayGet($data,
  305. 'openid.' .
  306. $field, ''));
  307. }
  308. return $pairs;
  309. }
  310. /**
  311. * Given an {@link Auth_OpenID_Message}, return the signature for
  312. * the signed list in the message.
  313. *
  314. * @access private
  315. */
  316. function getMessageSignature($message)
  317. {
  318. $pairs = $this->_makePairs($message);
  319. return base64_encode($this->sign($pairs));
  320. }
  321. /**
  322. * Confirm that the signature of these fields matches the
  323. * signature contained in the data.
  324. *
  325. * @access private
  326. */
  327. function checkMessageSignature($message)
  328. {
  329. $sig = $message->getArg(Auth_OpenID_OPENID_NS,
  330. 'sig');
  331. if (!$sig || Auth_OpenID::isFailure($sig)) {
  332. return false;
  333. }
  334. $calculated_sig = $this->getMessageSignature($message);
  335. return Auth_OpenID_CryptUtil::constEq($calculated_sig, $sig);
  336. }
  337. }
  338. function Auth_OpenID_getSecretSize($assoc_type)
  339. {
  340. if ($assoc_type == 'HMAC-SHA1') {
  341. return 20;
  342. } else if ($assoc_type == 'HMAC-SHA256') {
  343. return 32;
  344. } else {
  345. return null;
  346. }
  347. }
  348. function Auth_OpenID_getAllAssociationTypes()
  349. {
  350. return array('HMAC-SHA1', 'HMAC-SHA256');
  351. }
  352. function Auth_OpenID_getSupportedAssociationTypes()
  353. {
  354. $a = array('HMAC-SHA1');
  355. if (Auth_OpenID_HMACSHA256_SUPPORTED) {
  356. $a[] = 'HMAC-SHA256';
  357. }
  358. return $a;
  359. }
  360. function Auth_OpenID_getSessionTypes($assoc_type)
  361. {
  362. $assoc_to_session = array(
  363. 'HMAC-SHA1' => array('DH-SHA1', 'no-encryption'));
  364. if (Auth_OpenID_HMACSHA256_SUPPORTED) {
  365. $assoc_to_session['HMAC-SHA256'] =
  366. array('DH-SHA256', 'no-encryption');
  367. }
  368. return Auth_OpenID::arrayGet($assoc_to_session, $assoc_type, array());
  369. }
  370. function Auth_OpenID_checkSessionType($assoc_type, $session_type)
  371. {
  372. if (!in_array($session_type,
  373. Auth_OpenID_getSessionTypes($assoc_type))) {
  374. return false;
  375. }
  376. return true;
  377. }
  378. function Auth_OpenID_getDefaultAssociationOrder()
  379. {
  380. $order = array();
  381. if (!Auth_OpenID_noMathSupport()) {
  382. $order[] = array('HMAC-SHA1', 'DH-SHA1');
  383. if (Auth_OpenID_HMACSHA256_SUPPORTED) {
  384. $order[] = array('HMAC-SHA256', 'DH-SHA256');
  385. }
  386. }
  387. $order[] = array('HMAC-SHA1', 'no-encryption');
  388. if (Auth_OpenID_HMACSHA256_SUPPORTED) {
  389. $order[] = array('HMAC-SHA256', 'no-encryption');
  390. }
  391. return $order;
  392. }
  393. function Auth_OpenID_getOnlyEncryptedOrder()
  394. {
  395. $result = array();
  396. foreach (Auth_OpenID_getDefaultAssociationOrder() as $pair) {
  397. list($assoc, $session) = $pair;
  398. if ($session != 'no-encryption') {
  399. if (Auth_OpenID_HMACSHA256_SUPPORTED &&
  400. ($assoc == 'HMAC-SHA256')) {
  401. $result[] = $pair;
  402. } else if ($assoc != 'HMAC-SHA256') {
  403. $result[] = $pair;
  404. }
  405. }
  406. }
  407. return $result;
  408. }
  409. function Auth_OpenID_getDefaultNegotiator()
  410. {
  411. return new Auth_OpenID_SessionNegotiator(
  412. Auth_OpenID_getDefaultAssociationOrder());
  413. }
  414. function Auth_OpenID_getEncryptedNegotiator()
  415. {
  416. return new Auth_OpenID_SessionNegotiator(
  417. Auth_OpenID_getOnlyEncryptedOrder());
  418. }
  419. /**
  420. * A session negotiator controls the allowed and preferred association
  421. * types and association session types. Both the {@link
  422. * Auth_OpenID_Consumer} and {@link Auth_OpenID_Server} use
  423. * negotiators when creating associations.
  424. *
  425. * You can create and use negotiators if you:
  426. * - Do not want to do Diffie-Hellman key exchange because you use
  427. * transport-layer encryption (e.g. SSL)
  428. *
  429. * - Want to use only SHA-256 associations
  430. *
  431. * - Do not want to support plain-text associations over a non-secure
  432. * channel
  433. *
  434. * It is up to you to set a policy for what kinds of associations to
  435. * accept. By default, the library will make any kind of association
  436. * that is allowed in the OpenID 2.0 specification.
  437. *
  438. * Use of negotiators in the library
  439. * =================================
  440. *
  441. * When a consumer makes an association request, it calls {@link
  442. * getAllowedType} to get the preferred association type and
  443. * association session type.
  444. *
  445. * The server gets a request for a particular association/session type
  446. * and calls {@link isAllowed} to determine if it should create an
  447. * association. If it is supported, negotiation is complete. If it is
  448. * not, the server calls {@link getAllowedType} to get an allowed
  449. * association type to return to the consumer.
  450. *
  451. * If the consumer gets an error response indicating that the
  452. * requested association/session type is not supported by the server
  453. * that contains an assocation/session type to try, it calls {@link
  454. * isAllowed} to determine if it should try again with the given
  455. * combination of association/session type.
  456. *
  457. * @package OpenID
  458. */
  459. class Auth_OpenID_SessionNegotiator {
  460. function Auth_OpenID_SessionNegotiator($allowed_types)
  461. {
  462. $this->allowed_types = array();
  463. $this->setAllowedTypes($allowed_types);
  464. }
  465. /**
  466. * Set the allowed association types, checking to make sure each
  467. * combination is valid.
  468. *
  469. * @access private
  470. */
  471. function setAllowedTypes($allowed_types)
  472. {
  473. foreach ($allowed_types as $pair) {
  474. list($assoc_type, $session_type) = $pair;
  475. if (!Auth_OpenID_checkSessionType($assoc_type, $session_type)) {
  476. return false;
  477. }
  478. }
  479. $this->allowed_types = $allowed_types;
  480. return true;
  481. }
  482. /**
  483. * Add an association type and session type to the allowed types
  484. * list. The assocation/session pairs are tried in the order that
  485. * they are added.
  486. *
  487. * @access private
  488. */
  489. function addAllowedType($assoc_type, $session_type = null)
  490. {
  491. if ($this->allowed_types === null) {
  492. $this->allowed_types = array();
  493. }
  494. if ($session_type === null) {
  495. $available = Auth_OpenID_getSessionTypes($assoc_type);
  496. if (!$available) {
  497. return false;
  498. }
  499. foreach ($available as $session_type) {
  500. $this->addAllowedType($assoc_type, $session_type);
  501. }
  502. } else {
  503. if (Auth_OpenID_checkSessionType($assoc_type, $session_type)) {
  504. $this->allowed_types[] = array($assoc_type, $session_type);
  505. } else {
  506. return false;
  507. }
  508. }
  509. return true;
  510. }
  511. // Is this combination of association type and session type allowed?
  512. function isAllowed($assoc_type, $session_type)
  513. {
  514. $assoc_good = in_array(array($assoc_type, $session_type),
  515. $this->allowed_types);
  516. $matches = in_array($session_type,
  517. Auth_OpenID_getSessionTypes($assoc_type));
  518. return ($assoc_good && $matches);
  519. }
  520. /**
  521. * Get a pair of assocation type and session type that are
  522. * supported.
  523. */
  524. function getAllowedType()
  525. {
  526. if (!$this->allowed_types) {
  527. return array(null, null);
  528. }
  529. return $this->allowed_types[0];
  530. }
  531. }