diff --git a/lib/primary.js b/lib/primary.js index c76a80af9a5723d98ecb53bf3e18943315e21be2..e47f13fa7109b8a983a6fbf0909f4c93f3b37923 100644 --- a/lib/primary.js +++ b/lib/primary.js @@ -173,6 +173,15 @@ if (process.env['SHIMMED_PRIMARIES']) { }); } +exports.getPublicKey = function(domain, cb) { + exports.checkSupport(domain, function(err, rv) { + if (err) return cb(err); + var pubKey = g_cache[domain].publicKey; + if (!pubKey) return cb("can't get public key for " + domain); + cb(null, pubKey); + }); +}; + // verify an assertion generated to authenticate to browserid exports.verifyAssertion = function(assertion, cb) { try { @@ -188,10 +197,8 @@ exports.verifyAssertion = function(assertion, cb) { if (issuer === config.get('hostname')) { cb("cannot authenticate to browserid with a certificate issued by it."); } else { - exports.checkSupport(issuer, function(err, rv) { + exports.getPublicKey(issuer, function(err, pubKey) { if (err) return cb(err); - var pubKey = g_cache[issuer].publicKey; - if (!pubKey) return cb("can't get public key for " + issuer); next(pubKey); }); } @@ -213,4 +220,4 @@ exports.verifyAssertion = function(assertion, cb) { cb("can't verify assertion: " + e.toString()); } }, cb); -}; \ No newline at end of file +}; diff --git a/lib/verifier/certassertion.js b/lib/verifier/certassertion.js index 0a954a76c72f07e8ccc9013318d6a9fac7b4f807..e41345af9407ca21dde3d98fc405b8cb778c1f42 100644 --- a/lib/verifier/certassertion.js +++ b/lib/verifier/certassertion.js @@ -47,7 +47,8 @@ jwcert = require("jwcrypto/jwcert"), vep = require("jwcrypto/vep"), config = require("../configuration.js"), logger = require("../logging.js").logger, -secrets = require('../secrets.js'); +secrets = require('../secrets.js'), +primary = require('../primary.js'); try { const publicKey = secrets.loadPublicKey(); @@ -126,12 +127,29 @@ function verify(assertion, audience, successCB, errorCB) { return errorCB("malformed assertion"); } + var ultimateIssuer; + jwcert.JWCert.verifyChain( bundle.certificates, new Date(), function(issuer, next) { + // update issuer with each issuer in the chain, so the + // returned issuer will be the last cert in the chain + ultimateIssuer = issuer; + // allow other retrievers for testing if (issuer === config.get('hostname')) return next(publicKey); - return errorCB("this verifier doesn't respect certs issued from domains other than: " + config.get('hostname')); + + // XXX: this network work happening inside a compute process. + // if we have a large number of requests to auth assertions that require + // keyfetch, this could theoretically hurt our throughput. We could + // move the fetch up into the browserid process and pass it into the + // compute process at some point. + + // let's go fetch the public key for this host + primary.getPublicKey(issuer, function(err, pubKey) { + if (err) return errorCB(err); + next(pubKey); + }); }, function(pk, principal) { var tok = new jwt.JWT(); tok.parse(bundle.assertion); @@ -144,8 +162,18 @@ function verify(assertion, audience, successCB, errorCB) { return errorCB("audience mismatch: " + err); } + // verify that the issuer is the same as the email domain + // NOTE: for "delegation of authority" support we'll need to make this check + // more sophisticated + var domainFromEmail = principal.email.replace(/^.*@/, ''); + if (ultimateIssuer != config.get('hostname') && ultimateIssuer !== domainFromEmail) + { + return errorCB("issuer issue '" + ultimateIssuer + "' may not speak for emails from '" + + domainFromEmail + "'"); + } + if (tok.verify(pk)) { - successCB(principal.email, tok.audience, tok.expires, config.get('hostname')); + successCB(principal.email, tok.audience, tok.expires, ultimateIssuer); } else { errorCB("verification failure"); } diff --git a/tests/verifier-test.js b/tests/verifier-test.js index 1cf64f544483fe8829b246fadbc563a2cc69ecfd..2d90a5ff31c0e7793b0bb6dd34451fa4c745e072 100755 --- a/tests/verifier-test.js +++ b/tests/verifier-test.js @@ -49,7 +49,8 @@ jwt = require('jwcrypto/jwt.js'), vep = require('jwcrypto/vep.js'), jwcert = require('jwcrypto/jwcert.js'), http = require('http'), -querystring = require('querystring'); +querystring = require('querystring'), +path = require('path'); var suite = vows.describe('verifier'); @@ -657,10 +658,10 @@ suite.addBatch({ } }); -// now verify that no-one other than browserid is allowed to issue assertions -// (until primary support is implemented) +// now verify that assertions from a primary who does not have browserid support +// will fail to verify suite.addBatch({ - "generating an assertion from a cert signed by some other domain": { + "generating an assertion from a cert signed by a bogus primary": { topic: function() { var fakeDomainKeypair = jwk.KeyPair.generate("RS", 64); var newClientKeypair = jwk.KeyPair.generate("DS", 256); @@ -686,7 +687,85 @@ suite.addBatch({ "to return a clear error message": function (r, err) { var resp = JSON.parse(r.body); assert.strictEqual(resp.status, 'failure'); - assert.strictEqual(resp.reason, "this verifier doesn't respect certs issued from domains other than: 127.0.0.1"); + assert.strictEqual(resp.reason, "can't get public key for otherdomain.tld"); + } + } + } +}); + +// now verify that assertions from a primary who does have browserid support +// but has no authority to speak for an email address will fail +suite.addBatch({ + "generating an assertion from a cert signed by a real (simulated) primary": { + topic: function() { + var secretKey = jwk.SecretKey.fromSimpleObject( + JSON.parse(require('fs').readFileSync( + path.join(__dirname, '..', 'example', 'primary', 'sample.privatekey')))); + + var newClientKeypair = jwk.KeyPair.generate("DS", 256); + expiration = new Date(new Date().getTime() + (1000 * 60 * 60 * 6)); + var cert = new jwcert.JWCert("example.domain", expiration, new Date(), newClientKeypair.publicKey, + {email: TEST_EMAIL}).sign(secretKey); + + var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000)); + var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN); + return vep.bundleCertsAndAssertion([cert], tok.sign(newClientKeypair.secretKey)); + }, + "yields a good looking assertion": function (r, err) { + assert.isString(r); + assert.equal(r.length > 0, true); + }, + "will cause the verifier": { + topic: function(assertion) { + wsapi.post('/verify', { + audience: TEST_ORIGIN, + assertion: assertion + }).call(this); + }, + "to return a clear error message": function (r, err) { + var resp = JSON.parse(r.body); + assert.strictEqual(resp.status, 'failure'); + assert.strictEqual(resp.reason, "issuer issue 'example.domain' may not speak for emails from 'somedomain.com'"); + } + } + } +}); + +// now verify that assertions from a primary who does have browserid support +// and may speak for an email address will succeed +suite.addBatch({ + "generating an assertion from a cert signed by a real (simulated) primary": { + topic: function() { + var secretKey = jwk.SecretKey.fromSimpleObject( + JSON.parse(require('fs').readFileSync( + path.join(__dirname, '..', 'example', 'primary', 'sample.privatekey')))); + + var newClientKeypair = jwk.KeyPair.generate("DS", 256); + expiration = new Date(new Date().getTime() + (1000 * 60 * 60 * 6)); + var cert = new jwcert.JWCert("example.domain", expiration, new Date(), newClientKeypair.publicKey, + {email: "foo@example.domain"}).sign(secretKey); + + var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000)); + var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN); + return vep.bundleCertsAndAssertion([cert], tok.sign(newClientKeypair.secretKey)); + }, + "yields a good looking assertion": function (r, err) { + assert.isString(r); + assert.equal(r.length > 0, true); + }, + "will cause the verifier": { + topic: function(assertion) { + wsapi.post('/verify', { + audience: TEST_ORIGIN, + assertion: assertion + }).call(this); + }, + "to return a clear error message": function (r, err) { + var resp = JSON.parse(r.body); + assert.strictEqual(resp.status, 'okay'); + assert.strictEqual(resp.issuer, "example.domain"); + assert.strictEqual(resp.audience, TEST_ORIGIN); + assert.strictEqual(resp.email, "foo@example.domain"); } } }