[6a3a178] | 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 | };
|
---|