You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					402 lines
				
				13 KiB
			
		
		
			
		
	
	
					402 lines
				
				13 KiB
			| 
											3 years ago
										 | // Copyright 2012 Joyent, Inc.  All rights reserved.
 | ||
|  | 
 | ||
|  | var assert = require('assert-plus'); | ||
|  | var crypto = require('crypto'); | ||
|  | var http = require('http'); | ||
|  | var util = require('util'); | ||
|  | var sshpk = require('sshpk'); | ||
|  | var jsprim = require('jsprim'); | ||
|  | var utils = require('./utils'); | ||
|  | 
 | ||
|  | var sprintf = require('util').format; | ||
|  | 
 | ||
|  | var HASH_ALGOS = utils.HASH_ALGOS; | ||
|  | var PK_ALGOS = utils.PK_ALGOS; | ||
|  | var InvalidAlgorithmError = utils.InvalidAlgorithmError; | ||
|  | var HttpSignatureError = utils.HttpSignatureError; | ||
|  | var validateAlgorithm = utils.validateAlgorithm; | ||
|  | 
 | ||
|  | ///--- Globals
 | ||
|  | 
 | ||
|  | var AUTHZ_FMT = | ||
|  |   'Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"'; | ||
|  | 
 | ||
|  | ///--- Specific Errors
 | ||
|  | 
 | ||
|  | function MissingHeaderError(message) { | ||
|  |   HttpSignatureError.call(this, message, MissingHeaderError); | ||
|  | } | ||
|  | util.inherits(MissingHeaderError, HttpSignatureError); | ||
|  | 
 | ||
|  | function StrictParsingError(message) { | ||
|  |   HttpSignatureError.call(this, message, StrictParsingError); | ||
|  | } | ||
|  | util.inherits(StrictParsingError, HttpSignatureError); | ||
|  | 
 | ||
|  | /* See createSigner() */ | ||
|  | function RequestSigner(options) { | ||
|  |   assert.object(options, 'options'); | ||
|  | 
 | ||
|  |   var alg = []; | ||
|  |   if (options.algorithm !== undefined) { | ||
|  |     assert.string(options.algorithm, 'options.algorithm'); | ||
|  |     alg = validateAlgorithm(options.algorithm); | ||
|  |   } | ||
|  |   this.rs_alg = alg; | ||
|  | 
 | ||
|  |   /* | ||
|  |    * RequestSigners come in two varieties: ones with an rs_signFunc, and ones | ||
|  |    * with an rs_signer. | ||
|  |    * | ||
|  |    * rs_signFunc-based RequestSigners have to build up their entire signing | ||
|  |    * string within the rs_lines array and give it to rs_signFunc as a single | ||
|  |    * concat'd blob. rs_signer-based RequestSigners can add a line at a time to | ||
|  |    * their signing state by using rs_signer.update(), thus only needing to | ||
|  |    * buffer the hash function state and one line at a time. | ||
|  |    */ | ||
|  |   if (options.sign !== undefined) { | ||
|  |     assert.func(options.sign, 'options.sign'); | ||
|  |     this.rs_signFunc = options.sign; | ||
|  | 
 | ||
|  |   } else if (alg[0] === 'hmac' && options.key !== undefined) { | ||
|  |     assert.string(options.keyId, 'options.keyId'); | ||
|  |     this.rs_keyId = options.keyId; | ||
|  | 
 | ||
|  |     if (typeof (options.key) !== 'string' && !Buffer.isBuffer(options.key)) | ||
|  |       throw (new TypeError('options.key for HMAC must be a string or Buffer')); | ||
|  | 
 | ||
|  |     /* | ||
|  |      * Make an rs_signer for HMACs, not a rs_signFunc -- HMACs digest their | ||
|  |      * data in chunks rather than requiring it all to be given in one go | ||
|  |      * at the end, so they are more similar to signers than signFuncs. | ||
|  |      */ | ||
|  |     this.rs_signer = crypto.createHmac(alg[1].toUpperCase(), options.key); | ||
|  |     this.rs_signer.sign = function () { | ||
|  |       var digest = this.digest('base64'); | ||
|  |       return ({ | ||
|  |         hashAlgorithm: alg[1], | ||
|  |         toString: function () { return (digest); } | ||
|  |       }); | ||
|  |     }; | ||
|  | 
 | ||
|  |   } else if (options.key !== undefined) { | ||
|  |     var key = options.key; | ||
|  |     if (typeof (key) === 'string' || Buffer.isBuffer(key)) | ||
|  |       key = sshpk.parsePrivateKey(key); | ||
|  | 
 | ||
|  |     assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]), | ||
|  |       'options.key must be a sshpk.PrivateKey'); | ||
|  |     this.rs_key = key; | ||
|  | 
 | ||
|  |     assert.string(options.keyId, 'options.keyId'); | ||
|  |     this.rs_keyId = options.keyId; | ||
|  | 
 | ||
|  |     if (!PK_ALGOS[key.type]) { | ||
|  |       throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' + | ||
|  |         'keys are not supported')); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (alg[0] !== undefined && key.type !== alg[0]) { | ||
|  |       throw (new InvalidAlgorithmError('options.key must be a ' + | ||
|  |         alg[0].toUpperCase() + ' key, was given a ' + | ||
|  |         key.type.toUpperCase() + ' key instead')); | ||
|  |     } | ||
|  | 
 | ||
|  |     this.rs_signer = key.createSign(alg[1]); | ||
|  | 
 | ||
|  |   } else { | ||
|  |     throw (new TypeError('options.sign (func) or options.key is required')); | ||
|  |   } | ||
|  | 
 | ||
|  |   this.rs_headers = []; | ||
|  |   this.rs_lines = []; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Adds a header to be signed, with its value, into this signer. | ||
|  |  * | ||
|  |  * @param {String} header | ||
|  |  * @param {String} value | ||
|  |  * @return {String} value written | ||
|  |  */ | ||
|  | RequestSigner.prototype.writeHeader = function (header, value) { | ||
|  |   assert.string(header, 'header'); | ||
|  |   header = header.toLowerCase(); | ||
|  |   assert.string(value, 'value'); | ||
|  | 
 | ||
|  |   this.rs_headers.push(header); | ||
|  | 
 | ||
|  |   if (this.rs_signFunc) { | ||
|  |     this.rs_lines.push(header + ': ' + value); | ||
|  | 
 | ||
|  |   } else { | ||
|  |     var line = header + ': ' + value; | ||
|  |     if (this.rs_headers.length > 0) | ||
|  |       line = '\n' + line; | ||
|  |     this.rs_signer.update(line); | ||
|  |   } | ||
|  | 
 | ||
|  |   return (value); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Adds a default Date header, returning its value. | ||
|  |  * | ||
|  |  * @return {String} | ||
|  |  */ | ||
|  | RequestSigner.prototype.writeDateHeader = function () { | ||
|  |   return (this.writeHeader('date', jsprim.rfc1123(new Date()))); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Adds the request target line to be signed. | ||
|  |  * | ||
|  |  * @param {String} method, HTTP method (e.g. 'get', 'post', 'put') | ||
|  |  * @param {String} path | ||
|  |  */ | ||
|  | RequestSigner.prototype.writeTarget = function (method, path) { | ||
|  |   assert.string(method, 'method'); | ||
|  |   assert.string(path, 'path'); | ||
|  |   method = method.toLowerCase(); | ||
|  |   this.writeHeader('(request-target)', method + ' ' + path); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Calculate the value for the Authorization header on this request | ||
|  |  * asynchronously. | ||
|  |  * | ||
|  |  * @param {Func} callback (err, authz) | ||
|  |  */ | ||
|  | RequestSigner.prototype.sign = function (cb) { | ||
|  |   assert.func(cb, 'callback'); | ||
|  | 
 | ||
|  |   if (this.rs_headers.length < 1) | ||
|  |     throw (new Error('At least one header must be signed')); | ||
|  | 
 | ||
|  |   var alg, authz; | ||
|  |   if (this.rs_signFunc) { | ||
|  |     var data = this.rs_lines.join('\n'); | ||
|  |     var self = this; | ||
|  |     this.rs_signFunc(data, function (err, sig) { | ||
|  |       if (err) { | ||
|  |         cb(err); | ||
|  |         return; | ||
|  |       } | ||
|  |       try { | ||
|  |         assert.object(sig, 'signature'); | ||
|  |         assert.string(sig.keyId, 'signature.keyId'); | ||
|  |         assert.string(sig.algorithm, 'signature.algorithm'); | ||
|  |         assert.string(sig.signature, 'signature.signature'); | ||
|  |         alg = validateAlgorithm(sig.algorithm); | ||
|  | 
 | ||
|  |         authz = sprintf(AUTHZ_FMT, | ||
|  |           sig.keyId, | ||
|  |           sig.algorithm, | ||
|  |           self.rs_headers.join(' '), | ||
|  |           sig.signature); | ||
|  |       } catch (e) { | ||
|  |         cb(e); | ||
|  |         return; | ||
|  |       } | ||
|  |       cb(null, authz); | ||
|  |     }); | ||
|  | 
 | ||
|  |   } else { | ||
|  |     try { | ||
|  |       var sigObj = this.rs_signer.sign(); | ||
|  |     } catch (e) { | ||
|  |       cb(e); | ||
|  |       return; | ||
|  |     } | ||
|  |     alg = (this.rs_alg[0] || this.rs_key.type) + '-' + sigObj.hashAlgorithm; | ||
|  |     var signature = sigObj.toString(); | ||
|  |     authz = sprintf(AUTHZ_FMT, | ||
|  |       this.rs_keyId, | ||
|  |       alg, | ||
|  |       this.rs_headers.join(' '), | ||
|  |       signature); | ||
|  |     cb(null, authz); | ||
|  |   } | ||
|  | }; | ||
|  | 
 | ||
|  | ///--- Exported API
 | ||
|  | 
 | ||
|  | module.exports = { | ||
|  |   /** | ||
|  |    * Identifies whether a given object is a request signer or not. | ||
|  |    * | ||
|  |    * @param {Object} object, the object to identify | ||
|  |    * @returns {Boolean} | ||
|  |    */ | ||
|  |   isSigner: function (obj) { | ||
|  |     if (typeof (obj) === 'object' && obj instanceof RequestSigner) | ||
|  |       return (true); | ||
|  |     return (false); | ||
|  |   }, | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Creates a request signer, used to asynchronously build a signature | ||
|  |    * for a request (does not have to be an http.ClientRequest). | ||
|  |    * | ||
|  |    * @param {Object} options, either: | ||
|  |    *                   - {String} keyId | ||
|  |    *                   - {String|Buffer} key | ||
|  |    *                   - {String} algorithm (optional, required for HMAC) | ||
|  |    *                 or: | ||
|  |    *                   - {Func} sign (data, cb) | ||
|  |    * @return {RequestSigner} | ||
|  |    */ | ||
|  |   createSigner: function createSigner(options) { | ||
|  |     return (new RequestSigner(options)); | ||
|  |   }, | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Adds an 'Authorization' header to an http.ClientRequest object. | ||
|  |    * | ||
|  |    * Note that this API will add a Date header if it's not already set. Any | ||
|  |    * other headers in the options.headers array MUST be present, or this | ||
|  |    * will throw. | ||
|  |    * | ||
|  |    * You shouldn't need to check the return type; it's just there if you want | ||
|  |    * to be pedantic. | ||
|  |    * | ||
|  |    * The optional flag indicates whether parsing should use strict enforcement | ||
|  |    * of the version draft-cavage-http-signatures-04 of the spec or beyond. | ||
|  |    * The default is to be loose and support | ||
|  |    * older versions for compatibility. | ||
|  |    * | ||
|  |    * @param {Object} request an instance of http.ClientRequest. | ||
|  |    * @param {Object} options signing parameters object: | ||
|  |    *                   - {String} keyId required. | ||
|  |    *                   - {String} key required (either a PEM or HMAC key). | ||
|  |    *                   - {Array} headers optional; defaults to ['date']. | ||
|  |    *                   - {String} algorithm optional (unless key is HMAC); | ||
|  |    *                              default is the same as the sshpk default | ||
|  |    *                              signing algorithm for the type of key given | ||
|  |    *                   - {String} httpVersion optional; defaults to '1.1'. | ||
|  |    *                   - {Boolean} strict optional; defaults to 'false'. | ||
|  |    * @return {Boolean} true if Authorization (and optionally Date) were added. | ||
|  |    * @throws {TypeError} on bad parameter types (input). | ||
|  |    * @throws {InvalidAlgorithmError} if algorithm was bad or incompatible with | ||
|  |    *                                 the given key. | ||
|  |    * @throws {sshpk.KeyParseError} if key was bad. | ||
|  |    * @throws {MissingHeaderError} if a header to be signed was specified but | ||
|  |    *                              was not present. | ||
|  |    */ | ||
|  |   signRequest: function signRequest(request, options) { | ||
|  |     assert.object(request, 'request'); | ||
|  |     assert.object(options, 'options'); | ||
|  |     assert.optionalString(options.algorithm, 'options.algorithm'); | ||
|  |     assert.string(options.keyId, 'options.keyId'); | ||
|  |     assert.optionalArrayOfString(options.headers, 'options.headers'); | ||
|  |     assert.optionalString(options.httpVersion, 'options.httpVersion'); | ||
|  | 
 | ||
|  |     if (!request.getHeader('Date')) | ||
|  |       request.setHeader('Date', jsprim.rfc1123(new Date())); | ||
|  |     if (!options.headers) | ||
|  |       options.headers = ['date']; | ||
|  |     if (!options.httpVersion) | ||
|  |       options.httpVersion = '1.1'; | ||
|  | 
 | ||
|  |     var alg = []; | ||
|  |     if (options.algorithm) { | ||
|  |       options.algorithm = options.algorithm.toLowerCase(); | ||
|  |       alg = validateAlgorithm(options.algorithm); | ||
|  |     } | ||
|  | 
 | ||
|  |     var i; | ||
|  |     var stringToSign = ''; | ||
|  |     for (i = 0; i < options.headers.length; i++) { | ||
|  |       if (typeof (options.headers[i]) !== 'string') | ||
|  |         throw new TypeError('options.headers must be an array of Strings'); | ||
|  | 
 | ||
|  |       var h = options.headers[i].toLowerCase(); | ||
|  | 
 | ||
|  |       if (h === 'request-line') { | ||
|  |         if (!options.strict) { | ||
|  |           /** | ||
|  |            * We allow headers from the older spec drafts if strict parsing isn't | ||
|  |            * specified in options. | ||
|  |            */ | ||
|  |           stringToSign += | ||
|  |             request.method + ' ' + request.path + ' HTTP/' + | ||
|  |             options.httpVersion; | ||
|  |         } else { | ||
|  |           /* Strict parsing doesn't allow older draft headers. */ | ||
|  |           throw (new StrictParsingError('request-line is not a valid header ' + | ||
|  |             'with strict parsing enabled.')); | ||
|  |         } | ||
|  |       } else if (h === '(request-target)') { | ||
|  |         stringToSign += | ||
|  |           '(request-target): ' + request.method.toLowerCase() + ' ' + | ||
|  |           request.path; | ||
|  |       } else { | ||
|  |         var value = request.getHeader(h); | ||
|  |         if (value === undefined || value === '') { | ||
|  |           throw new MissingHeaderError(h + ' was not in the request'); | ||
|  |         } | ||
|  |         stringToSign += h + ': ' + value; | ||
|  |       } | ||
|  | 
 | ||
|  |       if ((i + 1) < options.headers.length) | ||
|  |         stringToSign += '\n'; | ||
|  |     } | ||
|  | 
 | ||
|  |     /* This is just for unit tests. */ | ||
|  |     if (request.hasOwnProperty('_stringToSign')) { | ||
|  |       request._stringToSign = stringToSign; | ||
|  |     } | ||
|  | 
 | ||
|  |     var signature; | ||
|  |     if (alg[0] === 'hmac') { | ||
|  |       if (typeof (options.key) !== 'string' && !Buffer.isBuffer(options.key)) | ||
|  |         throw (new TypeError('options.key must be a string or Buffer')); | ||
|  | 
 | ||
|  |       var hmac = crypto.createHmac(alg[1].toUpperCase(), options.key); | ||
|  |       hmac.update(stringToSign); | ||
|  |       signature = hmac.digest('base64'); | ||
|  | 
 | ||
|  |     } else { | ||
|  |       var key = options.key; | ||
|  |       if (typeof (key) === 'string' || Buffer.isBuffer(key)) | ||
|  |         key = sshpk.parsePrivateKey(options.key); | ||
|  | 
 | ||
|  |       assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]), | ||
|  |         'options.key must be a sshpk.PrivateKey'); | ||
|  | 
 | ||
|  |       if (!PK_ALGOS[key.type]) { | ||
|  |         throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' + | ||
|  |           'keys are not supported')); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (alg[0] !== undefined && key.type !== alg[0]) { | ||
|  |         throw (new InvalidAlgorithmError('options.key must be a ' + | ||
|  |           alg[0].toUpperCase() + ' key, was given a ' + | ||
|  |           key.type.toUpperCase() + ' key instead')); | ||
|  |       } | ||
|  | 
 | ||
|  |       var signer = key.createSign(alg[1]); | ||
|  |       signer.update(stringToSign); | ||
|  |       var sigObj = signer.sign(); | ||
|  |       if (!HASH_ALGOS[sigObj.hashAlgorithm]) { | ||
|  |         throw (new InvalidAlgorithmError(sigObj.hashAlgorithm.toUpperCase() + | ||
|  |           ' is not a supported hash algorithm')); | ||
|  |       } | ||
|  |       options.algorithm = key.type + '-' + sigObj.hashAlgorithm; | ||
|  |       signature = sigObj.toString(); | ||
|  |       assert.notStrictEqual(signature, '', 'empty signature produced'); | ||
|  |     } | ||
|  | 
 | ||
|  |     var authzHeaderName = options.authorizationHeaderName || 'Authorization'; | ||
|  | 
 | ||
|  |     request.setHeader(authzHeaderName, sprintf(AUTHZ_FMT, | ||
|  |                                                options.keyId, | ||
|  |                                                options.algorithm, | ||
|  |                                                options.headers.join(' '), | ||
|  |                                                signature)); | ||
|  | 
 | ||
|  |     return true; | ||
|  |   } | ||
|  | 
 | ||
|  | }; |