123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- <?php
- /*
- * Copyright (c) 2014 David Gwynne <david@gwynne.id.au>
- *
- * Permission to use, copy, modify, and distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
- class HttpSignatureError extends Exception { };
- class ExpiredRequestError extends HttpSignatureError { };
- class InvalidHeaderError extends HttpSignatureError { };
- class InvalidParamsError extends HttpSignatureError { };
- class MissingHeaderError extends HttpSignatureError { };
- class InvalidAlgorithmError extends HttpSignatureError { };
- class HTTPSignature {
- static function parse(array $headers, array $options = array())
- {
- if (!array_key_exists('signature', $headers)) {
- throw new MissingHeaderError('no signature header in the request');
- }
- $auth = 'Signature '.$headers['signature'];
- if (!array_key_exists('headers', $options)) {
- $options['headers'] = array(isset($headers['x-date']) ? 'x-date' : 'date');
- } else {
- if (!is_array($options['headers'])) {
- throw new Exception('headers option is not an array');
- }
- if (sizeof(array_filter($options['headers'], function ($a) { return (!is_string($a)); }))) {
- throw new Exception('headers option is not an array of strings');
- }
- }
- if (!array_key_exists('clockSkew', $options)) {
- $options['clockSkew'] = 300;
- } elseif (!is_numeric($options['clockSkew'])) {
- throw new Exception('clockSkew option is not numeric');
- }
- if (array_key_exists('algorithms', $options)) {
- if (!is_array($options['algorithms'])) {
- throw new Exception('algorithms option is not an array');
- }
- if (sizeof(array_filter($options['algorithms'], function ($a) { return (!is_string($a)); }))) {
- throw new Exception('algorithms option is not an array of strings');
- }
- }
- $headers['request-line'] = array_key_exists('requestLine', $options) ?
- $options['requestLine'] :
- sprintf("%s %s %s", $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']);
- foreach ($options['headers'] as $header) {
- if (!array_key_exists($header, $headers)) {
- throw new MissingHeaderError("$header was not in the request");
- }
- }
- $states = array(
- 'start' => 0,
- 'scheme' => 1,
- 'space' => 2,
- 'param' => 3,
- 'quote' => 4,
- 'value' => 5,
- 'comma' => 6
- );
- $scheme = '';
- $params = array();
- $param = '';
- $value = '';
- $state = $states['start'];
- for ($i = 0; $i < strlen($auth); $i++) {
- $ch = $auth[$i];
- switch ($state) {
- case $states['start']:
- if (ctype_space($ch)) {
- break;
- }
- $state = $states['scheme'];
- /* FALLTHROUGH */
- case $states['scheme']:
- if (ctype_space($ch)) {
- $state = $states['space'];
- } else {
- $scheme .= $ch;
- }
- break;
- case $states['space'];
- if (ctype_space($ch)) {
- continue;
- }
- $state = $states['param'];
- /* FALLTHROUGH */
- case $states['param']:
- if ($ch === '=') {
- if ($param === '') {
- throw new InvalidHeaderError('bad param name');
- }
- if (array_key_exists($param, $params)) {
- throw new InvalidHeaderError('param specified again');
- }
- $state = $states['quote'];
- break;
- }
- if (!ctype_alpha($ch)) {
- throw new InvalidHeaderError('bad param format');
- }
- $param .= $ch;
- break;
- case $states['quote'];
- if ($ch !== '"') {
- throw new InvalidHeaderError('bad param format');
- }
- $state = $states['value'];
- break;
- case $states['value']:
- if ($ch === '"') {
- $params[$param] = $value;
- $param = '';
- $value = '';
- $state = $states['comma'];
- break;
- }
- $value .= $ch;
- break;
- case $states['comma']:
- if ($ch !== ',') {
- throw new InvalidHeaderError('bad param format');
- }
- $state = $states['param'];
- break;
- default:
- throw new Error('invalid state');
- }
- }
- if ($state !== $states['comma']) {
- throw new InvalidHeaderError("bad param format");
- }
- if ($scheme !== 'Signature') {
- throw new InvalidHeaderError('scheme was not "Signature"');
- }
- $required = array('keyId', 'algorithm', 'signature');
- foreach ($required as $param) {
- if (!array_key_exists($param, $params)) {
- throw new InvalidHeaderError("$param was not specified");
- }
- }
- if (array_key_exists('headers', $params)) {
- $params['headers'] = explode(' ', $params['headers']);
- } else {
- $params['headers'] = array(isset($headers['x-date']) ? 'x-date' : 'date');
- }
- foreach ($options['headers'] as $header) {
- if (!in_array($header, $params['headers'])) {
- throw new MissingHeaderError("$header was not a signed header");
- }
- }
- if (isset($options['algorithms']) && !in_array($params['algorithm'], $options['algorithms'])) {
- throw new InvalidParamsError($params['algorithm'] . " is not a supported algorithm");
- }
- $date = null;
- if (isset($headers['date'])) {
- $date = strtotime($headers['date']);
- } elseif (isset($headers['x-date'])) {
- $date = strtotime($headers['x-date']);
- }
- if (!is_null($date)) {
- if ($date === FALSE) {
- throw new InvalidHeaderError('unable to parse date header');
- }
- $skew = abs(time() - $date);
- if ($skew > $options['clockSkew']) {
- throw new ExpiredRequestError(sprintf("clock skew of %ds was greater than %ds", $skew, $options['clockSkew']));
- }
- }
- $sign = array();
- foreach ($params['headers'] as $header) {
- $sign[] = $header === 'request-line' ? $headers['request-line'] : sprintf("%s: %s", $header, $headers[$header]);
- }
- return (array('scheme' => $scheme, 'params' => $params, 'signingString' => implode("\n", $sign)));
- }
- static function verify(array $res, $key, $keytype)
- {
- if (!is_string($key)) {
- throw new Exception('key is not a string');
- }
- $alg = explode('-', $res['params']['algorithm'], 2);
- if (sizeof($alg) != 2) {
- throw new InvalidAlgorithmError("unsupported algorithm");
- }
- if ($alg[0] != $keytype) {
- throw new InvalidAlgorithmError("algorithm type doesn't match key type");
- }
- switch ($alg[0]) {
- case 'rsa':
- $map = array('sha1' => OPENSSL_ALGO_SHA1, 'sha256' => OPENSSL_ALGO_SHA256, 'sha512' => OPENSSL_ALGO_SHA512);
- if (!array_key_exists($alg[1], $map)) {
- throw new InvalidAlgorithmError('unsupported algorithm');
- }
- $pkey = openssl_get_publickey($key);
- if ($pkey === FALSE) {
- throw new Exception('key could not be parsed');
- }
- $rv = openssl_verify($res['signingString'], base64_decode($res['params']['signature']), $pkey, $map[$alg[1]]);
- openssl_free_key($pkey);
- switch ($rv) {
- case 0:
- return (FALSE);
- case 1:
- return (TRUE);
- default:
- throw new Exception('key could not be verified');
- }
- break;
- case 'hmac':
- return (hash_hmac($alg[1], $res['signingString'], $key, true) === base64_decode($res['params']['signature']));
- break;
- default:
- throw new InvalidAlgorithmError("unsupported algorithm");
- }
- }
- static function sign(&$headers = array(), array $options = array())
- {
- if (is_null($headers)) {
- $headers = array();
- } elseif (!is_array($headers)) {
- throw new Exception('headers are not an array');
- }
- if (!array_key_exists('keyId', $options)) {
- throw new Exception('keyId option is missing');
- } elseif (!is_string($options['keyId'])) {
- throw new Exception('keyId option is not a string');
- }
- if (!array_key_exists('key', $options)) {
- throw new Exception('key option is missing');
- } elseif (!is_string($options['key'])) {
- throw new Exception('key option is not a string');
- }
- if (!array_key_exists('headers', $options)) {
- $options['headers'] = array('date');
- } else {
- if (!is_array($options['headers'])) {
- throw new Exception('headers option is not an array');
- }
- if (sizeof(array_filter($options['headers'], function ($a) { return (!is_string($a)); }))) {
- throw new Exception('headers option is not an array of strings');
- }
- }
- if (!array_key_exists('algorithm', $options)) {
- $options['algorithm'] = 'rsa-sha256';
- }
- if (!array_key_exists('date', $headers)) {
- $headers['date'] = date(DATE_RFC1123);
- }
- $headers['request-line'] = array_key_exists('requestLine', $options) ?
- $options['requestLine'] :
- sprintf("%s %s %s", $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']);
- $sign = array();
- foreach ($options['headers'] as $header) {
- if (!array_key_exists($header, $headers)) {
- throw new MissingHeaderError("$header was not in the request");
- }
- $sign[] = $header === 'request-line' ? $headers['request-line'] : sprintf("%s: %s", $header, $headers[$header]);
- }
- $data = join("\n", $sign);
- $alg = explode('-', $options['algorithm'], 2);
- if (sizeof($alg) != 2) {
- throw new InvalidAlgorithmError("unsupported algorithm");
- }
- switch ($alg[0]) {
- case 'rsa':
- $map = array('sha1' => OPENSSL_ALGO_SHA1, 'sha256' => OPENSSL_ALGO_SHA256, 'sha512' => OPENSSL_ALGO_SHA512);
- if (!array_key_exists($alg[1], $map)) {
- throw new InvalidAlgorithmError('unsupported algorithm');
- }
- $key = openssl_get_privatekey($options['key']);
- if ($key === FALSE) {
- error_log(openssl_error_string());
- throw new Exception('key option could not be parsed');
- }
- if (openssl_sign($data, $signature, $key, $map[$alg[1]]) === FALSE) {
- throw new Exception('unable to sign');
- }
- break;
- case 'hmac':
- $signature = hash_hmac($alg[1], $data, $options['key'], true);
- break;
- default:
- throw new InvalidAlgorithmError("unsupported algorithm");
- }
- unset($headers['request-line']);
- $headers['authorization'] = sprintf('Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"',
- $options['keyId'], $options['algorithm'], implode(' ', $options['headers']),
- base64_encode($signature));
- }
- }
|