Initial commit
[yaffs-website] / node_modules / http-signature / lib / signer.js
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     request.setHeader('Authorization', sprintf(AUTHZ_FMT,
391                                                options.keyId,
392                                                options.algorithm,
393                                                options.headers.join(' '),
394                                                signature));
395
396     return true;
397   }
398
399 };