http-signature-auth.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. <?php
  2. /*
  3. * Copyright (c) 2014 David Gwynne <david@gwynne.id.au>
  4. *
  5. * Permission to use, copy, modify, and distribute this software for any
  6. * purpose with or without fee is hereby granted, provided that the above
  7. * copyright notice and this permission notice appear in all copies.
  8. *
  9. * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  10. * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  11. * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  12. * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  13. * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  14. * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  15. * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  16. */
  17. class HttpSignatureError extends Exception { };
  18. class ExpiredRequestError extends HttpSignatureError { };
  19. class InvalidHeaderError extends HttpSignatureError { };
  20. class InvalidParamsError extends HttpSignatureError { };
  21. class MissingHeaderError extends HttpSignatureError { };
  22. class InvalidAlgorithmError extends HttpSignatureError { };
  23. class HTTPSignature {
  24. static function parse(array $headers, array $options = array())
  25. {
  26. if (!array_key_exists('signature', $headers)) {
  27. throw new MissingHeaderError('no signature header in the request');
  28. }
  29. $auth = 'Signature '.$headers['signature'];
  30. if (!array_key_exists('headers', $options)) {
  31. $options['headers'] = array(isset($headers['x-date']) ? 'x-date' : 'date');
  32. } else {
  33. if (!is_array($options['headers'])) {
  34. throw new Exception('headers option is not an array');
  35. }
  36. if (sizeof(array_filter($options['headers'], function ($a) { return (!is_string($a)); }))) {
  37. throw new Exception('headers option is not an array of strings');
  38. }
  39. }
  40. if (!array_key_exists('clockSkew', $options)) {
  41. $options['clockSkew'] = 300;
  42. } elseif (!is_numeric($options['clockSkew'])) {
  43. throw new Exception('clockSkew option is not numeric');
  44. }
  45. if (array_key_exists('algorithms', $options)) {
  46. if (!is_array($options['algorithms'])) {
  47. throw new Exception('algorithms option is not an array');
  48. }
  49. if (sizeof(array_filter($options['algorithms'], function ($a) { return (!is_string($a)); }))) {
  50. throw new Exception('algorithms option is not an array of strings');
  51. }
  52. }
  53. $headers['request-line'] = array_key_exists('requestLine', $options) ?
  54. $options['requestLine'] :
  55. sprintf("%s %s %s", $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']);
  56. foreach ($options['headers'] as $header) {
  57. if (!array_key_exists($header, $headers)) {
  58. throw new MissingHeaderError("$header was not in the request");
  59. }
  60. }
  61. $states = array(
  62. 'start' => 0,
  63. 'scheme' => 1,
  64. 'space' => 2,
  65. 'param' => 3,
  66. 'quote' => 4,
  67. 'value' => 5,
  68. 'comma' => 6
  69. );
  70. $scheme = '';
  71. $params = array();
  72. $param = '';
  73. $value = '';
  74. $state = $states['start'];
  75. for ($i = 0; $i < strlen($auth); $i++) {
  76. $ch = $auth[$i];
  77. switch ($state) {
  78. case $states['start']:
  79. if (ctype_space($ch)) {
  80. break;
  81. }
  82. $state = $states['scheme'];
  83. /* FALLTHROUGH */
  84. case $states['scheme']:
  85. if (ctype_space($ch)) {
  86. $state = $states['space'];
  87. } else {
  88. $scheme .= $ch;
  89. }
  90. break;
  91. case $states['space'];
  92. if (ctype_space($ch)) {
  93. continue;
  94. }
  95. $state = $states['param'];
  96. /* FALLTHROUGH */
  97. case $states['param']:
  98. if ($ch === '=') {
  99. if ($param === '') {
  100. throw new InvalidHeaderError('bad param name');
  101. }
  102. if (array_key_exists($param, $params)) {
  103. throw new InvalidHeaderError('param specified again');
  104. }
  105. $state = $states['quote'];
  106. break;
  107. }
  108. if (!ctype_alpha($ch)) {
  109. throw new InvalidHeaderError('bad param format');
  110. }
  111. $param .= $ch;
  112. break;
  113. case $states['quote'];
  114. if ($ch !== '"') {
  115. throw new InvalidHeaderError('bad param format');
  116. }
  117. $state = $states['value'];
  118. break;
  119. case $states['value']:
  120. if ($ch === '"') {
  121. $params[$param] = $value;
  122. $param = '';
  123. $value = '';
  124. $state = $states['comma'];
  125. break;
  126. }
  127. $value .= $ch;
  128. break;
  129. case $states['comma']:
  130. if ($ch !== ',') {
  131. throw new InvalidHeaderError('bad param format');
  132. }
  133. $state = $states['param'];
  134. break;
  135. default:
  136. throw new Error('invalid state');
  137. }
  138. }
  139. if ($state !== $states['comma']) {
  140. throw new InvalidHeaderError("bad param format");
  141. }
  142. if ($scheme !== 'Signature') {
  143. throw new InvalidHeaderError('scheme was not "Signature"');
  144. }
  145. $required = array('keyId', 'algorithm', 'signature');
  146. foreach ($required as $param) {
  147. if (!array_key_exists($param, $params)) {
  148. throw new InvalidHeaderError("$param was not specified");
  149. }
  150. }
  151. if (array_key_exists('headers', $params)) {
  152. $params['headers'] = explode(' ', $params['headers']);
  153. } else {
  154. $params['headers'] = array(isset($headers['x-date']) ? 'x-date' : 'date');
  155. }
  156. foreach ($options['headers'] as $header) {
  157. if (!in_array($header, $params['headers'])) {
  158. throw new MissingHeaderError("$header was not a signed header");
  159. }
  160. }
  161. if (isset($options['algorithms']) && !in_array($params['algorithm'], $options['algorithms'])) {
  162. throw new InvalidParamsError($params['algorithm'] . " is not a supported algorithm");
  163. }
  164. $date = null;
  165. if (isset($headers['date'])) {
  166. $date = strtotime($headers['date']);
  167. } elseif (isset($headers['x-date'])) {
  168. $date = strtotime($headers['x-date']);
  169. }
  170. if (!is_null($date)) {
  171. if ($date === FALSE) {
  172. throw new InvalidHeaderError('unable to parse date header');
  173. }
  174. $skew = abs(time() - $date);
  175. if ($skew > $options['clockSkew']) {
  176. throw new ExpiredRequestError(sprintf("clock skew of %ds was greater than %ds", $skew, $options['clockSkew']));
  177. }
  178. }
  179. $sign = array();
  180. foreach ($params['headers'] as $header) {
  181. $sign[] = $header === 'request-line' ? $headers['request-line'] : sprintf("%s: %s", $header, $headers[$header]);
  182. }
  183. return (array('scheme' => $scheme, 'params' => $params, 'signingString' => implode("\n", $sign)));
  184. }
  185. static function verify(array $res, $key, $keytype)
  186. {
  187. if (!is_string($key)) {
  188. throw new Exception('key is not a string');
  189. }
  190. $alg = explode('-', $res['params']['algorithm'], 2);
  191. if (sizeof($alg) != 2) {
  192. throw new InvalidAlgorithmError("unsupported algorithm");
  193. }
  194. if ($alg[0] != $keytype) {
  195. throw new InvalidAlgorithmError("algorithm type doesn't match key type");
  196. }
  197. switch ($alg[0]) {
  198. case 'rsa':
  199. $map = array('sha1' => OPENSSL_ALGO_SHA1, 'sha256' => OPENSSL_ALGO_SHA256, 'sha512' => OPENSSL_ALGO_SHA512);
  200. if (!array_key_exists($alg[1], $map)) {
  201. throw new InvalidAlgorithmError('unsupported algorithm');
  202. }
  203. $pkey = openssl_get_publickey($key);
  204. if ($pkey === FALSE) {
  205. throw new Exception('key could not be parsed');
  206. }
  207. $rv = openssl_verify($res['signingString'], base64_decode($res['params']['signature']), $pkey, $map[$alg[1]]);
  208. openssl_free_key($pkey);
  209. switch ($rv) {
  210. case 0:
  211. return (FALSE);
  212. case 1:
  213. return (TRUE);
  214. default:
  215. throw new Exception('key could not be verified');
  216. }
  217. break;
  218. case 'hmac':
  219. return (hash_hmac($alg[1], $res['signingString'], $key, true) === base64_decode($res['params']['signature']));
  220. break;
  221. default:
  222. throw new InvalidAlgorithmError("unsupported algorithm");
  223. }
  224. }
  225. static function sign(&$headers = array(), array $options = array())
  226. {
  227. if (is_null($headers)) {
  228. $headers = array();
  229. } elseif (!is_array($headers)) {
  230. throw new Exception('headers are not an array');
  231. }
  232. if (!array_key_exists('keyId', $options)) {
  233. throw new Exception('keyId option is missing');
  234. } elseif (!is_string($options['keyId'])) {
  235. throw new Exception('keyId option is not a string');
  236. }
  237. if (!array_key_exists('key', $options)) {
  238. throw new Exception('key option is missing');
  239. } elseif (!is_string($options['key'])) {
  240. throw new Exception('key option is not a string');
  241. }
  242. if (!array_key_exists('headers', $options)) {
  243. $options['headers'] = array('date');
  244. } else {
  245. if (!is_array($options['headers'])) {
  246. throw new Exception('headers option is not an array');
  247. }
  248. if (sizeof(array_filter($options['headers'], function ($a) { return (!is_string($a)); }))) {
  249. throw new Exception('headers option is not an array of strings');
  250. }
  251. }
  252. if (!array_key_exists('algorithm', $options)) {
  253. $options['algorithm'] = 'rsa-sha256';
  254. }
  255. if (!array_key_exists('date', $headers)) {
  256. $headers['date'] = date(DATE_RFC1123);
  257. }
  258. $headers['request-line'] = array_key_exists('requestLine', $options) ?
  259. $options['requestLine'] :
  260. sprintf("%s %s %s", $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']);
  261. $sign = array();
  262. foreach ($options['headers'] as $header) {
  263. if (!array_key_exists($header, $headers)) {
  264. throw new MissingHeaderError("$header was not in the request");
  265. }
  266. $sign[] = $header === 'request-line' ? $headers['request-line'] : sprintf("%s: %s", $header, $headers[$header]);
  267. }
  268. $data = join("\n", $sign);
  269. $alg = explode('-', $options['algorithm'], 2);
  270. if (sizeof($alg) != 2) {
  271. throw new InvalidAlgorithmError("unsupported algorithm");
  272. }
  273. switch ($alg[0]) {
  274. case 'rsa':
  275. $map = array('sha1' => OPENSSL_ALGO_SHA1, 'sha256' => OPENSSL_ALGO_SHA256, 'sha512' => OPENSSL_ALGO_SHA512);
  276. if (!array_key_exists($alg[1], $map)) {
  277. throw new InvalidAlgorithmError('unsupported algorithm');
  278. }
  279. $key = openssl_get_privatekey($options['key']);
  280. if ($key === FALSE) {
  281. error_log(openssl_error_string());
  282. throw new Exception('key option could not be parsed');
  283. }
  284. if (openssl_sign($data, $signature, $key, $map[$alg[1]]) === FALSE) {
  285. throw new Exception('unable to sign');
  286. }
  287. break;
  288. case 'hmac':
  289. $signature = hash_hmac($alg[1], $data, $options['key'], true);
  290. break;
  291. default:
  292. throw new InvalidAlgorithmError("unsupported algorithm");
  293. }
  294. unset($headers['request-line']);
  295. $headers['authorization'] = sprintf('Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"',
  296. $options['keyId'], $options['algorithm'], implode(' ', $options['headers']),
  297. base64_encode($signature));
  298. }
  299. }