/*! * jws/sign.js - Sign to JWS * * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. */ "use strict"; var merge = require("../util/merge"), util = require("../util"), JWK = require("../jwk"), slice = require("./helpers").slice; var clone = require("lodash/clone"); var uniq = require("lodash/uniq"); var DEFAULTS = require("./defaults"); /** * @class JWS.Signer * @classdesc Generator of signed content. * * @description * **NOTE:** this class cannot be instantiated directly. Instead call {@link * JWS.createSign}. */ var JWSSigner = function(cfg, signatories) { var finalized = false, format = cfg.format || "general", content = Buffer.alloc(0); /** * @member {Boolean} JWS.Signer#compact * @description * Indicates whether the outuput of this signature generator is using * the Compact serialization (`true`) or the JSON serialization * (`false`). */ Object.defineProperty(this, "compact", { get: function() { return "compact" === format; }, enumerable: true }); Object.defineProperty(this, "format", { get: function() { return format; }, enumerable: true }); /** * @method JWS.Signer#update * @description * Updates the signing content for this signature content. The content * is appended to the end of any other content already applied. * * If {data} is a Buffer, {encoding} is ignored. Otherwise, {data} is * converted to a Buffer internally to {encoding}. * * @param {Buffer|String} data The data to sign. * @param {String} [encoding="binary"] The encoding of {data}. * @returns {JWS.Signer} This signature generator. * @throws {Error} If a signature has already been generated. */ Object.defineProperty(this, "update", { value: function(data, encoding) { if (finalized) { throw new Error("already final"); } if (data != null) { data = util.asBuffer(data, encoding); if (content.length) { content = Buffer.concat([content, data], content.length + data.length); } else { content = data; } } return this; } }); /** * @method JWS.Signer#final * @description * Finishes the signature operation. * * The returned Promise, when fulfilled, is the JSON Web Signature (JWS) * object, either in the Compact (if {@link JWS.Signer#format} is * `"compact"`), the flattened JSON (if {@link JWS.Signer#format} is * "flattened"), or the general JSON serialization. * * @param {Buffer|String} [data] The final content to apply. * @param {String} [encoding="binary"] The encoding of the final content * (if any). * @returns {Promise} The promise for the signatures * @throws {Error} If a signature has already been generated. */ Object.defineProperty(this, "final", { value: function(data, encoding) { if (finalized) { return Promise.reject(new Error("already final")); } // last-minute data this.update(data, encoding); // mark as done...ish finalized = true; var promise; // map signatory promises to just signatories promise = Promise.all(signatories); promise = promise.then(function(sigs) { // prepare content content = util.base64url.encode(content); sigs = sigs.map(function(s) { // prepare protected var protect = {}, lenProtect = 0, unprotect = clone(s.header), lenUnprotect = Object.keys(unprotect).length; s.protected.forEach(function(h) { if (!(h in unprotect)) { return; } protect[h] = unprotect[h]; lenProtect++; delete unprotect[h]; lenUnprotect--; }); if (lenProtect > 0) { protect = JSON.stringify(protect); protect = util.base64url.encode(protect); } else { protect = ""; } // signit! var data = Buffer.from(protect + "." + content, "ascii"); s = s.key.sign(s.header.alg, data, s.header); s = s.then(function(result) { var sig = {}; if (0 < lenProtect) { sig.protected = protect; } if (0 < lenUnprotect) { sig.header = unprotect; } sig.signature = util.base64url.encode(result.mac); return sig; }); return s; }); sigs = [Promise.resolve(content)].concat(sigs); return Promise.all(sigs); }); promise = promise.then(function(results) { var content = results[0]; return { payload: content, signatures: results.slice(1) }; }); switch (format) { case "compact": promise = promise.then(function(jws) { var compact = [ jws.signatures[0].protected, jws.payload, jws.signatures[0].signature ]; compact = compact.join("."); return compact; }); break; case "flattened": promise = promise.then(function(jws) { var flattened = {}; flattened.payload = jws.payload; var sig = jws.signatures[0]; if (sig.protected) { flattened.protected = sig.protected; } if (sig.header) { flattened.header = sig.header; } flattened.signature = sig.signature; return flattened; }); break; } return promise; } }); }; /** * @description * Creates a new JWS.Signer with the given options and signatories. * * @param {Object} [opts] The signing options * @param {Boolean} [opts.compact] Use compact serialization? * @param {String} [opts.format] The serialization format to use ("compact", * "flattened", "general") * @param {Object} [opts.fields] Additional header fields * @param {JWK.Key[]|Object[]} [signs] Signatories, either as an array of * JWK.Key instances; or an array of objects, each with the following * properties * @param {JWK.Key} signs.key Key used to sign content * @param {Object} [signs.header] Per-signatory header fields * @param {String} [signs.reference] Reference field to identify the key * @param {String[]|String} [signs.protect] List of fields to integrity * protect ("*" to protect all fields) * @returns {JWS.Signer} The signature generator. * @throws {Error} If Compact serialization is requested but there are * multiple signatories */ function createSign(opts, signs) { // fixup signatories var options = opts, signStart = 1, signList = signs; if (arguments.length === 0) { throw new Error("at least one signatory must be provided"); } if (arguments.length === 1) { signList = opts; signStart = 0; options = {}; } else if (JWK.isKey(opts) || (opts && "kty" in opts) || (opts && "key" in opts && (JWK.isKey(opts.key) || "kty" in opts.key))) { signList = opts; signStart = 0; options = {}; } else { options = clone(opts); } if (!Array.isArray(signList)) { signList = slice(arguments, signStart); } // fixup options options = merge(clone(DEFAULTS), options); // setup header fields var allFields = options.fields || {}; // setup serialization format var format = options.format; if (!format) { format = options.compact ? "compact" : "general"; } if (("compact" === format || "flattened" === format) && 1 < signList.length) { throw new Error("too many signatories for compact or flattened JSON serialization"); } // note protected fields (globally) // protected fields are per signature var protectAll = ("*" === options.protect); if (options.compact) { protectAll = true; } signList = signList.map(function(s, idx) { var p; // resolve a key if (s && "kty" in s) { p = JWK.asKey(s); p = p.then(function(k) { return { key: k }; }); } else if (s) { p = JWK.asKey(s.key); p = p.then(function(k) { return { header: s.header, reference: s.reference, protect: s.protect, key: k }; }); } else { p = Promise.reject(new Error("missing key for signatory " + idx)); } // resolve the complete signatory p = p.then(function(signatory) { var key = signatory.key; // make sure there is a header var header = signatory.header || {}; header = merge(merge({}, allFields), header); signatory.header = header; // ensure an algorithm if (!header.alg) { header.alg = key.algorithms(JWK.MODE_SIGN)[0] || ""; } // determine the key reference var ref = signatory.reference; delete signatory.reference; if (undefined === ref) { // header already contains the key reference ref = ["kid", "jku", "x5c", "x5t", "x5u"].some(function(k) { return (k in header); }); ref = !ref ? "kid" : null; } else if ("boolean" === typeof ref) { // explicit (positive | negative) request for key reference ref = ref ? "kid" : null; } var jwk; if (ref) { jwk = key.toJSON(); if ("jwk" === ref) { if ("oct" === key.kty) { return Promise.reject(new Error("cannot embed key")); } header.jwk = jwk; } else if (ref in jwk) { header[ref] = jwk[ref]; } } // determine protected fields var protect = signatory.protect; if (protectAll || "*" === protect) { protect = Object.keys(header); } else if ("string" === protect) { protect = [protect]; } else if (Array.isArray(protect)) { protect = protect.concat(); } else if (!protect) { protect = []; } else { return Promise.reject(new Error("protect must be a list of fields")); } protect = uniq(protect); signatory.protected = protect; // freeze signatory signatory = Object.freeze(signatory); return signatory; }); return p; }); var cfg = { format: format }; return new JWSSigner(cfg, signList); } module.exports = { signer: JWSSigner, createSign: createSign };