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