signer.js (13013B)
1 // Copyright 2012 Joyent, Inc. All rights reserved. 2 3 var assert = require('assert-plus'); 4 var crypto = require('crypto'); 5 var http = require('http'); 6 var util = require('util'); 7 var sshpk = require('sshpk'); 8 var jsprim = require('jsprim'); 9 var utils = require('./utils'); 10 11 var sprintf = require('util').format; 12 13 var HASH_ALGOS = utils.HASH_ALGOS; 14 var PK_ALGOS = utils.PK_ALGOS; 15 var InvalidAlgorithmError = utils.InvalidAlgorithmError; 16 var HttpSignatureError = utils.HttpSignatureError; 17 var validateAlgorithm = utils.validateAlgorithm; 18 19 ///--- Globals 20 21 var AUTHZ_FMT = 22 'Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"'; 23 24 ///--- Specific Errors 25 26 function MissingHeaderError(message) { 27 HttpSignatureError.call(this, message, MissingHeaderError); 28 } 29 util.inherits(MissingHeaderError, HttpSignatureError); 30 31 function StrictParsingError(message) { 32 HttpSignatureError.call(this, message, StrictParsingError); 33 } 34 util.inherits(StrictParsingError, HttpSignatureError); 35 36 /* See createSigner() */ 37 function RequestSigner(options) { 38 assert.object(options, 'options'); 39 40 var alg = []; 41 if (options.algorithm !== undefined) { 42 assert.string(options.algorithm, 'options.algorithm'); 43 alg = validateAlgorithm(options.algorithm); 44 } 45 this.rs_alg = alg; 46 47 /* 48 * RequestSigners come in two varieties: ones with an rs_signFunc, and ones 49 * with an rs_signer. 50 * 51 * rs_signFunc-based RequestSigners have to build up their entire signing 52 * string within the rs_lines array and give it to rs_signFunc as a single 53 * concat'd blob. rs_signer-based RequestSigners can add a line at a time to 54 * their signing state by using rs_signer.update(), thus only needing to 55 * buffer the hash function state and one line at a time. 56 */ 57 if (options.sign !== undefined) { 58 assert.func(options.sign, 'options.sign'); 59 this.rs_signFunc = options.sign; 60 61 } else if (alg[0] === 'hmac' && options.key !== undefined) { 62 assert.string(options.keyId, 'options.keyId'); 63 this.rs_keyId = options.keyId; 64 65 if (typeof (options.key) !== 'string' && !Buffer.isBuffer(options.key)) 66 throw (new TypeError('options.key for HMAC must be a string or Buffer')); 67 68 /* 69 * Make an rs_signer for HMACs, not a rs_signFunc -- HMACs digest their 70 * data in chunks rather than requiring it all to be given in one go 71 * at the end, so they are more similar to signers than signFuncs. 72 */ 73 this.rs_signer = crypto.createHmac(alg[1].toUpperCase(), options.key); 74 this.rs_signer.sign = function () { 75 var digest = this.digest('base64'); 76 return ({ 77 hashAlgorithm: alg[1], 78 toString: function () { return (digest); } 79 }); 80 }; 81 82 } else if (options.key !== undefined) { 83 var key = options.key; 84 if (typeof (key) === 'string' || Buffer.isBuffer(key)) 85 key = sshpk.parsePrivateKey(key); 86 87 assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]), 88 'options.key must be a sshpk.PrivateKey'); 89 this.rs_key = key; 90 91 assert.string(options.keyId, 'options.keyId'); 92 this.rs_keyId = options.keyId; 93 94 if (!PK_ALGOS[key.type]) { 95 throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' + 96 'keys are not supported')); 97 } 98 99 if (alg[0] !== undefined && key.type !== alg[0]) { 100 throw (new InvalidAlgorithmError('options.key must be a ' + 101 alg[0].toUpperCase() + ' key, was given a ' + 102 key.type.toUpperCase() + ' key instead')); 103 } 104 105 this.rs_signer = key.createSign(alg[1]); 106 107 } else { 108 throw (new TypeError('options.sign (func) or options.key is required')); 109 } 110 111 this.rs_headers = []; 112 this.rs_lines = []; 113 } 114 115 /** 116 * Adds a header to be signed, with its value, into this signer. 117 * 118 * @param {String} header 119 * @param {String} value 120 * @return {String} value written 121 */ 122 RequestSigner.prototype.writeHeader = function (header, value) { 123 assert.string(header, 'header'); 124 header = header.toLowerCase(); 125 assert.string(value, 'value'); 126 127 this.rs_headers.push(header); 128 129 if (this.rs_signFunc) { 130 this.rs_lines.push(header + ': ' + value); 131 132 } else { 133 var line = header + ': ' + value; 134 if (this.rs_headers.length > 0) 135 line = '\n' + line; 136 this.rs_signer.update(line); 137 } 138 139 return (value); 140 }; 141 142 /** 143 * Adds a default Date header, returning its value. 144 * 145 * @return {String} 146 */ 147 RequestSigner.prototype.writeDateHeader = function () { 148 return (this.writeHeader('date', jsprim.rfc1123(new Date()))); 149 }; 150 151 /** 152 * Adds the request target line to be signed. 153 * 154 * @param {String} method, HTTP method (e.g. 'get', 'post', 'put') 155 * @param {String} path 156 */ 157 RequestSigner.prototype.writeTarget = function (method, path) { 158 assert.string(method, 'method'); 159 assert.string(path, 'path'); 160 method = method.toLowerCase(); 161 this.writeHeader('(request-target)', method + ' ' + path); 162 }; 163 164 /** 165 * Calculate the value for the Authorization header on this request 166 * asynchronously. 167 * 168 * @param {Func} callback (err, authz) 169 */ 170 RequestSigner.prototype.sign = function (cb) { 171 assert.func(cb, 'callback'); 172 173 if (this.rs_headers.length < 1) 174 throw (new Error('At least one header must be signed')); 175 176 var alg, authz; 177 if (this.rs_signFunc) { 178 var data = this.rs_lines.join('\n'); 179 var self = this; 180 this.rs_signFunc(data, function (err, sig) { 181 if (err) { 182 cb(err); 183 return; 184 } 185 try { 186 assert.object(sig, 'signature'); 187 assert.string(sig.keyId, 'signature.keyId'); 188 assert.string(sig.algorithm, 'signature.algorithm'); 189 assert.string(sig.signature, 'signature.signature'); 190 alg = validateAlgorithm(sig.algorithm); 191 192 authz = sprintf(AUTHZ_FMT, 193 sig.keyId, 194 sig.algorithm, 195 self.rs_headers.join(' '), 196 sig.signature); 197 } catch (e) { 198 cb(e); 199 return; 200 } 201 cb(null, authz); 202 }); 203 204 } else { 205 try { 206 var sigObj = this.rs_signer.sign(); 207 } catch (e) { 208 cb(e); 209 return; 210 } 211 alg = (this.rs_alg[0] || this.rs_key.type) + '-' + sigObj.hashAlgorithm; 212 var signature = sigObj.toString(); 213 authz = sprintf(AUTHZ_FMT, 214 this.rs_keyId, 215 alg, 216 this.rs_headers.join(' '), 217 signature); 218 cb(null, authz); 219 } 220 }; 221 222 ///--- Exported API 223 224 module.exports = { 225 /** 226 * Identifies whether a given object is a request signer or not. 227 * 228 * @param {Object} object, the object to identify 229 * @returns {Boolean} 230 */ 231 isSigner: function (obj) { 232 if (typeof (obj) === 'object' && obj instanceof RequestSigner) 233 return (true); 234 return (false); 235 }, 236 237 /** 238 * Creates a request signer, used to asynchronously build a signature 239 * for a request (does not have to be an http.ClientRequest). 240 * 241 * @param {Object} options, either: 242 * - {String} keyId 243 * - {String|Buffer} key 244 * - {String} algorithm (optional, required for HMAC) 245 * or: 246 * - {Func} sign (data, cb) 247 * @return {RequestSigner} 248 */ 249 createSigner: function createSigner(options) { 250 return (new RequestSigner(options)); 251 }, 252 253 /** 254 * Adds an 'Authorization' header to an http.ClientRequest object. 255 * 256 * Note that this API will add a Date header if it's not already set. Any 257 * other headers in the options.headers array MUST be present, or this 258 * will throw. 259 * 260 * You shouldn't need to check the return type; it's just there if you want 261 * to be pedantic. 262 * 263 * The optional flag indicates whether parsing should use strict enforcement 264 * of the version draft-cavage-http-signatures-04 of the spec or beyond. 265 * The default is to be loose and support 266 * older versions for compatibility. 267 * 268 * @param {Object} request an instance of http.ClientRequest. 269 * @param {Object} options signing parameters object: 270 * - {String} keyId required. 271 * - {String} key required (either a PEM or HMAC key). 272 * - {Array} headers optional; defaults to ['date']. 273 * - {String} algorithm optional (unless key is HMAC); 274 * default is the same as the sshpk default 275 * signing algorithm for the type of key given 276 * - {String} httpVersion optional; defaults to '1.1'. 277 * - {Boolean} strict optional; defaults to 'false'. 278 * @return {Boolean} true if Authorization (and optionally Date) were added. 279 * @throws {TypeError} on bad parameter types (input). 280 * @throws {InvalidAlgorithmError} if algorithm was bad or incompatible with 281 * the given key. 282 * @throws {sshpk.KeyParseError} if key was bad. 283 * @throws {MissingHeaderError} if a header to be signed was specified but 284 * was not present. 285 */ 286 signRequest: function signRequest(request, options) { 287 assert.object(request, 'request'); 288 assert.object(options, 'options'); 289 assert.optionalString(options.algorithm, 'options.algorithm'); 290 assert.string(options.keyId, 'options.keyId'); 291 assert.optionalArrayOfString(options.headers, 'options.headers'); 292 assert.optionalString(options.httpVersion, 'options.httpVersion'); 293 294 if (!request.getHeader('Date')) 295 request.setHeader('Date', jsprim.rfc1123(new Date())); 296 if (!options.headers) 297 options.headers = ['date']; 298 if (!options.httpVersion) 299 options.httpVersion = '1.1'; 300 301 var alg = []; 302 if (options.algorithm) { 303 options.algorithm = options.algorithm.toLowerCase(); 304 alg = validateAlgorithm(options.algorithm); 305 } 306 307 var i; 308 var stringToSign = ''; 309 for (i = 0; i < options.headers.length; i++) { 310 if (typeof (options.headers[i]) !== 'string') 311 throw new TypeError('options.headers must be an array of Strings'); 312 313 var h = options.headers[i].toLowerCase(); 314 315 if (h === 'request-line') { 316 if (!options.strict) { 317 /** 318 * We allow headers from the older spec drafts if strict parsing isn't 319 * specified in options. 320 */ 321 stringToSign += 322 request.method + ' ' + request.path + ' HTTP/' + 323 options.httpVersion; 324 } else { 325 /* Strict parsing doesn't allow older draft headers. */ 326 throw (new StrictParsingError('request-line is not a valid header ' + 327 'with strict parsing enabled.')); 328 } 329 } else if (h === '(request-target)') { 330 stringToSign += 331 '(request-target): ' + request.method.toLowerCase() + ' ' + 332 request.path; 333 } else { 334 var value = request.getHeader(h); 335 if (value === undefined || value === '') { 336 throw new MissingHeaderError(h + ' was not in the request'); 337 } 338 stringToSign += h + ': ' + value; 339 } 340 341 if ((i + 1) < options.headers.length) 342 stringToSign += '\n'; 343 } 344 345 /* This is just for unit tests. */ 346 if (request.hasOwnProperty('_stringToSign')) { 347 request._stringToSign = stringToSign; 348 } 349 350 var signature; 351 if (alg[0] === 'hmac') { 352 if (typeof (options.key) !== 'string' && !Buffer.isBuffer(options.key)) 353 throw (new TypeError('options.key must be a string or Buffer')); 354 355 var hmac = crypto.createHmac(alg[1].toUpperCase(), options.key); 356 hmac.update(stringToSign); 357 signature = hmac.digest('base64'); 358 359 } else { 360 var key = options.key; 361 if (typeof (key) === 'string' || Buffer.isBuffer(key)) 362 key = sshpk.parsePrivateKey(options.key); 363 364 assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]), 365 'options.key must be a sshpk.PrivateKey'); 366 367 if (!PK_ALGOS[key.type]) { 368 throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' + 369 'keys are not supported')); 370 } 371 372 if (alg[0] !== undefined && key.type !== alg[0]) { 373 throw (new InvalidAlgorithmError('options.key must be a ' + 374 alg[0].toUpperCase() + ' key, was given a ' + 375 key.type.toUpperCase() + ' key instead')); 376 } 377 378 var signer = key.createSign(alg[1]); 379 signer.update(stringToSign); 380 var sigObj = signer.sign(); 381 if (!HASH_ALGOS[sigObj.hashAlgorithm]) { 382 throw (new InvalidAlgorithmError(sigObj.hashAlgorithm.toUpperCase() + 383 ' is not a supported hash algorithm')); 384 } 385 options.algorithm = key.type + '-' + sigObj.hashAlgorithm; 386 signature = sigObj.toString(); 387 assert.notStrictEqual(signature, '', 'empty signature produced'); 388 } 389 390 var authzHeaderName = options.authorizationHeaderName || 'Authorization'; 391 392 request.setHeader(authzHeaderName, sprintf(AUTHZ_FMT, 393 options.keyId, 394 options.algorithm, 395 options.headers.join(' '), 396 signature)); 397 398 return true; 399 } 400 401 };