From 58dab177daf06144ce840005a71a15865175869b Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 7 Mar 2019 01:38:21 -0700 Subject: [PATCH] shims for jwt and jws authentication --- bin/telebit-remote.js | 10 +-- bin/telebitd.js | 149 ++++++++++++++++++++++++++++++++---------- lib/eggspress.js | 6 +- lib/rc/index.js | 41 ++++++++++-- package-lock.json | 6 +- package.json | 2 +- 6 files changed, 163 insertions(+), 51 deletions(-) diff --git a/bin/telebit-remote.js b/bin/telebit-remote.js index 29f25de..5a4aab1 100755 --- a/bin/telebit-remote.js +++ b/bin/telebit-remote.js @@ -13,6 +13,7 @@ var YAML = require('js-yaml'); var TOML = require('toml'); var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8')); var JWT = require('../lib/jwt.js'); +var keypairs = require('keypairs'); /* if ('function' !== typeof TOML.stringify) { @@ -766,17 +767,18 @@ var keyname = 'telebit-remote'; state.keystore = keystore; state.keystoreSecure = !keystore.insecure; keystore.get(keyname).then(function (key) { - if (key && key.kty) { + if (key && key.kty && key.kid) { state.key = key; + state.pub = keypairs.neuter({ jwk: key }); fs.readFile(confpath, 'utf8', parseConfig); return; } - var keypairs = require('keypairs'); return keypairs.generate().then(function (pair) { var jwk = pair.private; - return keystore.set(keyname, jwk).then(function () { - return keypairs.thumbprint({ jwk: pair.public }).then(function (kid) { + return keypairs.thumbprint({ jwk: pair.public }).then(function (kid) { + jwk.kid = kid; + return keystore.set(keyname, jwk).then(function () { var size = (jwk.crv || Buffer.from(jwk.n, 'base64').byteLength * 8); console.info("Generated new %s %s private key with thumbprint %s", jwk.kty, size, kid); state.key = jwk; diff --git a/bin/telebitd.js b/bin/telebitd.js index 7d7c4c5..9f5e622 100755 --- a/bin/telebitd.js +++ b/bin/telebitd.js @@ -11,7 +11,7 @@ try { var pkg = require('../package.json'); -var url = require('url'); +//var url = require('url'); var path = require('path'); var os = require('os'); var fs = require('fs'); @@ -374,46 +374,123 @@ controllers.relay = function (req, res) { }); }; +function jsonEggspress(req, res, next) { + /* + var opts = url.parse(req.url, true); + if (false && opts.query._body) { + try { + req.body = JSON.parse(decodeURIComponent(opts.query._body, true)); + } catch(e) { + res.statusCode = 500; + res.end('{"error":{"message":"?_body={{bad_format}}"}}'); + return; + } + } + */ + + var hasLength = req.headers['content-length'] > 0; + if (!hasLength && !req.headers['content-type']) { + next(); + return; + } + + var body = ''; + req.on('readable', function () { + var data; + while (true) { + data = req.read(); + if (!data) { break; } + body += data.toString(); + } + }); + req.on('end', function () { + try { + req.body = JSON.parse(body); + } catch(e) { + res.statusCode = 400; + res.end('{"error":{"message":"POST body is not valid json"}}'); + return; + } + next(); + }); +} + +function decodeJwt(jwt) { + var parts = jwt.split('.'); + var jws = { + protected: parts[0] + , payload: parts[0] + , signature: parts[2] //Buffer.from(parts[2], 'base64') + }; + jws.header = JSON.parse(Buffer.from(jws.protected, 'base64')); + jws.claims = JSON.parse(Buffer.from(jws.payload, 'base64')); + return jws; +} +function jwtEggspress(req, res, next) { + var jwt = (req.headers.authorization||'').replace(/Bearer /i, ''); + if (!jwt) { next(); return; } + + try { + req.jwt = decodeJwt(jwt); + } catch(e) { + // ignore + } + + // TODO verify if possible + next(); +} + +function verifyJws(jwk, jws) { + return require('keypairs').export({ jwk: jwk }).then(function (pem) { + var alg = 'RSA-SHA' + jws.header.alg.replace(/[^\d]+/i, ''); + // XXX + // TODO check for public key in keytar + // XXX + return require('crypto') + .createVerify(alg) + .update(jws.protected + '.' + jws.payload) + .verify(pem, jws.signature, 'base64'); + }); +} + +function jwsEggspress(req, res, next) { + // TODO check header application/jose+json ?? + if (!req.body || !(req.body.protected && req.body.payload && req.body.signature)) { + next(); + return; + } + req.jws = req.body; + req.jws.header = JSON.parse(Buffer.from(req.jws.protected, 'base64')); + req.body = Buffer.from(req.jws.payload, 'base64'); + if ('{'.charCodeAt(0) === req.body[0] || '['.charCodeAt(0) === req.body[0]) { + req.body = JSON.parse(req.body); + } + if (req.jws.header.jwk) { + verifyJws(req.jws.header.jwk, req.jws).then(function (verified) { + req.jws.selfVerified = verified; + next(); + }); + return; + } + + // TODO verify if possible + next(); +} + function handleApi() { var app = eggspress(); + app.use('/', jwtEggspress); + app.use('/', jsonEggspress); + app.use('/', jwsEggspress); app.use('/', function (req, res, next) { - var opts = url.parse(req.url, true); - if (false && opts.query._body) { - try { - req.body = JSON.parse(decodeURIComponent(opts.query._body, true)); - } catch(e) { - res.statusCode = 500; - res.end('{"error":{"message":"?_body={{bad_format}}"}}'); - return; - } + if (req.jwt) { + console.log('jwt', req.jwt); + } else if (req.jws) { + console.log('jws', req.jws); + console.log('body', req.body); } - - var hasLength = req.headers['content-length'] > 0; - if (!hasLength && !req.headers['content-type']) { - next(); - return; - } - - var body = ''; - req.on('readable', function () { - var data; - while (true) { - data = req.read(); - if (!data) { break; } - body += data.toString(); - } - }); - req.on('end', function () { - try { - req.body = JSON.parse(body); - } catch(e) { - res.statusCode = 400; - res.end('{"error":{"message":"POST body is not valid json"}}'); - return; - } - next(); - }); + next(); }); function listSuccess(req, res) { diff --git a/lib/eggspress.js b/lib/eggspress.js index 0979a29..1f76b63 100644 --- a/lib/eggspress.js +++ b/lib/eggspress.js @@ -5,7 +5,11 @@ module.exports = function eggspress() { var allPatterns = []; var app = function (req, res) { var patterns = allPatterns.slice(0).reverse(); - function next() { + function next(err) { + if (err) { + req.end(err.message); + return; + } var todo = patterns.pop(); if (!todo) { console.log('[eggspress] Did not match any patterns', req.url); diff --git a/lib/rc/index.js b/lib/rc/index.js index aa1f3d0..d418da0 100644 --- a/lib/rc/index.js +++ b/lib/rc/index.js @@ -3,9 +3,11 @@ var os = require('os'); var path = require('path'); var http = require('http'); +var keypairs = require('keypairs'); var common = require('../cli-common.js'); +/* function packConfig(config) { return Object.keys(config).map(function (key) { var val = config[key]; @@ -22,6 +24,7 @@ function packConfig(config) { return key + ':' + val; // converts arrays to strings with , }); } +*/ module.exports.create = function (state) { common._init( @@ -72,16 +75,20 @@ module.exports.create = function (state) { RC.request = function request(opts, fn) { if (!opts) { opts = {}; } var service = opts.service || 'config'; + /* var args = opts.data; if (args && 'control' === service) { args = packConfig(args); } - var json = JSON.stringify(args); + var json = JSON.stringify(opts.data); + */ var url = '/rpc/' + service; + /* if (json) { url += ('?_body=' + encodeURIComponent(json)); } - var method = opts.method || (args && 'POST') || 'GET'; + */ + var method = opts.method || (opts.data && 'POST') || 'GET'; var reqOpts = { method: method , path: url @@ -124,11 +131,33 @@ module.exports.create = function (state) { fn(err); }); - if ('POST' === method && opts.data) { - req.setHeader("content-type", 'application/json'); - req.write(json || opts.data); + + // Simple GET + if ('POST' !== method || !opts.data) { + return keypairs.signJwt({ + jwk: state.key + , claims: { iss: false, exp: Math.round(Date.now()/1000) + (15 * 60) } + //TODO , exp: '15m' + }).then(function (jwt) { + req.setHeader("authorization", 'bearer ' + jwt); + req.end(); + }); } - req.end(); + + return keypairs.signJws({ + jwk: state.key + , protected: { + // alg will be filled out automatically + jwk: state.pub + , nonce: require('crypto').randomBytes(16).toString('hex') // TODO get from server + , url: 'https://' + reqOpts.host + reqOpts.path + } + , payload: JSON.stringify(opts.data) + }).then(function (jws) { + req.setHeader("content-type", 'application/json'); + req.write(JSON.stringify(jws)); + req.end(); + }); }; return RC; }; diff --git a/package-lock.json b/package-lock.json index f8adae0..628f60f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -430,9 +430,9 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "keypairs": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.5.tgz", - "integrity": "sha512-VKUxQ4iQB5LvVMtObOzNmZRfgXLTr5GMr+wg9A2BnILArBLrtg/DIuWRJQpDNRRfAGRQjHXxSVOW+7xpzIAY1Q==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.6.tgz", + "integrity": "sha512-sJDaZvJqHWUawJjrOGKJvKGLfPh0eo2WV7td4RSL88w3BjPYCYI9PkqBn0hLqc6uw0HFSqZMikhGn/jgPpcWnQ==", "requires": { "eckles": "^1.4.1", "rasha": "^1.2.4" diff --git a/package.json b/package.json index 29df0ee..155dae4 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "finalhandler": "^1.1.1", "greenlock": "^2.6.7", "js-yaml": "^3.11.0", - "keypairs": "^1.2.5", + "keypairs": "^1.2.6", "mkdirp": "^0.5.1", "proxy-packer": "^2.0.2", "ps-list": "^5.0.0",