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