PHP OpenID consumer
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

keymanager.inc.php 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. <?PHP
  2. /* Poidsy 0.6 - http://chris.smith.name/projects/poidsy
  3. * Copyright (c) 2008-2010 Chris Smith
  4. *
  5. * Permission is hereby granted, free of charge, to any person obtaining a copy
  6. * of this software and associated documentation files (the "Software"), to deal
  7. * in the Software without restriction, including without limitation the rights
  8. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. * copies of the Software, and to permit persons to whom the Software is
  10. * furnished to do so, subject to the following conditions:
  11. *
  12. * The above copyright notice and this permission notice shall be included in
  13. * all copies or substantial portions of the Software.
  14. *
  15. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. * SOFTWARE.
  22. */
  23. require_once(dirname(__FILE__) . '/bigmath.inc.php');
  24. require_once(dirname(__FILE__) . '/logging.inc.php');
  25. require_once(dirname(__FILE__) . '/poster.inc.php');
  26. require_once(dirname(__FILE__) . '/urlbuilder.inc.php');
  27. class KeyManager {
  28. /** Diffie-Hellman P value, defined by OpenID specification. */
  29. const DH_P = '155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443';
  30. /** Diffie-Hellman G value. */
  31. const DH_G = '2';
  32. private static $header = null;
  33. private static $data = null;
  34. private static $bigmath = null;
  35. /**
  36. * Loads the KeyManager's data array from disk.
  37. */
  38. private static function loadData() {
  39. if (self::$data == null) {
  40. Logger::log('Loading data from %s/keycache.php', dirname(__FILE__));
  41. $data = file(dirname(__FILE__) . '/keycache.php');
  42. self::$header = array_shift($data);
  43. self::$data = unserialize(implode("\n", $data));
  44. }
  45. }
  46. /**
  47. * Saves the KeyManager's data array to disk.
  48. */
  49. private static function saveData() {
  50. Logger::log('Saving data to %s/keycache.php', dirname(__FILE__));
  51. file_put_contents(dirname(__FILE__) . '/keycache.php', self::$header . serialize(self::$data));
  52. }
  53. /**
  54. * Attempts to associate with the specified server.
  55. *
  56. * @param String $server The server to associate with
  57. */
  58. public static function associate($server, $assocType = null, $sessionType = null) {
  59. Logger::log('Attempting to associate with %s, assocType: %s, sessionType: %s', $server, $assocType, $sessionType);
  60. $data = URLBuilder::buildAssociate($server, $_SESSION['openid']['version'], $assocType, $sessionType);
  61. try {
  62. $res = Poster::post($server, $data);
  63. } catch (Exception $ex) {
  64. Logger::log('Exception while posting: %s', $ex->getMessage());
  65. return;
  66. }
  67. $data = array();
  68. foreach (explode("\n", $res) as $line) {
  69. if (preg_match('/^(.*?):(.*)$/', $line, $m)) {
  70. $data[$m[1]] = $m[2];
  71. }
  72. }
  73. if (isset($data['error_code']) && $data['error_code'] == 'unsupported-type') {
  74. $cont = false;
  75. if (isset($data['session_type']) && $data['session_type'] != $sessionType) {
  76. // TODO: Check we support it before actually trying
  77. $sessionType = $data['session_type'];
  78. $cont = true;
  79. }
  80. if (isset($data['assoc_type']) && $data['assoc_type'] != $assocType) {
  81. $assocType = $data['assoc_type'];
  82. $cont = true;
  83. }
  84. if ($cont) {
  85. self::associate($server, $assocType, $sessionType);
  86. }
  87. return;
  88. }
  89. try {
  90. $data = self::decodeKey($server, $data);
  91. } catch (Exception $ex) {
  92. return;
  93. }
  94. $data['expires_at'] = time() + $data['expires_in'];
  95. self::$data[$server][$data['assoc_handle']] = $data;
  96. self::saveData();
  97. }
  98. /**
  99. * Decodes the MAC key specified in the $data array.
  100. *
  101. * @param String $server The server which sent the data
  102. * @param Array $data Array of association data from the server
  103. * @return A copy of the $data array with the mac_key present
  104. */
  105. private static function decodeKey($server, $data) {
  106. switch (strtolower($data['session_type'])) {
  107. case 'dh-sha1':
  108. $algo = 'sha1';
  109. break;
  110. case 'dh-sha256':
  111. $algo = 'sha256';
  112. break;
  113. case 'no-encryption':
  114. case 'blank':
  115. case '':
  116. $algo = false;
  117. break;
  118. default:
  119. throw new Exception('Unable to handle session type ' . $data['session_type']);
  120. }
  121. if ($algo !== false) {
  122. // The key is DH'd
  123. $mac = base64_decode($data['enc_mac_key']);
  124. $x = self::getDhPrivateKey($server);
  125. $temp = self::$bigmath->btwoc_undo(base64_decode($data['dh_server_public']));
  126. $temp = self::$bigmath->powmod($temp, $x, self::DH_P);
  127. $temp = self::$bigmath->btwoc($temp);
  128. $temp = hash($algo, $temp, true);
  129. $mac = $mac ^ $temp;
  130. $data['mac_key'] = base64_encode($mac);
  131. unset($data['enc_mac_key'], $data['dh_server_public']);
  132. }
  133. return $data;
  134. }
  135. /**
  136. * Retrieves an active assoc_handle for the specified server.
  137. *
  138. * @param String $server The server whose handle we're looking for
  139. * @return An association handle for the server or null on failure
  140. */
  141. public static function getHandle($server) {
  142. self::loadData();
  143. if (!isset(self::$data[$server])) {
  144. Logger::log('No data found for %s', $server);
  145. return null;
  146. }
  147. foreach (self::$data[$server] as $handle => $data) {
  148. if ($handle == '__private') { continue; }
  149. if ($data['expires_at'] < time()) {
  150. Logger::log('Handle for %s expired at %s, unsetting', $server, $data['expires_at']);
  151. unset(self::$data[$server][$handle]);
  152. } else {
  153. return $handle;
  154. }
  155. }
  156. return null;
  157. }
  158. /**
  159. * Determines if the KeyManager has at least one assoc_handle for the
  160. * specified server.
  161. *
  162. * @param String $server The server to check for
  163. * @return True if the KeyManager has a handle, false otherwise
  164. */
  165. public static function hasHandle($server) {
  166. return self::getHandle($server) !== null;
  167. }
  168. /**
  169. * Retrieves the association data array for the specified server and assoc
  170. * handle.
  171. *
  172. * @param String $server The server whose data is being requested
  173. * @param String $handle The current association handle for the server
  174. * @return Array of association data or null if none was found
  175. */
  176. public static function getData($server, $handle) {
  177. self::loadData();
  178. if (isset(self::$data[$server][$handle])) {
  179. if (self::$data[$server][$handle]['expires_at'] < time()) {
  180. self::revokeHandle($server, $handle);
  181. return null;
  182. } else {
  183. return self::$data[$server][$handle];
  184. }
  185. } else {
  186. return null;
  187. }
  188. }
  189. /**
  190. * Attempts to authenticate that the specified arguments are a valid query
  191. * from the specified server. If smart authentication is not available
  192. * or an error is encountered, throws an exception.
  193. *
  194. * @param String $server The server that supposedly sent the request
  195. * @param Array $args The arguments included in the request
  196. * @return True if the message was authenticated, false if it's a fake
  197. */
  198. public static function authenticate($server, $args) {
  199. Logger::log('Authenticating message from %s', $server);
  200. $data = self::getData($server, $args['openid_assoc_handle']);
  201. if ($data === null) {
  202. Logger::log('Can\'t authenticate, no key available');
  203. throw new Exception('No key available for that server/handle');
  204. }
  205. $contents = '';
  206. foreach (explode(',', $args['openid_signed']) as $arg) {
  207. $argn = str_replace('.', '_', $arg);
  208. $contents .= $arg . ':' . $args['openid_' . $argn] . "\n";
  209. }
  210. switch (strtolower($data['assoc_type'])) {
  211. case 'hmac-sha1':
  212. $algo = 'sha1';
  213. break;
  214. case 'hmac-sha256':
  215. $algo = 'sha256';
  216. break;
  217. default:
  218. Logger::log('Can\'t authenticate, unknown assoc type %s', $data['assoc_type']);
  219. throw new Exception('Unable to handle association type ' . $data['assoc_type']);
  220. }
  221. $sig = base64_encode(hash_hmac($algo, $contents, base64_decode($data['mac_key']), true));
  222. Logger::log('Expected signature %s, received signature %s', $sig, $args['openid_sig']);
  223. // Manually compare characters to prevent timing attacks
  224. $res = strlen($sig) == strlen($args['openid_sig']);
  225. for ($i = 0; $i < strlen($sig); $i++) {
  226. $res &= $sig[$i] == $args['openid_sig'][$i];
  227. }
  228. Logger::log('Authentication result: %s', $res ? 'good' : 'bad');
  229. return $res;
  230. }
  231. /**
  232. * Validates the current request using dumb authentication (a POST to the
  233. * provider).
  234. *
  235. * @return True if the request has been authenticated, false otherwise.
  236. */
  237. public static function dumbAuthenticate() {
  238. Logger::log('Performing dumb authentication');
  239. $url = URLBuilder::buildAuth($_REQUEST, $_SESSION['openid']['version']);
  240. try {
  241. $data = Poster::post($_SESSION['openid']['endpointUrl'], $url);
  242. } catch (Exception $ex) {
  243. return false;
  244. }
  245. $valid = false;
  246. foreach (explode("\n", $data) as $line) {
  247. if (substr($line, 0, 9) == 'is_valid:') {
  248. $valid = (boolean) substr($line, 9);
  249. }
  250. }
  251. Logger::log('Authentication result: %s', $valid ? 'good' : 'bad');
  252. return $valid;
  253. }
  254. /**
  255. * Removes the specified association handle from the specified server's
  256. * records.
  257. *
  258. * @param String $server The server which is revoking the handle
  259. * @param String $handle The handle which is being revoked
  260. */
  261. public static function revokeHandle($server, $handle) {
  262. self::loadData();
  263. unset(self::$data[$server][$handle]);
  264. self::saveData();
  265. }
  266. /**
  267. * Determines if the keymanager is supported by the local environment or not.
  268. *
  269. * @return True if the keymanager can be used, false otherwise
  270. */
  271. public static function isSupported() {
  272. return @is_writable(dirname(__FILE__) . '/keycache.php')
  273. && function_exists('hash_hmac');
  274. }
  275. /**
  276. * Returns the base64-encoded representation of the dh_modulus parameter.
  277. *
  278. * @return Base64-encoded representation of dh_modulus
  279. */
  280. public static function getDhModulus() {
  281. return base64_encode(self::$bigmath->btwoc(self::DH_P));
  282. }
  283. /**
  284. * Returns the base64-encoded representation of the dh_gen parameter.
  285. *
  286. * @return Base64-encoded representation of dh_gen
  287. */
  288. public static function getDhGen() {
  289. return base64_encode(self::$bigmath->btwoc(self::DH_G));
  290. }
  291. /**
  292. * Retrieves our private key for the specified server.
  293. *
  294. * @param String $server The server which we're communicating with
  295. * @return Our private key for the specified server, or null if we don't have
  296. * one
  297. */
  298. public static function getDhPrivateKey($server) {
  299. self::loadData();
  300. if (isset(self::$data[$server])) {
  301. return self::$data[$server]['__private'];
  302. } else {
  303. return null;
  304. }
  305. }
  306. /**
  307. * Retrieves our public key for use with the specified server.
  308. *
  309. * @param String $server The server we wish to send the public key to
  310. * @return Base64-encoded public key for the specified server
  311. */
  312. public static function getDhPublicKey($server) {
  313. self::loadData();
  314. $key = self::createDhKey($server);
  315. self::saveData();
  316. return base64_encode(self::$bigmath->btwoc(self::$bigmath->powmod(self::DH_G, $key, self::DH_P)));
  317. }
  318. /**
  319. * Creates a private key for use when exchanging keys with the specified
  320. * server.
  321. *
  322. * @param String $server The name of the server we're dealing with
  323. * @return The server's new private key
  324. */
  325. private static function createDhKey($server) {
  326. return self::$data[$server]['__private'] = self::$bigmath->rand(self::DH_P);
  327. }
  328. /**
  329. * Determines whether the keymanager can support Diffie-Hellman key exchange.
  330. *
  331. * @return True if D-H exchange is supported, false otherwise.
  332. */
  333. public static function supportsDH() {
  334. return self::$bigmath != null;
  335. }
  336. /**
  337. * Initialises the key manager.
  338. */
  339. public static function init() {
  340. self::$bigmath = BigMath::getBigMath();
  341. }
  342. }
  343. KeyManager::init();
  344. ?>