diff --git a/lib/primary.js b/lib/primary.js index 67d8cf6dd66a4c1c04968a25a7c40a865a9a1c2d..aee2de492ff67ee70db326e1221f530e76f18b65 100644 --- a/lib/primary.js +++ b/lib/primary.js @@ -19,19 +19,53 @@ config = require("./configuration.js"); const WELL_KNOWN_URL = "/.well-known/browserid"; +// Protect from stack overflows and network DDOS attacks +const MAX_AUTHORITY_DELEGATIONS = 6; + const HOSTNAME = urlparse(config.get('public_url')).host; var g_shim_cache = {}; -function parseWellKnownBody(body, domain) { +// This becomes async +function parseWellKnownBody(body, domain, delegates, cb) { var v = JSON.parse(body); - const want = [ 'public-key', 'authentication', 'provisioning' ]; var got = Object.keys(v); - + var bail = false; + got.forEach(function (k) { + if ('authority' === k) { + // Recursion + var dels = Object.keys(delegates); + if (delegates[domain] !== undefined) { + // return to break out of function, but callbacks are actual program flow + bail = true; + return cb("Circular reference in delegating authority " + JSON.stringify(delegates)); + } + if (Object.keys(delegates).length > MAX_AUTHORITY_DELEGATIONS) { + bail = true; + return cb("Too many hops while delegating authority " + JSON.stringify(dels)); + } + logger.debug(domain,' is delegating to', v[k]); + // recurse into low level get /.well-known/browserid and parse again? + // If everything goes well, finally call our original callback + delegates[domain] = dels.length; + getWellKnown(v[k], delegates, function (err, nbody, ndomain, ndelegates) { + if (err) { + cb(err); + } + parseWellKnownBody(nbody, ndomain, ndelegates, cb); + }); + bail = true;; + } + }); + if (bail) return; want.forEach(function(k) { - if (-1 === got.indexOf(k)) throw "missing required key: " + k; + if (-1 === got.indexOf(k)) { + cb("missing required key: " + k); + bail = true; + } }); + if (bail) return; // Allow SHIMMED_PRIMARIES to change example.com into 127.0.0.1:10005 var url_prefix = 'https://' + domain; @@ -49,25 +83,13 @@ function parseWellKnownBody(body, domain) { urlparse(urls.prov).validate(); // parse the public key - return { + return cb(null, { publicKey: jwk.PublicKey.fromSimpleObject(v['public-key']), urls: urls - }; + }); } -exports.checkSupport = function(domain, cb) { - if (!cb) throw "missing required callback function"; - - if (config.get('disable_primary_support')) { - return process.nextTick(function() { cb(null, false); }); - } - if (typeof domain !== 'string' || !domain.length) { - return process.nextTick(function() { cb("invalid domain"); }); - } - - getWellKnown(domain, cb); -}; // Support "shimmed primaries" for local development. That is an environment variable that is any number of // CSV values of the form: @@ -93,9 +115,8 @@ if (process.env['SHIMMED_PRIMARIES']) { }); } -var getWellKnown = function (domain, cb) { +var getWellKnown = function (domain, delegates, cb) { function handleResponse(res) { - var msg; if (res.statusCode !== 200) { logger.debug(domain + ' is not a browserid primary - non-200 response code to ' + WELL_KNOWN_URL); return cb(null, false, null); @@ -108,22 +129,12 @@ var getWellKnown = function (domain, cb) { var body = ""; res.on('data', function(chunk) { body += chunk; }); res.on('end', function() { - try { - var r = parseWellKnownBody(body, domain); - logger.info(domain + ' is a valid browserid primary'); - return cb(null, r.urls, r.publicKey); - } catch(e) { - msg = domain + ' is a broken browserid primary, malformed dec of support: ' + e.toString(); - logger.debug(msg); - return cb(msg); - } + cb(null, body, domain, delegates); }); }; if (g_shim_cache[domain]) { - var body = g_shim_cache[domain].body, - r = parseWellKnownBody(body, domain); - return cb(null, r.urls, r.publicKey); + return cb(null, g_shim_cache[domain].body, domain, delegates); } // now we need to check to see if domain purports to being a primary @@ -156,6 +167,48 @@ var getWellKnown = function (domain, cb) { }); }; +exports.checkSupport = function(domain, cb, delegates) { + + // Delegates will be populatd via recursion to detect cycles + if (! delegates) { + delegates = {}; + } + if (!cb) throw "missing required callback function"; + + if (config.get('disable_primary_support')) { + return process.nextTick(function() { cb(null, false); }); + } + + if (typeof domain !== 'string' || !domain.length) { + return process.nextTick(function() { cb("invalid domain"); }); + } + getWellKnown(domain, delegates, function (err, body, domain, delegates) { + if (err) { + logger.debug(err); + return cb(err); + } else { + try { + var r = parseWellKnownBody(body, domain, delegates, function (err, r) { + if (err) { + logger.debug(err); + cb(err); + } else { + logger.info(domain + ' is a valid browserid primary'); + return cb(null, r.urls, r.publicKey); + } + + }); + + } catch(e) { + var msg = domain + ' is a broken browserid primary, malformed dec of support: ' + e.toString(); + logger.debug(msg); + return cb(msg); + } + } + }); +}; + + exports.getPublicKey = function(domain, cb) { exports.checkSupport(domain, function(err, urls, publicKey) { if (publicKey === null) { @@ -183,6 +236,7 @@ exports.verifyAssertion = function(assertion, cb) { if (issuer === HOSTNAME) { cb("cannot authenticate to browserid with a certificate issued by it."); } else { + exports.getPublicKey(issuer, function(err, pubKey) { if (err) return cb(err); next(pubKey); diff --git a/tests/data/cycle.domain/.well-known/browserid b/tests/data/cycle.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..3c3c21814d2d95774f093bbad0186d9d4826a49c --- /dev/null +++ b/tests/data/cycle.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "cycle2.domain" } diff --git a/tests/data/cycle2.domain/.well-known/browserid b/tests/data/cycle2.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..22334d71fb7dd8cd2d6552612031c4b3ebae4361 --- /dev/null +++ b/tests/data/cycle2.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "cycle.domain" } \ No newline at end of file diff --git a/tests/data/delegate0.domain/.well-known/browserid b/tests/data/delegate0.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..d12273097e4a22993026cee7cfa2cbfefaa32a42 --- /dev/null +++ b/tests/data/delegate0.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "delegate1.domain" } diff --git a/tests/data/delegate1.domain/.well-known/browserid b/tests/data/delegate1.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..f5af2e8e81a0f1584f587f7f312263b661c26843 --- /dev/null +++ b/tests/data/delegate1.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "delegate2.domain" } diff --git a/tests/data/delegate10.domain/.well-known/browserid b/tests/data/delegate10.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..88c678cea1948dbba0dc7b505d8263d9b008158f --- /dev/null +++ b/tests/data/delegate10.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "delegate11.domain" } diff --git a/tests/data/delegate2.domain/.well-known/browserid b/tests/data/delegate2.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..a526a7552d37c4ca6e3b05d3fd21f3a14c8c3ce9 --- /dev/null +++ b/tests/data/delegate2.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "delegate3.domain" } diff --git a/tests/data/delegate3.domain/.well-known/browserid b/tests/data/delegate3.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..41e59a5b1058520dc7ab6176fc325940b2f5cfc7 --- /dev/null +++ b/tests/data/delegate3.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "delegate4.domain" } diff --git a/tests/data/delegate4.domain/.well-known/browserid b/tests/data/delegate4.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..c43664eefb8c8666a77a5476e0df8f42d9d2bfc0 --- /dev/null +++ b/tests/data/delegate4.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "delegate5.domain" } diff --git a/tests/data/delegate5.domain/.well-known/browserid b/tests/data/delegate5.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..71ea103e56726f9b9cc284edc742919491002c76 --- /dev/null +++ b/tests/data/delegate5.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "delegate6.domain" } diff --git a/tests/data/delegate6.domain/.well-known/browserid b/tests/data/delegate6.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..1193daef7d0c4b8d69c4366ef039cea7e25fc674 --- /dev/null +++ b/tests/data/delegate6.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "delegate7.domain" } diff --git a/tests/data/delegate7.domain/.well-known/browserid b/tests/data/delegate7.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..811565beed12687518288b44f14188902cbb9791 --- /dev/null +++ b/tests/data/delegate7.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "delegate8.domain" } diff --git a/tests/data/delegate8.domain/.well-known/browserid b/tests/data/delegate8.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..78e2041bb5f96ad97c65a034fbcc4c27962961a1 --- /dev/null +++ b/tests/data/delegate8.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "delegate9.domain" } diff --git a/tests/data/delegate9.domain/.well-known/browserid b/tests/data/delegate9.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..8189ff31c984a8cbca1a69bbe6c8ffc9f59a1a96 --- /dev/null +++ b/tests/data/delegate9.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "delegate10.domain" } diff --git a/tests/data/hozed.domain/.well-known/browserid b/tests/data/hozed.domain/.well-known/browserid new file mode 100644 index 0000000000000000000000000000000000000000..936d032fc0b4c3da6b6a053bcf775b744dbe22c7 --- /dev/null +++ b/tests/data/hozed.domain/.well-known/browserid @@ -0,0 +1 @@ +{ "authority": "hozed.domain" } diff --git a/tests/delegated-primary-test.js b/tests/delegated-primary-test.js new file mode 100644 index 0000000000000000000000000000000000000000..a9a286a231c8866d6a9ff8cc2f4a5acfbe6a0b22 --- /dev/null +++ b/tests/delegated-primary-test.js @@ -0,0 +1,123 @@ +require('./lib/test_env.js'); + +const +assert = require('assert'), +vows = require('vows'), +path = require('path'), +util = require('util'); + +const TEST_DOMAIN = 'example.domain', + TEST_DOMAIN_PATH = path.join(__dirname, + '..', 'example', 'primary', '.well-known', 'browserid'), + TEST_ORIGIN = 'http://127.0.0.1:10002', + TEST_DELEGATE_DOMAIN = 'delegate.example.domain', + TEST_DELEGATE_DOMAIN_PATH = path.join(__dirname, + '..', 'example', 'delegated_primary', '.well-known', 'browserid'); + +// Good examples +process.env['SHIMMED_PRIMARIES'] = + 'example.domain|http://127.0.0.1:10005|' + TEST_DOMAIN_PATH; +process.env['SHIMMED_PRIMARIES'] += "," + + 'delegate.example.domain|http://127.0.0.1:10005|' + TEST_DELEGATE_DOMAIN_PATH; + +// A series of redirects delegate0.domain -> delegate1.domain -> ... delegate11.domain +function mk_delegate(i) { + var f = util.format; + var p = path.join(__dirname, 'data', f('delegate%s.domain', i), '.well-known', 'browserid'); + process.env['SHIMMED_PRIMARIES'] += "," + + f("delegate%s.domain|http://127.0.0.1:10005|%s", i, p); +} +for (var i=0; i <= 10; i++) { + mk_delegate(i); +} + +// delegates to hozed.domain +process.env['SHIMMED_PRIMARIES'] += "," + + util.format("hozed.domain|http://127.0.0.1:10005|%s", path.join(__dirname, 'data', + 'hozed.domain', '.well-known', 'browserid')); + +// Next two delegate to each other forming a cycle +process.env['SHIMMED_PRIMARIES'] += "," + + util.format("cycle.domain|http://127.0.0.1:10005|%s", path.join(__dirname, 'data', + 'cycle.domain', '.well-known', 'browserid')); + +process.env['SHIMMED_PRIMARIES'] += "," + + util.format("cycle2.domain|http://127.0.0.1:10005|%s", path.join(__dirname, 'data', + 'cycle2.domain', '.well-known', 'browserid')); + + +var primary = require('../lib/primary.js'); + +var suite = vows.describe('delegated-primary'); + +// DB test look + +// Tests related to domains that delegate their authority to another +// primary. + +// now let's generate an assertion using this user + +suite.addBatch({ + "Retrieving a public key is straight forward": { + topic: function() { + return primary.getPublicKey(TEST_DOMAIN, this.callback); + }, + "succeeds": function(err, pubKey) { + assert.equal(pubKey.keysize, '256'); + assert.equal(pubKey.algorithm, 'RS'); + } + } +}); + +suite.addBatch({ + "Retrieving a public key should follow authority delegation": { + topic: function() { + return primary.getPublicKey(TEST_DELEGATE_DOMAIN, this.callback); + }, + "succeeds": function(err, pubKey) { + assert.equal(pubKey.keysize, '256'); + assert.equal(pubKey.algorithm, 'RS'); + } + } +}); + +suite.addBatch({ + "Cycles should be detected": { + topic: function() { + return primary.getPublicKey('cycle.domain', this.callback); + }, + "succeeds": function(err, pubKey) { + assert.strictEqual(err, + 'Circular reference in delegating authority {"cycle.domain":0,"cycle2.domain":1}'); + } + } +}); + +suite.addBatch({ + "We should not follow an infinite series of delegations of authority": { + topic: function() { + return primary.getPublicKey('delegate0.domain', this.callback); + }, + "succeeds": function(err, pubKey) { + assert.strictEqual(err, + 'Too many hops while delegating authority ["delegate0.domain","delegate1.domain",' + + '"delegate2.domain","delegate3.domain","delegate4.domain","delegate5.domain",' + + '"delegate6.domain"]'); + } + } +}); + +suite.addBatch({ + "A domain delegating to itself is hozed...": { + topic: function() { + return primary.getPublicKey('hozed.domain', this.callback); + }, + "succeeds": function(err, pubKey) { + assert.strictEqual(err.indexOf('Circular reference in delegating authority '), 0); + } + } +}); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module);