* * 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)); } }