'use strict'; var Eckles = require('eckles'); var Rasha = require('rasha'); var Enc = {}; var Keypairs = module.exports; /*global Promise*/ Keypairs.generate = function (opts) { opts = opts || {}; var kty = opts.kty || opts.type; if ('RSA' === kty) { return Rasha.generate(opts); } return Eckles.generate(opts); }; Keypairs.import = function (opts) { return Eckles.import(opts.pem).catch(function () { return Rasha.import(opts.pem); }); }; Keypairs.export = function (opts) { return Promise.resolve().then(function () { if ('RSA' === opts.jwk.kty) { return Rasha.export(opts); } else { return Eckles.export(opts); } }); }; Keypairs.thumbprint = function (opts) { return Promise.resolve().then(function () { if ('RSA' === opts.jwk.kty) { return Rasha.thumbprint(opts); } else { return Eckles.thumbprint(opts); } }); }; // JWT a.k.a. JWS with Claims using Compact Serialization Keypairs.signJwt = function (opts) { return Keypairs.thumbprint({ jwk: opts.jwk }).then(function (thumb) { var header = opts.header || {}; var claims = JSON.parse(JSON.stringify(opts.claims || {})); header.typ = 'JWT'; if (!header.kid) { header.kid = thumb; } if (false === claims.iat) { claims.iat = undefined; } else if (!claims.iat) { claims.iat = Math.round(Date.now()/1000); } if (false === claims.exp) { claims.exp = undefined; } else if (!claims.exp) { throw new Error("opts.claims.exp should be the expiration date (as seconds since the Unix epoch) or false"); } if (false === claims.iss) { claims.iss = undefined; } else if (!claims.iss) { throw new Error("opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url"); } return Keypairs.signJws({ jwk: opts.jwk , pem: opts.pem , protected: header , header: undefined , payload: claims }).then(function (jws) { return [ jws.protected, jws.payload, jws.signature ].join('.'); }); }); }; Keypairs.signJws = function (opts) { return Keypairs.thumbprint(opts).then(function (thumb) { function alg() { if (!opts.jwk) { throw new Error("opts.jwk must exist and must declare 'typ'"); } return ('RSA' === opts.jwk.typ) ? "RS256" : "ES256"; } function sign(pem) { var header = opts.header; var protect = opts.protected; var payload = opts.payload; // Compute JWS signature var protectedHeader = ""; // Because unprotected headers are allowed, regrettably... // https://stackoverflow.com/a/46288694 if (false !== protect) { if (!protect) { protect = {}; } if (!protect.alg) { protect.alg = alg(); } // There's a particular request where Let's Encrypt explicitly doesn't use a kid if (!protect.kid && false !== protect.kid) { protect.kid = thumb; } protectedHeader = JSON.stringify(protect); } // Convert payload to Buffer if ('string' !== typeof payload && !Buffer.isBuffer(payload)) { if (!payload) { throw new Error("opts.payload should be JSON, string, or Buffer (it may be empty, but that must be explicit)"); } payload = JSON.stringify(payload); } if ('string' === typeof payload) { payload = Buffer.from(payload, 'binary'); } // node specifies RSA-SHAxxx even whet it's actually ecdsa (it's all encoded x509 shasums anyway) var nodeAlg = "RSA-SHA" + (((protect||header).alg||'').replace(/^[^\d]+/, '')||'256'); var protected64 = Enc.strToUrlBase64(protectedHeader); var payload64 = Enc.bufToUrlBase64(payload); var sig = require('crypto') .createSign(nodeAlg) .update(protect ? (protected64 + "." + payload64) : payload64) .sign(pem, 'base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, '') ; return { header: header , protected: protected64 || undefined , payload: payload64 , signature: sig }; } if (opts.pem && opts.jwk) { return sign(opts.pem); } else { return Keypairs.export({ jwk: opts.jwk }).then(sign); } }); }; Enc.strToUrlBase64 = function (str) { // node automatically can tell the difference // between uc2 (utf-8) strings and binary strings // so we don't have to re-encode the strings return Buffer.from(str).toString('base64') .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); }; Enc.bufToUrlBase64 = function (buf) { // allow for Uint8Array as a Buffer return Buffer.from(buf).toString('base64') .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); };