twitst4tz

twitter statistics web application
Log | Files | Refs | README | LICENSE

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 };