| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 | 
							- // 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');
 
-     }
 
-     request.setHeader('Authorization', sprintf(AUTHZ_FMT,
 
-                                                options.keyId,
 
-                                                options.algorithm,
 
-                                                options.headers.join(' '),
 
-                                                signature));
 
-     return true;
 
-   }
 
- };
 
 
  |