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.

315 lines
9.6 KiB

  1. // Copyright 2012 Joyent, Inc. All rights reserved.
  2. var assert = require('assert-plus');
  3. var util = require('util');
  4. var utils = require('./utils');
  5. ///--- Globals
  6. var HASH_ALGOS = utils.HASH_ALGOS;
  7. var PK_ALGOS = utils.PK_ALGOS;
  8. var HttpSignatureError = utils.HttpSignatureError;
  9. var InvalidAlgorithmError = utils.InvalidAlgorithmError;
  10. var validateAlgorithm = utils.validateAlgorithm;
  11. var State = {
  12. New: 0,
  13. Params: 1
  14. };
  15. var ParamsState = {
  16. Name: 0,
  17. Quote: 1,
  18. Value: 2,
  19. Comma: 3
  20. };
  21. ///--- Specific Errors
  22. function ExpiredRequestError(message) {
  23. HttpSignatureError.call(this, message, ExpiredRequestError);
  24. }
  25. util.inherits(ExpiredRequestError, HttpSignatureError);
  26. function InvalidHeaderError(message) {
  27. HttpSignatureError.call(this, message, InvalidHeaderError);
  28. }
  29. util.inherits(InvalidHeaderError, HttpSignatureError);
  30. function InvalidParamsError(message) {
  31. HttpSignatureError.call(this, message, InvalidParamsError);
  32. }
  33. util.inherits(InvalidParamsError, HttpSignatureError);
  34. function MissingHeaderError(message) {
  35. HttpSignatureError.call(this, message, MissingHeaderError);
  36. }
  37. util.inherits(MissingHeaderError, HttpSignatureError);
  38. function StrictParsingError(message) {
  39. HttpSignatureError.call(this, message, StrictParsingError);
  40. }
  41. util.inherits(StrictParsingError, HttpSignatureError);
  42. ///--- Exported API
  43. module.exports = {
  44. /**
  45. * Parses the 'Authorization' header out of an http.ServerRequest object.
  46. *
  47. * Note that this API will fully validate the Authorization header, and throw
  48. * on any error. It will not however check the signature, or the keyId format
  49. * as those are specific to your environment. You can use the options object
  50. * to pass in extra constraints.
  51. *
  52. * As a response object you can expect this:
  53. *
  54. * {
  55. * "scheme": "Signature",
  56. * "params": {
  57. * "keyId": "foo",
  58. * "algorithm": "rsa-sha256",
  59. * "headers": [
  60. * "date" or "x-date",
  61. * "digest"
  62. * ],
  63. * "signature": "base64"
  64. * },
  65. * "signingString": "ready to be passed to crypto.verify()"
  66. * }
  67. *
  68. * @param {Object} request an http.ServerRequest.
  69. * @param {Object} options an optional options object with:
  70. * - clockSkew: allowed clock skew in seconds (default 300).
  71. * - headers: required header names (def: date or x-date)
  72. * - algorithms: algorithms to support (default: all).
  73. * - strict: should enforce latest spec parsing
  74. * (default: false).
  75. * @return {Object} parsed out object (see above).
  76. * @throws {TypeError} on invalid input.
  77. * @throws {InvalidHeaderError} on an invalid Authorization header error.
  78. * @throws {InvalidParamsError} if the params in the scheme are invalid.
  79. * @throws {MissingHeaderError} if the params indicate a header not present,
  80. * either in the request headers from the params,
  81. * or not in the params from a required header
  82. * in options.
  83. * @throws {StrictParsingError} if old attributes are used in strict parsing
  84. * mode.
  85. * @throws {ExpiredRequestError} if the value of date or x-date exceeds skew.
  86. */
  87. parseRequest: function parseRequest(request, options) {
  88. assert.object(request, 'request');
  89. assert.object(request.headers, 'request.headers');
  90. if (options === undefined) {
  91. options = {};
  92. }
  93. if (options.headers === undefined) {
  94. options.headers = [request.headers['x-date'] ? 'x-date' : 'date'];
  95. }
  96. assert.object(options, 'options');
  97. assert.arrayOfString(options.headers, 'options.headers');
  98. assert.optionalFinite(options.clockSkew, 'options.clockSkew');
  99. var authzHeaderName = options.authorizationHeaderName || 'authorization';
  100. if (!request.headers[authzHeaderName]) {
  101. throw new MissingHeaderError('no ' + authzHeaderName + ' header ' +
  102. 'present in the request');
  103. }
  104. options.clockSkew = options.clockSkew || 300;
  105. var i = 0;
  106. var state = State.New;
  107. var substate = ParamsState.Name;
  108. var tmpName = '';
  109. var tmpValue = '';
  110. var parsed = {
  111. scheme: '',
  112. params: {},
  113. signingString: ''
  114. };
  115. var authz = request.headers[authzHeaderName];
  116. for (i = 0; i < authz.length; i++) {
  117. var c = authz.charAt(i);
  118. switch (Number(state)) {
  119. case State.New:
  120. if (c !== ' ') parsed.scheme += c;
  121. else state = State.Params;
  122. break;
  123. case State.Params:
  124. switch (Number(substate)) {
  125. case ParamsState.Name:
  126. var code = c.charCodeAt(0);
  127. // restricted name of A-Z / a-z
  128. if ((code >= 0x41 && code <= 0x5a) || // A-Z
  129. (code >= 0x61 && code <= 0x7a)) { // a-z
  130. tmpName += c;
  131. } else if (c === '=') {
  132. if (tmpName.length === 0)
  133. throw new InvalidHeaderError('bad param format');
  134. substate = ParamsState.Quote;
  135. } else {
  136. throw new InvalidHeaderError('bad param format');
  137. }
  138. break;
  139. case ParamsState.Quote:
  140. if (c === '"') {
  141. tmpValue = '';
  142. substate = ParamsState.Value;
  143. } else {
  144. throw new InvalidHeaderError('bad param format');
  145. }
  146. break;
  147. case ParamsState.Value:
  148. if (c === '"') {
  149. parsed.params[tmpName] = tmpValue;
  150. substate = ParamsState.Comma;
  151. } else {
  152. tmpValue += c;
  153. }
  154. break;
  155. case ParamsState.Comma:
  156. if (c === ',') {
  157. tmpName = '';
  158. substate = ParamsState.Name;
  159. } else {
  160. throw new InvalidHeaderError('bad param format');
  161. }
  162. break;
  163. default:
  164. throw new Error('Invalid substate');
  165. }
  166. break;
  167. default:
  168. throw new Error('Invalid substate');
  169. }
  170. }
  171. if (!parsed.params.headers || parsed.params.headers === '') {
  172. if (request.headers['x-date']) {
  173. parsed.params.headers = ['x-date'];
  174. } else {
  175. parsed.params.headers = ['date'];
  176. }
  177. } else {
  178. parsed.params.headers = parsed.params.headers.split(' ');
  179. }
  180. // Minimally validate the parsed object
  181. if (!parsed.scheme || parsed.scheme !== 'Signature')
  182. throw new InvalidHeaderError('scheme was not "Signature"');
  183. if (!parsed.params.keyId)
  184. throw new InvalidHeaderError('keyId was not specified');
  185. if (!parsed.params.algorithm)
  186. throw new InvalidHeaderError('algorithm was not specified');
  187. if (!parsed.params.signature)
  188. throw new InvalidHeaderError('signature was not specified');
  189. // Check the algorithm against the official list
  190. parsed.params.algorithm = parsed.params.algorithm.toLowerCase();
  191. try {
  192. validateAlgorithm(parsed.params.algorithm);
  193. } catch (e) {
  194. if (e instanceof InvalidAlgorithmError)
  195. throw (new InvalidParamsError(parsed.params.algorithm + ' is not ' +
  196. 'supported'));
  197. else
  198. throw (e);
  199. }
  200. // Build the signingString
  201. for (i = 0; i < parsed.params.headers.length; i++) {
  202. var h = parsed.params.headers[i].toLowerCase();
  203. parsed.params.headers[i] = h;
  204. if (h === 'request-line') {
  205. if (!options.strict) {
  206. /*
  207. * We allow headers from the older spec drafts if strict parsing isn't
  208. * specified in options.
  209. */
  210. parsed.signingString +=
  211. request.method + ' ' + request.url + ' HTTP/' + request.httpVersion;
  212. } else {
  213. /* Strict parsing doesn't allow older draft headers. */
  214. throw (new StrictParsingError('request-line is not a valid header ' +
  215. 'with strict parsing enabled.'));
  216. }
  217. } else if (h === '(request-target)') {
  218. parsed.signingString +=
  219. '(request-target): ' + request.method.toLowerCase() + ' ' +
  220. request.url;
  221. } else {
  222. var value = request.headers[h];
  223. if (value === undefined)
  224. throw new MissingHeaderError(h + ' was not in the request');
  225. parsed.signingString += h + ': ' + value;
  226. }
  227. if ((i + 1) < parsed.params.headers.length)
  228. parsed.signingString += '\n';
  229. }
  230. // Check against the constraints
  231. var date;
  232. if (request.headers.date || request.headers['x-date']) {
  233. if (request.headers['x-date']) {
  234. date = new Date(request.headers['x-date']);
  235. } else {
  236. date = new Date(request.headers.date);
  237. }
  238. var now = new Date();
  239. var skew = Math.abs(now.getTime() - date.getTime());
  240. if (skew > options.clockSkew * 1000) {
  241. throw new ExpiredRequestError('clock skew of ' +
  242. (skew / 1000) +
  243. 's was greater than ' +
  244. options.clockSkew + 's');
  245. }
  246. }
  247. options.headers.forEach(function (hdr) {
  248. // Remember that we already checked any headers in the params
  249. // were in the request, so if this passes we're good.
  250. if (parsed.params.headers.indexOf(hdr.toLowerCase()) < 0)
  251. throw new MissingHeaderError(hdr + ' was not a signed header');
  252. });
  253. if (options.algorithms) {
  254. if (options.algorithms.indexOf(parsed.params.algorithm) === -1)
  255. throw new InvalidParamsError(parsed.params.algorithm +
  256. ' is not a supported algorithm');
  257. }
  258. parsed.algorithm = parsed.params.algorithm.toUpperCase();
  259. parsed.keyId = parsed.params.keyId;
  260. return parsed;
  261. }
  262. };