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