1 | // Copyright 2012 Joyent, Inc. All rights reserved.
|
---|
2 |
|
---|
3 | var assert = require('assert-plus');
|
---|
4 | var util = require('util');
|
---|
5 | var utils = require('./utils');
|
---|
6 |
|
---|
7 |
|
---|
8 |
|
---|
9 | ///--- Globals
|
---|
10 |
|
---|
11 | var HASH_ALGOS = utils.HASH_ALGOS;
|
---|
12 | var PK_ALGOS = utils.PK_ALGOS;
|
---|
13 | var HttpSignatureError = utils.HttpSignatureError;
|
---|
14 | var InvalidAlgorithmError = utils.InvalidAlgorithmError;
|
---|
15 | var validateAlgorithm = utils.validateAlgorithm;
|
---|
16 |
|
---|
17 | var State = {
|
---|
18 | New: 0,
|
---|
19 | Params: 1
|
---|
20 | };
|
---|
21 |
|
---|
22 | var ParamsState = {
|
---|
23 | Name: 0,
|
---|
24 | Quote: 1,
|
---|
25 | Value: 2,
|
---|
26 | Comma: 3
|
---|
27 | };
|
---|
28 |
|
---|
29 |
|
---|
30 | ///--- Specific Errors
|
---|
31 |
|
---|
32 |
|
---|
33 | function ExpiredRequestError(message) {
|
---|
34 | HttpSignatureError.call(this, message, ExpiredRequestError);
|
---|
35 | }
|
---|
36 | util.inherits(ExpiredRequestError, HttpSignatureError);
|
---|
37 |
|
---|
38 |
|
---|
39 | function InvalidHeaderError(message) {
|
---|
40 | HttpSignatureError.call(this, message, InvalidHeaderError);
|
---|
41 | }
|
---|
42 | util.inherits(InvalidHeaderError, HttpSignatureError);
|
---|
43 |
|
---|
44 |
|
---|
45 | function InvalidParamsError(message) {
|
---|
46 | HttpSignatureError.call(this, message, InvalidParamsError);
|
---|
47 | }
|
---|
48 | util.inherits(InvalidParamsError, HttpSignatureError);
|
---|
49 |
|
---|
50 |
|
---|
51 | function MissingHeaderError(message) {
|
---|
52 | HttpSignatureError.call(this, message, MissingHeaderError);
|
---|
53 | }
|
---|
54 | util.inherits(MissingHeaderError, HttpSignatureError);
|
---|
55 |
|
---|
56 | function StrictParsingError(message) {
|
---|
57 | HttpSignatureError.call(this, message, StrictParsingError);
|
---|
58 | }
|
---|
59 | util.inherits(StrictParsingError, HttpSignatureError);
|
---|
60 |
|
---|
61 | ///--- Exported API
|
---|
62 |
|
---|
63 | module.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 | };
|
---|