diff --git a/bin/verifier b/bin/verifier index 1d9f48e6097486e866f80dd2e31ce9d84aeb725c..4499256930558eed528919976ddd7a576ada1498 100755 --- a/bin/verifier +++ b/bin/verifier @@ -88,7 +88,7 @@ app.post('/verify', function(req, resp, next) { } catch (e) { return resp.json({ status: "failure", reason: "Content-Type expected to be one of: " + want_ct.join(", ") }, 415); } - return resp.json({ status: "failure", reason: "need assertion and audience" }, 400); + return resp.json({ status: "failure", reason: "need assertion and audience"}, 400); } var startTime = new Date(); diff --git a/lib/keysigner/ca.js b/lib/keysigner/ca.js index bc7be67f663cd32d6f7ce11b245021477a0f73cf..5e95fef85edcc18555b9e8774651e7e553c4eefe 100644 --- a/lib/keysigner/ca.js +++ b/lib/keysigner/ca.js @@ -4,15 +4,18 @@ // certificate authority -var jwcert = require('jwcrypto/jwcert'), - jwk = require('jwcrypto/jwk'), - jws = require('jwcrypto/jws'), +var jwcrypto = require('jwcrypto'), + cert = jwcrypto.cert, path = require("path"), fs = require("fs"), secrets = require('../secrets.js'), logger = require('../logging.js').logger, urlparse = require('urlparse'); +// load up the right algorithms +require("jwcrypto/lib/algs/rs"); +require("jwcrypto/lib/algs/ds"); + try { const secret_key = secrets.loadSecretKey(); const public_key = secrets.loadPublicKey(); @@ -22,37 +25,58 @@ try { } function parsePublicKey(serializedPK) { - return jwk.PublicKey.deserialize(serializedPK); -} - -function parseCert(serializedCert) { - var cert = new jwcert.JWCert(); - cert.parse(serializedCert); - return cert; + return jwcrypto.loadPublicKey(serializedPK); } -function certify(hostname, email, publicKey, expiration) { +function certify(hostname, email, publicKey, expiration, cb) { if (expiration == null) - throw "expiration cannot be null"; - return new jwcert.JWCert(hostname, expiration, new Date(), publicKey, {email: email}).sign(secret_key); + return cb("expiration cannot be null"); + + cert.sign(publicKey, {email: email}, + {issuer: hostname, issuedAt: new Date(), expiresAt: expiration}, + null, + secret_key, cb); } +// hostname is issuer +// certChain is an array of raw certs +// the cb is called with the last public key and principal function verifyChain(hostname, certChain, cb) { - // raw certs - return jwcert.JWCert.verifyChain( + return cert.verifyChain( certChain, new Date(), function(issuer, next) { // for now we only do browserid.org issued keys if (issuer != hostname) - return next(null); + return next("only verifying " + hostname + "-issued keys"); + + next(null, exports.PUBLIC_KEY); + }, function(err, certParamsArray) { + if (err) return cb(err); + + var lastParams = certParamsArray[certParamsArray.length - 1]; + cb(null, lastParams.certParams['public-key'], lastParams.certParams.principal, certParamsArray); + }); +} + +function verifyBundle(hostname, bundle, cb) { + return cert.verifyBundle( + bundle, new Date(), + function(issuer, next) { + // for now we only do browserid.org issued keys + if (issuer != hostname) + return next("only verifying " + hostname + "-issued keys"); + + next(null, exports.PUBLIC_KEY); + }, function(err, certParamsArray, payload, assertionParams) { + if (err) return cb(err); - next(exports.PUBLIC_KEY); - }, cb); + cb(null, certParamsArray, payload, assertionParams); + }); } // exports, not the key stuff exports.certify = certify; exports.verifyChain = verifyChain; +exports.verifyBundle = verifyBundle; exports.parsePublicKey = parsePublicKey; -exports.parseCert = parseCert; exports.PUBLIC_KEY = public_key; diff --git a/lib/keysigner/keysigner-compute.js b/lib/keysigner/keysigner-compute.js index 5a64adb0c92e6e8172a70b4ff51946097ad4a7f4..39e761756dd554998fc83a238f8c0c0454033e88 100644 --- a/lib/keysigner/keysigner-compute.js +++ b/lib/keysigner/keysigner-compute.js @@ -14,8 +14,12 @@ process.on('message', function(m) { // we certify it for a day for now var expiration = new Date(); expiration.setTime(new Date().valueOf() + m.validityPeriod); - var cert = ca.certify(m.hostname, m.email, pk, expiration); - process.send({"success": cert}); + ca.certify(m.hostname, m.email, pk, expiration, function(err, cert) { + if (err) + return process.send({"error": err}); + + process.send({"success": cert}); + }); } catch(e) { process.send({"error": e ? e.toString() : "unknown"}); } diff --git a/lib/primary.js b/lib/primary.js index 55861ece63a6614d02107bf4b4927705d3031e83..060d199cca3fb1a6c327d9b9aba3a0ad197bc5c0 100644 --- a/lib/primary.js +++ b/lib/primary.js @@ -11,12 +11,13 @@ https = require('https'), http = require('http'), logger = require('./logging.js').logger, urlparse = require('urlparse'), -jwk = require('jwcrypto/jwk'), -jwcert = require("jwcrypto/jwcert"), -vep = require("jwcrypto/vep"), -jwt = require("jwcrypto/jwt"), +jwcrypto = require("jwcrypto"), config = require("./configuration.js"); +// alg +require("jwcrypto/lib/algs/rs"); +require("jwcrypto/lib/algs/ds"); + const WELL_KNOWN_URL = "/.well-known/browserid"; // Protect from stack overflows and network DDOS attacks @@ -88,7 +89,7 @@ function parseWellKnownBody(body, domain, delegates, cb) { // parse the public key return cb(null, { - publicKey: jwk.PublicKey.fromSimpleObject(v['public-key']), + publicKey: jwcrypto.loadPublicKeyFromObject(v['public-key']), urls: urls }); } @@ -232,40 +233,32 @@ exports.verifyAssertion = function(assertion, cb) { return process.nextTick(function() { cb("primary support disabled") }); } - try { - var bundle = vep.unbundleCertsAndAssertion(assertion); - } catch(e) { - return process.nextTick(function() { cb("malformed assertion: " + e); }); - } - jwcert.JWCert.verifyChain( - bundle.certificates, - new Date(), function(issuer, next) { - // issuer cannot be the browserid - 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); - }); - } - }, function(pk, principal) { - try { - var tok = new jwt.JWT(); - tok.parse(bundle.assertion); - - // audience must be browserid itself - var want = urlparse(config.get('public_url')).originOnly(); - var got = urlparse(tok.audience).originOnly(); + var getRoot = function(issuer, next) { + // issuer cannot be the browserid + if (issuer === HOSTNAME) { + next("cannot authenticate to browserid with a certificate issued by it."); + } else { + exports.getPublicKey(issuer, function(err, pubKey) { + if (err) return next(err); + next(null, pubKey); + }); + } + }; + + // verify the assertion bundle + var now = new Date(); + jwcrypto.cert.verifyBundle(assertion, now, getRoot, function(err, certParamsArray, payload, assertionParams) { + if (err) return cb(err); + + // audience must be browserid itself + var want = urlparse(config.get('public_url')).originOnly(); + var got = urlparse(assertionParams.audience).originOnly(); + + if (want.toString() !== got.toString()) { + return cb("can't log in with an assertion for '" + got.toString() + "'"); + } - if (want.toString() !== got.toString()) { - return cb("can't log in with an assertion for '" + got.toString() + "'"); - } - if (!tok.verify(pk)) throw "verification failure"; - cb(null, principal.email); - } catch(e) { - cb("can't verify assertion: " + e.toString()); - } - }, cb); + // all is well, get the principal from the last cert + cb(null, certParamsArray[certParamsArray.length-1].certParams.principal.email); + }); }; diff --git a/lib/secrets.js b/lib/secrets.js index 4b854c3436d02d0238fb84823851dad088d367ba..eb674481e89d6b0487284c8c9597d6e4027e3988 100644 --- a/lib/secrets.js +++ b/lib/secrets.js @@ -5,8 +5,7 @@ const path = require('path'), fs = require('fs'), -jwk = require('jwcrypto/jwk'), -jwt = require('jwcrypto/jwt'), +jwcrypto = require('jwcrypto'), Buffer = require('buffer').Buffer, crypto = require('crypto'); @@ -88,7 +87,7 @@ exports.loadSecretKey = function(name, dir) { } // parse it - return jwk.SecretKey.deserialize(secret); + return jwcrypto.loadSecretKey(secret); } function readAndParseCert(name, dir) { @@ -107,9 +106,8 @@ function readAndParseCert(name, dir) { // parse it // it should be a JSON structure with alg and serialized key // {alg: <ALG>, value: <SERIALIZED_KEY>} - var tok = new jwt.JWT(); - tok.parse(cert); - return JSON.parse(new Buffer(tok.payloadSegment, 'base64').toString()); + var payloadSegment = jwcrypto.extractComponents(cert).payloadSegment; + return JSON.parse(new Buffer(payloadSegment, 'base64').toString()); } exports.publicKeyCreationDate = function(name, dir) { @@ -117,5 +115,5 @@ exports.publicKeyCreationDate = function(name, dir) { }; exports.loadPublicKey = function(name, dir) { - return jwk.PublicKey.deserialize(JSON.stringify(readAndParseCert(name, dir)['public-key'])); + return jwcrypto.loadPublicKey(JSON.stringify(readAndParseCert(name, dir)['public-key'])); }; diff --git a/lib/static_resources.js b/lib/static_resources.js index 1bd4a35f5d3b5f1ff58547c758a79f6a089c32fa..456f5ce695f4539c95737bcf07bfbcd191af2bc8 100644 --- a/lib/static_resources.js +++ b/lib/static_resources.js @@ -19,7 +19,7 @@ var common_js = [ '/lib/jquery-1.7.1.min.js', '/lib/winchan.js', '/lib/underscore-min.js', - '/lib/vepbundle.js', + '/lib/bidbundle-min.js', '/lib/ejs.js', '/lib/micrajax.js', '/shared/javascript-extensions.js', @@ -117,7 +117,7 @@ exports.resources = { '/lib/jschannel.js', '/lib/winchan.js', '/lib/underscore-min.js', - '/lib/vepbundle.js', + '/lib/bidbundle-min.js', '/lib/hub.js', '/lib/micrajax.js', '/shared/javascript-extensions.js', diff --git a/lib/verifier/certassertion.js b/lib/verifier/certassertion.js index b437fe4f9abbc5ecbcb38b66ae71e21557ceda50..42816d69830de1f4fc12937d7b83957c4c192a50 100644 --- a/lib/verifier/certassertion.js +++ b/lib/verifier/certassertion.js @@ -6,16 +6,16 @@ const http = require("http"), https = require("https"), url = require("url"), -jwk = require("jwcrypto/jwk"), -jwt = require("jwcrypto/jwt"), -jwcert = require("jwcrypto/jwcert"), -vep = require("jwcrypto/vep"), +jwcrypto = require("jwcrypto"), config = require("../configuration.js"), logger = require("../logging.js").logger, secrets = require('../secrets.js'), primary = require('../primary.js'), urlparse = require('urlparse'); +require("jwcrypto/lib/algs/ds"); +require("jwcrypto/lib/algs/rs"); + try { const publicKey = secrets.loadPublicKey(); if (typeof publicKey !== 'object') throw "secrets.loadPublicKey() returns non-object, load failure"; @@ -89,23 +89,17 @@ function compareAudiences(want, got) { // audience is a web origin, e.g. https://foo.com or http://foo.org:81 function verify(assertion, audience, successCB, errorCB) { // assertion is bundle - try { - var bundle = vep.unbundleCertsAndAssertion(assertion); - } catch(e) { - return errorCB("malformed assertion"); - } - var ultimateIssuer; - jwcert.JWCert.verifyChain( - bundle.certificates, + jwcrypto.cert.verifyBundle( + assertion, 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 === HOSTNAME) return next(publicKey); + if (issuer === HOSTNAME) return next(null, publicKey); else if (config.get('disable_primary_support')) { return errorCB("this verifier doesn't respect certs issued from domains other than: " + HOSTNAME); @@ -120,35 +114,33 @@ function verify(assertion, audience, successCB, errorCB) { // let's go fetch the public key for this host primary.getPublicKey(issuer, function(err, pubKey) { if (err) return errorCB(err); - next(pubKey); + next(null, pubKey); }); - }, function(pk, principal) { - var tok = new jwt.JWT(); - tok.parse(bundle.assertion); - + }, function(err, certParamsArray, payload, assertionParams) { + if (err) return errorCB(err); + // audience must match! - var err = compareAudiences(tok.audience, audience) + var err = compareAudiences(assertionParams.audience, audience) if (err) { logger.debug("verification failure, audience mismatch: '" - + tok.audience + "' != '" + audience + "': " + err); + + assertionParams.audience + "' != '" + audience + "': " + err); return errorCB("audience mismatch: " + err); } + // principal is in the last cert + var principal = certParamsArray[certParamsArray.length - 1].certParams.principal; + // 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 != HOSTNAME && ultimateIssuer !== domainFromEmail) { - return errorCB("issuer issue '" + ultimateIssuer + "' may not speak for emails from '" + return errorCB("issuer '" + ultimateIssuer + "' may not speak for emails from '" + domainFromEmail + "'"); } - if (tok.verify(pk)) { - successCB(principal.email, tok.audience, tok.expires, ultimateIssuer); - } else { - errorCB("verification failure"); - } + successCB(principal.email, assertionParams.audience, assertionParams.expiresAt, ultimateIssuer); }, errorCB); }; diff --git a/package.json b/package.json index e75ed621df3f3b3e85b3a9424434bfd53677ba22..4da780272d92c3946e403909f1d319763c90285c 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "etagify": "0.0.2", "express": "2.5.0", "iconv": "1.1.3", - "jwcrypto": "0.1.1", "mustache": "0.3.1-dev", + "jwcrypto": "https://github.com/mozilla/jwcrypto/tarball/43ef3846813882cb", "mysql": "0.9.5", "node-gettext": "0.1.1", "node-statsd": "https://github.com/downloads/lloyd/node-statsd/0509f85.tgz", diff --git a/resources/static/lib/bidbundle-min.js b/resources/static/lib/bidbundle-min.js new file mode 120000 index 0000000000000000000000000000000000000000..1e002cab82753cef375b961d7d590f706edb00a7 --- /dev/null +++ b/resources/static/lib/bidbundle-min.js @@ -0,0 +1 @@ +../../../node_modules/jwcrypto/bidbundle-min.js \ No newline at end of file diff --git a/resources/static/lib/vepbundle.js b/resources/static/lib/vepbundle.js deleted file mode 120000 index 99cb32376ee635d267c21a3ede971c60fff554f1..0000000000000000000000000000000000000000 --- a/resources/static/lib/vepbundle.js +++ /dev/null @@ -1 +0,0 @@ -../../../node_modules/jwcrypto/vepbundle.js \ No newline at end of file diff --git a/resources/static/shared/browserid.js b/resources/static/shared/browserid.js index 34481fb5c7a0a74fff17cce319daf5134d49d546..5f662387697380a5bbeb9639d4d2260842fa1be3 100644 --- a/resources/static/shared/browserid.js +++ b/resources/static/shared/browserid.js @@ -9,7 +9,11 @@ // Define some constants. _.extend(window.BrowserID, { - // always use 1024 DSA keys - see issue #1293 + // always use 1024/160 DSA keys - see issue #1293 + // this used to be called keysize 128, but that made + // no sense since no component of this is 128 bits + // so making this 160 as per DSA 1024/160 + // EXCEPT, for backwards compatibility this is still 128 for now KEY_LENGTH: 128 }); }()); diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js index 45f422fbe7aa32b59fddc09e9cdd636171efd798..b56ef5684c9e539ba1b9ef0069fe15f51b4df948 100644 --- a/resources/static/shared/network.js +++ b/resources/static/shared/network.js @@ -47,7 +47,8 @@ BrowserID.Network = (function() { // seed the PRNG // FIXME: properly abstract this out, probably by exposing a jwcrypto // interface for randomness - require("./libs/all").sjcl.random.addEntropy(result.random_seed); + // require("./libs/all").sjcl.random.addEntropy(result.random_seed); + // FIXME: this wasn't doing anything for real, so commenting this out for now } function withContext(cb, onFailure) { diff --git a/resources/static/shared/provisioning.js b/resources/static/shared/provisioning.js index 4eba6b1561caf66f590a4b528f72de6335344439..c0fc73410f5bcc6b137a50965ee3efd6106c96c3 100644 --- a/resources/static/shared/provisioning.js +++ b/resources/static/shared/provisioning.js @@ -6,7 +6,7 @@ BrowserID.Provisioning = (function() { "use strict"; - var jwk = require("./jwk"); + var jwcrypto = require("./lib/jwcrypto"); var Provisioning = function(args, successCB, failureCB) { function tearDown() { @@ -69,8 +69,10 @@ BrowserID.Provisioning = (function() { }); chan.bind('genKeyPair', function(trans, s) { - keypair = jwk.KeyPair.generate("DS", BrowserID.KEY_LENGTH); - return keypair.publicKey.toSimpleObject(); + trans.delayReturn(true); + jwcrypto.generateKeypair({algorithm: "DS", keysize: BrowserID.KEY_LENGTH}, function(err, keypair) { + trans.complete(keypair.publicKey.toSimpleObject()); + }); }); chan.bind('raiseProvisioningFailure', function(trans, s) { diff --git a/resources/static/shared/storage.js b/resources/static/shared/storage.js index 2233ea73c84121624db6fe6cc10b204f0b0ccb87..0447082ac25d516a7d4fd8a0c6398ac66e23deaa 100644 --- a/resources/static/shared/storage.js +++ b/resources/static/shared/storage.js @@ -5,7 +5,7 @@ BrowserID.Storage = (function() { "use strict"; - var jwk, + var jwcrypto, ONE_DAY_IN_MS = (1000 * 60 * 60 * 24), storage; @@ -26,8 +26,8 @@ BrowserID.Storage = (function() { } function prepareDeps() { - if (!jwk) { - jwk = require("./jwk"); + if (!jwcrypto) { + jwcrypto = require("./jwcrypto"); } } @@ -125,9 +125,10 @@ BrowserID.Storage = (function() { storage.tempKeypair = null; if (raw_kp) { prepareDeps(); - var kp = new jwk.KeyPair(); - kp.publicKey = jwk.PublicKey.fromSimpleObject(raw_kp.publicKey); - kp.secretKey = jwk.SecretKey.fromSimpleObject(raw_kp.secretKey); + + var kp = {}; + kp.publicKey = jwcrypto.loadPublicKeyFromObject(raw_kp.publicKey); + kp.secretKey = jwcrypto.loadSecretKeyFromObject(raw_kp.secretKey); return kp; } else { return null; diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js index fb95b25567fa639e3bee1fe83d68ea4cd919600f..5bf0599972680e19aa29b35da6bd99503feaa3fa 100644 --- a/resources/static/shared/user.js +++ b/resources/static/shared/user.js @@ -7,7 +7,7 @@ BrowserID.User = (function() { "use strict"; - var jwk, jwt, vep, jwcert, origin, + var jwcrypto, origin, bid = BrowserID, network = bid.Network, storage = bid.Storage, @@ -19,11 +19,8 @@ BrowserID.User = (function() { registrationComplete = false; function prepareDeps() { - if (!jwk) { - jwk= require("./jwk"); - jwt = require("./jwt"); - vep = require("./vep"); - jwcert = require("./jwcert"); + if (!jwcrypto) { + jwcrypto= require("./lib/jwcrypto"); } } @@ -36,14 +33,14 @@ BrowserID.User = (function() { // if the certificate expires in less that 5 minutes from now. function isExpired(cert) { // if it expires in less than 2 minutes, it's too old to use. - var diff = cert.expires.valueOf() - serverTime.valueOf(); + var diff = cert.payload.exp.valueOf() - serverTime.valueOf(); if (diff < (60 * 2 * 1000)) { return true; } // or if it was issued before the last time the domain key // was updated, it's invalid - if (!cert.issued_at || cert.issued_at < creationTime) { + if (!cert.payload.iat || cert.payload.iat < creationTime) { return true; } @@ -55,7 +52,7 @@ BrowserID.User = (function() { prepareDeps(); _(emails).each(function(email_obj, email_address) { try { - email_obj.pub = jwk.PublicKey.fromSimpleObject(email_obj.pub); + email_obj.pub = jwcrypto.loadPublicKeyFromObject(email_obj.pub); } catch (x) { storage.invalidateEmail(email_address); return; @@ -67,8 +64,7 @@ BrowserID.User = (function() { } else { try { // parse the cert - var cert = new jwcert.JWCert(); - cert.parse(emails[email_address].cert); + var cert = jwcrypto.extractComponents(emails[email_address].cert); // check if this certificate is still valid. if (isExpired(cert)) { @@ -917,10 +913,9 @@ BrowserID.User = (function() { */ syncEmailKeypair: function(email, onComplete, onFailure) { prepareDeps(); - var keypair = jwk.KeyPair.generate("DS", bid.KEY_LENGTH); - setTimeout(function() { + jwcrypto.generateKeypair({algorithm: "DS", keysize: bid.KEY_LENGTH}, function(err, keypair) { certifyEmailKeypair(email, keypair, onComplete, onFailure); - }, 0); + }); }, @@ -946,22 +941,20 @@ BrowserID.User = (function() { function createAssertion(idInfo) { network.serverTime(function(serverTime) { - var sk = jwk.SecretKey.fromSimpleObject(idInfo.priv); - - // yield! - setTimeout(function() { - // assertions are valid for 2 minutes - var expirationMS = serverTime.getTime() + (2 * 60 * 1000); - var expirationDate = new Date(expirationMS); - var tok = new jwt.JWT(null, expirationDate, audience); - - // yield! - setTimeout(function() { - assertion = vep.bundleCertsAndAssertion([idInfo.cert], tok.sign(sk), true); + var sk = jwcrypto.loadSecretKeyFromObject(idInfo.priv); + + // assertions are valid for 2 minutes + var expirationMS = serverTime.getTime() + (2 * 60 * 1000); + var expirationDate = new Date(expirationMS); + + jwcrypto.assertion.sign( + {}, {audience: audience, expiresAt: expirationDate}, + sk, + function(err, signedAssertion) { + assertion = jwcrypto.cert.bundle([idInfo.cert], signedAssertion); storage.site.set(audience, "email", email); complete(assertion); - }, 0); - }, 0); + }); }, onFailure); } diff --git a/resources/static/test/cases/shared/user.js b/resources/static/test/cases/shared/user.js index 455736004a20fc4b675c16c70b135ba6d6b5fc01..c44fe0cd719b806924b091a621d1f2a0e0af5cfa 100644 --- a/resources/static/test/cases/shared/user.js +++ b/resources/static/test/cases/shared/user.js @@ -3,10 +3,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -var jwk = require("./jwk"); -var jwt = require("./jwt"); -var jwcert = require("./jwcert"); -var vep = require("./vep"); +var jwcrypto = require("./lib/jwcrypto"); (function() { var bid = BrowserID, @@ -33,20 +30,18 @@ var vep = require("./vep"); // Decode the assertion to a bundle. // var bundle = JSON.parse(window.atob(assertion)); // WOW, ^^ was assuming a specific format, let's fix that - var bundle = vep.unbundleCertsAndAssertion(assertion); + var bundle = jwcrypto.cert.unbundle(assertion); // Make sure both parts of the bundle exist - ok(bundle.certificates && bundle.certificates.length, "we have an array like object for the certificates"); - equal(typeof bundle.assertion, "string"); + ok(bundle.certs && bundle.certs.length, "we have an array like object for the certificates"); + equal(typeof bundle.signedAssertion, "string"); // Decode the assertion itself - var tok = new jwt.JWT(); - tok.parse(bundle.assertion); - + var components = jwcrypto.extractComponents(bundle.signedAssertion); // Check for parts of the assertion - equal(tok.audience, testOrigin, "correct audience"); - var expires = tok.expires.getTime(); + equal(components.payload.aud, testOrigin, "correct audience"); + var expires = parseInt(components.payload.exp); ok(typeof expires === "number" && !isNaN(expires), "expiration date is valid"); // this should be based on server time, not local time. @@ -58,9 +53,9 @@ var vep = require("./vep"); var diff = Math.abs(expires - nowPlus2Mins); ok(diff < 5000, "expiration date must be within 5 seconds of 2 minutes from now: " + diff); - equal(typeof tok.cryptoSegment, "string", "cryptoSegment exists"); - equal(typeof tok.headerSegment, "string", "headerSegment exists"); - equal(typeof tok.payloadSegment, "string", "payloadSegment exists"); + equal(typeof components.cryptoSegment, "string", "cryptoSegment exists"); + equal(typeof components.headerSegment, "string", "headerSegment exists"); + equal(typeof components.payloadSegment, "string", "payloadSegment exists"); if(cb) cb(); }); @@ -155,15 +150,15 @@ var vep = require("./vep"); asyncTest("createPrimaryUser with primary, user verified with primary - expect 'primary.verified'", function() { xhr.useResult("primary"); - provisioning.setStatus(provisioning.AUTHENTICATED); - - lib.createPrimaryUser({email: "unregistered@testuser.com"}, function(status) { - equal(status, "primary.verified", "primary user is already verified, correct status"); - network.checkAuth(function(authenticated) { - equal(authenticated, "assertion", "after provisioning user, user should be automatically authenticated to BrowserID"); - start(); - }); - }, testHelpers.unexpectedXHRFailure); + provisioning.setStatus(provisioning.AUTHENTICATED, function() { + lib.createPrimaryUser({email: "unregistered@testuser.com"}, function(status) { + equal(status, "primary.verified", "primary user is already verified, correct status"); + network.checkAuth(function(authenticated) { + equal(authenticated, "assertion", "after provisioning user, user should be automatically authenticated to BrowserID"); + start(); + }); + }, testHelpers.unexpectedXHRFailure); + }); }); asyncTest("createPrimaryUser with primary, user must authenticate with primary - expect 'primary.verify'", function() { diff --git a/resources/static/test/mocks/provisioning.js b/resources/static/test/mocks/provisioning.js index 1b3dc6b48f25c3b558f5c90e725ef5c58f5172a4..cdee24b1306b946483456f8fc2fad4e6ca16de3a 100644 --- a/resources/static/test/mocks/provisioning.js +++ b/resources/static/test/mocks/provisioning.js @@ -11,7 +11,7 @@ BrowserID.Mocks.Provisioning = (function() { // this cert is meaningless, but it has the right format cert = "eyJhbGciOiJSUzEyOCJ9.eyJpc3MiOiJpc3N1ZXIuY29tIiwiZXhwIjoxMzE2Njk1MzY3NzA3LCJwdWJsaWMta2V5Ijp7ImFsZ29yaXRobSI6IlJTIiwibiI6IjU2MDYzMDI4MDcwNDMyOTgyMzIyMDg3NDE4MTc2ODc2NzQ4MDcyMDM1NDgyODk4MzM0ODExMzY4NDA4NTI1NTk2MTk4MjUyNTE5MjY3MTA4MTMyNjA0MTk4MDA0NzkyODQ5MDc3ODY4OTUxOTA2MTcwODEyNTQwNzEzOTgyOTU0NjUzODEwNTM5OTQ5Mzg0NzEyNzczMzkwMjAwNzkxOTQ5NTY1OTAzNDM5NTIxNDI0OTA5NTc2ODMyNDE4ODkwODE5MjA0MzU0NzI5MjE3MjA3MzYwMTA1OTA2MDM5MDIzMjk5NTYxMzc0MDk4OTQyNzg5OTk2NzgwMTAyMDczMDcxNzYwODUyODQxMDY4OTg5ODYwNDAzNDMxNzM3NDgwMTgyNzI1ODUzODk5NzMzNzA2MDY5IiwiZSI6IjY1NTM3In0sInByaW5jaXBhbCI6eyJlbWFpbCI6InRlc3R1c2VyQHRlc3R1c2VyLmNvbSJ9fQ.aVIO470S_DkcaddQgFUXciGwq2F_MTdYOJtVnEYShni7I6mqBwK3fkdWShPEgLFWUSlVUtcy61FkDnq2G-6ikSx1fUZY7iBeSCOKYlh6Kj9v43JX-uhctRSB2pI17g09EUtvmb845EHUJuoowdBLmLa4DSTdZE-h4xUQ9MsY7Ik", failure, - jwk = require("./jwk"), + jwcrypto = require("./lib/jwcrypto"), status; function Provisioning(info, onsuccess, onfailure) { @@ -21,20 +21,25 @@ BrowserID.Mocks.Provisioning = (function() { else onfailure(failure); } - Provisioning.setStatus = function(newStatus) { + Provisioning.setStatus = function(newStatus, cb) { failure = null; + status = newStatus; + if(newStatus === Provisioning.NOT_AUTHENTICATED) { failure = { code: "primaryError", msg: "user is not authenticated as target user" }; + if (cb) cb(); } else if(newStatus === Provisioning.AUTHENTICATED) { - keypair = keypair || jwk.KeyPair.generate("DS", 256); + if (!keypair) + jwcrypto.generateKeypair({algorithm: "DS", keysize: 256}, function(err, kp) { + keypair = kp; + if (cb) cb(); + }); } - - status = newStatus; }; Provisioning.NOT_AUTHENTICATED = "not_authenticated"; diff --git a/resources/views/test.ejs b/resources/views/test.ejs index cde5603356951b9e18ed8894031b9178c5bbf88f..31a0236d8349d220175a77df873877c3e6b37734 100644 --- a/resources/views/test.ejs +++ b/resources/views/test.ejs @@ -69,7 +69,7 @@ <script src="/i18n/en_US/client.json"></script> <script src="/shared/javascript-extensions.js"></script> <script src="/shared/gettext.js"></script> - <script src="/lib/vepbundle.js"></script> + <script src="/lib/bidbundle-min.js"></script> <script src="http://testmob.org/include.js"></script> <script src="/shared/browserid.js"></script> <script src="/lib/dom-jquery.js"></script> diff --git a/scripts/serve_example_primary.js b/scripts/serve_example_primary.js index 0a5a4617bc34859dceeb2c753293ff9c47266a2a..e7198487ff2202c6158e6f43f0f92b6423239428 100755 --- a/scripts/serve_example_primary.js +++ b/scripts/serve_example_primary.js @@ -12,8 +12,10 @@ urlparse = require('urlparse'), postprocess = require('postprocess'), querystring = require('querystring'), sessions = require('connect-cookie-session'), -jwk = require('jwcrypto/jwk'), -jwcert = require('jwcrypto/jwcert'); +jwcrypto = require("jwcrypto"); + +// alg +require("jwcrypto/lib/algs/rs"); var exampleServer = express.createServer(); @@ -75,9 +77,9 @@ exampleServer.get("/api/logout", function (req, res) { return res.json(null); }); -var _privKey = jwk.SecretKey.fromSimpleObject( - JSON.parse(require('fs').readFileSync( - path.join(__dirname, '..', 'example', 'primary', 'sample.privatekey')))); +var _privKey = jwcrypto.loadSecretKey( + require('fs').readFileSync( + path.join(__dirname, '..', 'example', 'primary', 'sample.privatekey'))); exampleServer.post("/api/cert_key", function (req, res) { var user = req.session.user; @@ -85,10 +87,11 @@ exampleServer.post("/api/cert_key", function (req, res) { var domain = process.env['SHIMMED_DOMAIN']; var expiration = new Date(); - var pubkey = jwk.PublicKey.fromSimpleObject(req.body.pubkey); + var pubkey = jwcrypto.loadPublicKeyFromObject(req.body.pubkey); expiration.setTime(new Date().valueOf() + req.body.duration * 1000); - var cert = new jwcert.JWCert(domain, expiration, new Date(), - pubkey, {email: user + "@" + domain}).sign(_privKey); + jwcrypto.cert.sign(pubkey, {email: user + "@" + domain}, {issuer: domain, expiresAt: expiration, issuedAt: new Date()}, _privkey, function(err, cert) { + res.json({ cert: cert }); + }); res.json({ cert: cert }); }); diff --git a/tests/add-email-with-assertion-test.js b/tests/add-email-with-assertion-test.js index 19a58f0ae405b8c595101759dea3e47c14d5997e..e6e0688ea47dfd29fd26e39bc96bed566d3622fc 100755 --- a/tests/add-email-with-assertion-test.js +++ b/tests/add-email-with-assertion-test.js @@ -13,16 +13,17 @@ start_stop = require('./lib/start-stop.js'), wsapi = require('./lib/wsapi.js'), db = require('../lib/db.js'), config = require('../lib/configuration.js'), -jwk = require('jwcrypto/jwk.js'), -jwt = require('jwcrypto/jwt.js'), -vep = require('jwcrypto/vep.js'), -jwcert = require('jwcrypto/jwcert.js'), +jwcrypto = require('jwcrypto'), http = require('http'), querystring = require('querystring'), path = require("path"); var suite = vows.describe('auth-with-assertion'); +// algs +require("jwcrypto/lib/algs/ds"); +require("jwcrypto/lib/algs/rs"); + // disable vows (often flakey?) async error behavior suite.options.error = false; @@ -43,9 +44,9 @@ var g_keypair, g_cert; suite.addBatch({ "generating a keypair": { topic: function() { - return jwk.KeyPair.generate("DS", 256) + jwcrypto.generateKeypair({algorithm: "DS", keysize: 256}, this.callback); }, - "succeeds": function(r, err) { + "succeeds": function(err, r) { assert.isObject(r); assert.isObject(r.publicKey); assert.isObject(r.secretKey); @@ -56,9 +57,8 @@ suite.addBatch({ // for this trick we'll need the "secret" key of our built in // primary -var g_privKey = jwk.SecretKey.fromSimpleObject( - JSON.parse(require('fs').readFileSync( - path.join(__dirname, '..', 'example', 'primary', 'sample.privatekey')))); +var g_privKey = jwcrypto.loadSecretKey(require('fs').readFileSync( + path.join(__dirname, '..', 'example', 'primary', 'sample.privatekey'))); suite.addBatch({ @@ -68,13 +68,12 @@ suite.addBatch({ var expiration = new Date(); expiration.setTime(new Date().valueOf() + 60 * 60 * 1000); - g_cert = new jwcert.JWCert(TEST_DOMAIN, expiration, new Date(), - g_keypair.publicKey, {email: TEST_EMAIL}).sign(g_privKey); - return g_cert; + jwcrypto.cert.sign(g_keypair.publicKey, {email: TEST_EMAIL}, {issuer: TEST_DOMAIN, expiresAt: expiration, issuedAt: new Date()}, null, g_privKey, this.callback); }, - "works swimmingly": function(cert, err) { + "works swimmingly": function(err, cert) { assert.isString(cert); assert.lengthOf(cert.split('.'), 3); + g_cert = cert; } } }); @@ -83,11 +82,21 @@ suite.addBatch({ suite.addBatch({ "generating an assertion": { topic: function() { + var self = this; var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000)); - var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN); - return vep.bundleCertsAndAssertion([g_cert], tok.sign(g_keypair.secretKey)); + jwcrypto.assertion.sign( + {}, + {audience: TEST_ORIGIN, + issuer: TEST_DOMAIN, + expiresAt: expirationDate}, + g_keypair.secretKey, + function(err, signedAssertion) { + self.callback(err, jwcrypto.cert.bundle([g_cert], signedAssertion)); + }); + // var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN); + //return vep.bundleCertsAndAssertion([g_cert], tok.sign(g_keypair.secretKey)); }, - "succeeds": function(r, err) { + "succeeds": function(err, r) { assert.isString(r); g_assertion = r; } diff --git a/tests/auth-with-assertion-test.js b/tests/auth-with-assertion-test.js index eda9f1768df9931dbebd5c39817f1ef2fbd997ed..94a0e9ff1592254241627f123add37974b0077e0 100755 --- a/tests/auth-with-assertion-test.js +++ b/tests/auth-with-assertion-test.js @@ -36,17 +36,28 @@ var primaryUser = new primary({ domain: TEST_DOMAIN }); +suite.addBatch({ + "set things up": { + topic: function() { + primaryUser.setup(this.callback); + }, + "works": function() { + // nothing to do here + } + } +}); + // now let's generate an assertion using this user suite.addBatch({ "generating an assertion": { topic: function() { - return primaryUser.getAssertion(TEST_ORIGIN); + primaryUser.getAssertion(TEST_ORIGIN, this.callback); }, - "succeeds": function(r, err) { + "succeeds": function(err, r) { assert.isString(r); }, "and logging in with the assertion succeeds": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/wsapi/auth_with_assertion', { assertion: assertion, ephemeral: true diff --git a/tests/ca-test.js b/tests/ca-test.js index ecce1e0897dc7e4c7103bde217444a26d2a4ade7..43e1ccefc0aeac5ade5e2ae31a8424f8c6871e84 100755 --- a/tests/ca-test.js +++ b/tests/ca-test.js @@ -12,45 +12,62 @@ start_stop = require('./lib/start-stop.js'), wsapi = require('./lib/wsapi.js'), email = require('../lib/email.js'), ca = require('../lib/keysigner/ca.js'), -jwcert = require('jwcrypto/jwcert'), -jwk = require('jwcrypto/jwk'), -jws = require('jwcrypto/jws'); +jwcrypto = require('jwcrypto'), +cert = jwcrypto.cert, +assertion = jwcrypto.assertion; + +// algorithms +require("jwcrypto/lib/algs/rs"); var suite = vows.describe('ca'); // disable vows (often flakey?) async error behavior suite.options.error = false; -// generate a public key -var kp = jwk.KeyPair.generate("RS",64); - var email_addr = "foo@foo.com"; +var issuer = "127.0.0.1"; + +var kp = null; // certify a key suite.addBatch({ - "certify a public key": { + "generate a keypair": { topic: function() { - var expiration = new Date(); - expiration.setTime(new Date().valueOf() + 5000); - return ca.certify('127.0.0.1', email_addr, kp.publicKey, expiration); + // generate a public key + jwcrypto.generateKeypair({algorithm: "RS", keysize: 64}, this.callback); }, - "parses" : function(cert_raw, err) { - var cert = ca.parseCert(cert_raw); - assert.notEqual(cert, null); + "got a keypair": function(err, keypair) { + assert.isNull(err); + assert.isObject(keypair); + kp = keypair; }, - "verifies": function(cert_raw, err) { - // FIXME we might want to turn this into a true async test - // rather than one that is assumed to be synchronous although - // it has an async structure - ca.verifyChain('127.0.0.1', [cert_raw], function(pk) { - assert.isTrue(kp.publicKey.equals(pk)); - }); + "certify a public key": { + topic: function() { + var expiration = new Date(); + expiration.setTime(new Date().valueOf() + 5000); + ca.certify(issuer, email_addr, kp.publicKey, expiration, this.callback); + }, + "does not error out": function(err, cert_raw) { + assert.isNull(err); + assert.isNotNull(cert_raw); + }, + "looks ok" : function(err, cert_raw) { + assert.equal(cert_raw.split(".").length, 3); + }, + "upon verification": { + topic: function(err, cert_raw) { + ca.verifyChain(issuer, [cert_raw], this.callback); + }, + "verifies": function(err, pk, principal) { + assert.isNull(err); + assert.isTrue(kp.publicKey.equals(pk)); + assert.equal(principal.email, email_addr); + } + } } - }, - "certify a chain of keys": { } }); - + // run or export the suite. if (process.argv[1] === __filename) suite.run(); else suite.export(module); diff --git a/tests/cert-emails-test.js b/tests/cert-emails-test.js index a8f746f568d1fd78242098618fe9c393984a77e2..dab8fa36775d5adcbbc4708da14832e565def5f8 100755 --- a/tests/cert-emails-test.js +++ b/tests/cert-emails-test.js @@ -12,8 +12,7 @@ start_stop = require('./lib/start-stop.js'), wsapi = require('./lib/wsapi.js'), email = require('../lib/email.js'), ca = require('../lib/keysigner/ca.js'), -jwk = require('jwcrypto/jwk'), -jwt = require('jwcrypto/jwt'); +jwcrypto = require("jwcrypto"); var suite = vows.describe('cert-emails'); @@ -70,90 +69,118 @@ var cert_key_url = "/wsapi/cert_key"; // generate a keypair, we'll use this to sign assertions, as if // this keypair is stored in the browser localStorage -var kp = jwk.KeyPair.generate("RS",64); +var kp; suite.addBatch({ - "check the public key": { - topic: wsapi.get("/pk"), - "returns a 200": function(err, r) { - assert.strictEqual(r.code, 200); + "generate a keypair": { + topic: function() { + jwcrypto.generateKeypair({algorithm: "RS", keysize: 64}, this.callback); }, - "returns the right public key": function(err, r) { - var pk = jwk.PublicKey.deserialize(r.body); - assert.ok(pk); - } - }, - "cert key with no parameters": { - topic: wsapi.post(cert_key_url, {}), - "fails with HTTP 400" : function(err, r) { - assert.strictEqual(r.code, 400); - } - }, - "cert key invoked with just an email": { - topic: wsapi.post(cert_key_url, { email: 'syncer@somehost.com' }), - "returns a 400" : function(err, r) { - assert.strictEqual(r.code, 400); - } - }, - "cert key invoked with proper argument": { - topic: wsapi.post(cert_key_url, { - email: 'syncer@somehost.com', - pubkey: kp.publicKey.serialize(), - ephemeral: false - }), - "returns a response with a proper content-type" : function(err, r) { - assert.strictEqual(r.code, 200); + "works": function(err, keypair) { + assert.isNull(err); + assert.isObject(keypair); + kp = keypair; + }, + "check the public key": { + topic: function() { + wsapi.get("/pk").call(this); + }, + "returns a 200": function(err, r) { + assert.strictEqual(r.code, 200); + }, + "returns the right public key": function(err, r) { + var pk = jwcrypto.loadPublicKey(r.body); + assert.ok(pk); + } }, - "returns a proper cert": function(err, r) { - ca.verifyChain('127.0.0.1', [r.body], function(pk) { - assert.isTrue(kp.publicKey.equals(pk)); - }); + "cert key with no parameters": { + topic: function() { + wsapi.post(cert_key_url, {}).call(this); + }, + "fails with HTTP 400" : function(err, r) { + assert.strictEqual(r.code, 400); + } + }, + "cert key invoked with just an email": { + topic: function() { + wsapi.post(cert_key_url, { email: 'syncer@somehost.com' }).call(this); + }, + "returns a 400" : function(err, r) { + assert.strictEqual(r.code, 400); + } }, - "generate an assertion": { - topic: function(err, r) { - var serializedCert = r.body.toString(); - var expiration = new Date(new Date().getTime() + (2 * 60 * 1000)); - var assertion = new jwt.JWT(null, expiration, "rp.com"); - var full_assertion = { - certificates: [serializedCert], - assertion: assertion.sign(kp.secretKey) - }; - - return full_assertion; + "cert key invoked with proper argument": { + topic: function() { + wsapi.post(cert_key_url, { + email: 'syncer@somehost.com', + pubkey: kp.publicKey.serialize(), + ephemeral: false + }).call(this); + }, + "returns a response with a proper content-type" : function(err, r) { + assert.strictEqual(r.code, 200); }, - "full assertion looks good": function(full_assertion) { - assert.equal(full_assertion.certificates[0].split(".").length, 3); - assert.equal(full_assertion.assertion.split(".").length, 3); + "returns a proper cert": { + topic: function(err, r) { + ca.verifyChain('127.0.0.1', [r.body], this.callback); + }, + "that verifies": function(err, pk, principal) { + assert.isNull(err); + assert.equal(principal.email, 'syncer@somehost.com'); + assert.equal(kp.publicKey.serialize(), pk.serialize()); + } }, - "assertion verifies": { - topic: function(full_assertion) { - var cb = this.callback; - // extract public key at the tail of the chain - ca.verifyChain('127.0.0.1', full_assertion.certificates, function(pk) { - if (!pk) - cb(false); - - var assertion = new jwt.JWT(); - assertion.parse(full_assertion.assertion); - cb(assertion.verify(pk)); + "generate an assertion": { + topic: function(err, r) { + var serializedCert = r.body.toString(); + var expiration = new Date(new Date().getTime() + (2 * 60 * 1000)); + + var self = this; + jwcrypto.assertion.sign({}, {issuer: "127.0.0.1", expiresAt: expiration, issuedAt: new Date()}, kp.secretKey, function(err, signedObject) { + if (err) return self.callback(err); + + self.callback(null, { + certificates: [serializedCert], + assertion: signedObject + }); }); }, - "verifies": function(result, err) { - assert.isTrue(result); + "full bundle looks good": function(err, certs_and_assertion) { + assert.isNull(err); + assert.equal(certs_and_assertion.certificates[0].split(".").length, 3); + assert.equal(certs_and_assertion.assertion.split(".").length, 3); + }, + "assertion verifies": { + topic: function(err, certs_and_assertion) { + // bundle and verify + var bundle = jwcrypto.cert.bundle(certs_and_assertion.certificates, certs_and_assertion.assertion); + + var cb = this.callback; + // extract public key at the tail of the chain + ca.verifyBundle('127.0.0.1', bundle, this.callback); + }, + "verifies": function(err, certParamsArray, payload, assertionParams) { + assert.isNull(err); + assert.isArray(certParamsArray); + assert.isObject(payload); + assert.isObject(assertionParams); + } } } + }, + "cert key invoked proper arguments but incorrect email address": { + topic: function() { + wsapi.post(cert_key_url, { + email: 'syncer2@somehost.com', + pubkey: kp.publicKey.serialize(), + ephemeral: false + }).call(this); + }, + "returns a response with a proper error content-type" : function(err, r) { + assert.strictEqual(r.code, 400); + } } }, - "cert key invoked proper arguments but incorrect email address": { - topic: wsapi.post(cert_key_url, { - email: 'syncer2@somehost.com', - pubkey: kp.publicKey.serialize(), - ephemeral: false - }), - "returns a response with a proper error content-type" : function(err, r) { - assert.strictEqual(r.code, 400); - } - } }); start_stop.addShutdownBatches(suite); diff --git a/tests/conformance-test.js b/tests/conformance-test.js new file mode 100644 index 0000000000000000000000000000000000000000..205cb628990567b2092ca932885bc68dfb26fbf8 --- /dev/null +++ b/tests/conformance-test.js @@ -0,0 +1,299 @@ +#!/usr/bin/env node +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const +vows = require('vows'), +assert = require('assert'), +path = require('path'), +jwcrypto = require('jwcrypto'); + +require("jwcrypto/lib/algs/rs"); +require("jwcrypto/lib/algs/ds"); + +var suite = vows.describe('Conformance Tests'); + +var domainKeypair; +var userKeypair; + +suite.addBatch({ + "generate a keypair": { + topic: function() { + jwcrypto.generateKeypair({algorithm: "RS", keysize: 256}, this.callback); + }, + "works" : function(err, kp) { + assert.isNull(err); + domainKeypair = kp; + } + } +}); + +suite.addBatch({ + "generate a keypair": { + topic: function() { + jwcrypto.generateKeypair({algorithm: "DS", keysize: 128}, this.callback); + }, + "works" : function(err, kp) { + assert.isNull(err); + userKeypair = kp; + } + } +}); + +/* + * some functions to do b64url encoding/decoding + */ +function base64urlencode(arg) { + var s = new Buffer(arg).toString('base64'); // window.btoa(arg); + s = s.split('=')[0]; // Remove any trailing '='s + s = s.replace(/\+/g, '-'); // 62nd char of encoding + s = s.replace(/\//g, '_'); // 63rd char of encoding + // TODO optimize this; we can do much better + return s; +} + +function base64urldecode(arg) { + var s = arg; + s = s.replace(/-/g, '+'); // 62nd char of encoding + s = s.replace(/_/g, '/'); // 63rd char of encoding + switch (s.length % 4) // Pad with trailing '='s + { + case 0: break; // No pad chars in this case + case 2: s += "=="; break; // Two pad chars + case 3: s += "="; break; // One pad char + default: throw new InputException("Illegal base64url string!"); + } + return new Buffer(s,'base64').toString('ascii'); // window.atob(s); // Standard base64 decoder +} + +var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz"; +function int2char(n) { return BI_RM.charAt(n); } + +var b64urlmap="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; +function b64urltohex(s) { + var ret = ""; + var i; + var k = 0; // b64 state, 0-3 + var slop; + for(i = 0; i < s.length; ++i) { + var v = b64urlmap.indexOf(s.charAt(i)); + if(v < 0) continue; + if(k == 0) { + ret += int2char(v >> 2); + slop = v & 3; + k = 1; + } + else if(k == 1) { + ret += int2char((slop << 2) | (v >> 4)); + slop = v & 0xf; + k = 2; + } + else if(k == 2) { + ret += int2char(slop); + ret += int2char(v >> 2); + slop = v & 3; + k = 3; + } + else { + ret += int2char((slop << 2) | (v >> 4)); + ret += int2char(v & 0xf); + k = 0; + } + } + if(k == 1) + ret += int2char(slop << 2); + + // initial 0? only one for now + if (ret[0] == '0') + return ret.substring(1); + else + return ret; +} + +// this function (jwcrypto.extractComponents) is copied here so that +// a change in the library doesn't mess up these conformance tests. +function extractComponents(signedObject) { + if (typeof(signedObject) != 'string') + throw "malformed signature " + typeof(signedObject); + + var parts = signedObject.split("."); + if (parts.length != 3) { + throw "signed object must have three parts, this one has " + parts.length; + } + + var headerSegment = parts[0]; + var payloadSegment = parts[1]; + var cryptoSegment = parts[2]; + + // we verify based on the actual string + // FIXME: we should validate that the header contains only proper fields + var header = JSON.parse(base64urldecode(headerSegment)); + var payload = JSON.parse(base64urldecode(payloadSegment)); + var signature = b64urltohex(cryptoSegment); + + return {header: header, + payload: payload, + signature: signature, + headerSegment: headerSegment, + payloadSegment: payloadSegment, + cryptoSegment: cryptoSegment}; +}; + +const AUDIENCE = "http://foobar.com"; +const ISSUER = "issuer.com"; +const EMAIL = "john@example.com"; + +var now = new Date(); +var in_a_minute = new Date(new Date().valueOf() + 60000); + +suite.addBatch({ + "sign an assertion": { + topic: function() { + jwcrypto.assertion.sign({}, {audience: AUDIENCE, expiresAt: in_a_minute}, + userKeypair.secretKey, this.callback); + }, + "works" : function(err, signedObject) { + assert.isNull(err); + }, + "has three part": function(err, signedObject) { + assert.equal(signedObject.split(".").length, 3); + }, + "and then parsed": { + topic: function(signedObject) { + return extractComponents(signedObject); + }, + "has proper header": function(components) { + assert.isObject(components.header); + assert.equal(components.header.alg, 'DS128'); + assert.equal(Object.keys(components.header).length, 1); + }, + "has proper payload": function(components) { + assert.isObject(components.payload); + assert.equal(components.payload.exp, in_a_minute.valueOf()); + assert.equal(components.payload.aud, AUDIENCE); + + // nothing else + assert.equal(Object.keys(components.payload).length, 2); + }, + "has proper signature": function(components) { + assert.isString(components.signature); + + // 160 bits for r and s, 320 bits together, 80 hex chars + // but because of encoding, leading 0s may have gotten removed + // likelihood of X zeros, 1/(2^(4X)) + // let's allow for up to 5 zeros. + assert.ok(components.signature.length <= 80); + assert.ok(components.signature.length > 75); + } + } + } +}); + +suite.addBatch({ + "sign a cert": { + topic: function() { + jwcrypto.cert.sign(userKeypair.publicKey, {email: EMAIL}, + {issuedAt: now, issuer: ISSUER, expiresAt: in_a_minute}, + {}, + domainKeypair.secretKey, this.callback); + }, + "works" : function(err, signedObject) { + assert.isNull(err); + }, + "has three parts": function(err, signedObject) { + assert.equal(signedObject.split(".").length, 3); + }, + "and then parsed": { + topic: function(signedObject) { + return extractComponents(signedObject); + }, + "has proper header": function(components) { + assert.isObject(components.header); + assert.equal(components.header.alg, 'RS256'); + assert.equal(Object.keys(components.header).length, 1); + }, + "has proper payload": function(components) { + assert.isObject(components.payload); + assert.equal(components.payload.iss, ISSUER); + assert.equal(components.payload.exp, in_a_minute.valueOf()); + assert.equal(components.payload.iat, now.valueOf()); + + assert.isObject(components.payload.principal); + assert.equal(components.payload.principal.email, EMAIL); + assert.equal(Object.keys(components.payload.principal).length, 1); + + assert.equal(JSON.stringify(components.payload['public-key']), userKeypair.publicKey.serialize()); + + // nothing else + assert.equal(Object.keys(components.payload).length, 5); + }, + "has proper signature": function(components) { + assert.isString(components.signature); + + // 2048 bits = 512 hex chars, but could be less. Though very unlikely + // to be less than 32 bits less :) + assert.ok(480 < components.signature.length); + assert.ok(components.signature.length <= 512); + } + } + } +}); + +/* + * and the vectors + */ + +var VECTORS = [ + { + assertion: "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiIxMjcuMC4wLjEiLCJleHAiOjEzMzU1NjI2OTg3NjgsImlhdCI6MTMzNTU1OTA5ODc2OCwicHVibGljLWtleSI6eyJhbGdvcml0aG0iOiJEUyIsInkiOiIyN2Y2OTgzMWIzNzdlMmY1NzRiZGE5Njg1YWJmNTM5OTY1ZDAyNDI2Mjg0ZDZmYzViOWVkMjA0MzJmN2U5Yjg1YTFjMjJiMTQ2M2I0NmQwMzljMTIzOWJkZWI2NDc1ZDZjMDM0MWJlZmRiYzBjYjJmMjQ4MTUzYjRjMzFkZDMxNWFjZjFkZmY0ZWUwYmY2NGY4OTUyN2VlMTlmNTkxNTM3NWFjZTNkNTZjMWQ1NDUzY2FjNmRkMTE4NzU3NTI3MmRhYjBlZGQzMGYxYjRlOTI2Yzg3YTNlNGFjYWY2NmY5MmZlZDFhMDRhYjI3Y2NjNDkxM2FmZTI0ZGRjZjNmZTk4IiwicCI6ImZmNjAwNDgzZGI2YWJmYzViNDVlYWI3ODU5NGIzNTMzZDU1MGQ5ZjFiZjJhOTkyYTdhOGRhYTZkYzM0ZjgwNDVhZDRlNmUwYzQyOWQzMzRlZWVhYWVmZDdlMjNkNDgxMGJlMDBlNGNjMTQ5MmNiYTMyNWJhODFmZjJkNWE1YjMwNWE4ZDE3ZWIzYmY0YTA2YTM0OWQzOTJlMDBkMzI5NzQ0YTUxNzkzODAzNDRlODJhMThjNDc5MzM0MzhmODkxZTIyYWVlZjgxMmQ2OWM4Zjc1ZTMyNmNiNzBlYTAwMGMzZjc3NmRmZGJkNjA0NjM4YzJlZjcxN2ZjMjZkMDJlMTciLCJxIjoiZTIxZTA0ZjkxMWQxZWQ3OTkxMDA4ZWNhYWIzYmY3NzU5ODQzMDljMyIsImciOiJjNTJhNGEwZmYzYjdlNjFmZGYxODY3Y2U4NDEzODM2OWE2MTU0ZjRhZmE5Mjk2NmUzYzgyN2UyNWNmYTZjZjUwOGI5MGU1ZGU0MTllMTMzN2UwN2EyZTllMmEzY2Q1ZGVhNzA0ZDE3NWY4ZWJmNmFmMzk3ZDY5ZTExMGI5NmFmYjE3YzdhMDMyNTkzMjllNDgyOWIwZDAzYmJjNzg5NmIxNWI0YWRlNTNlMTMwODU4Y2MzNGQ5NjI2OWFhODkwNDFmNDA5MTM2YzcyNDJhMzg4OTVjOWQ1YmNjYWQ0ZjM4OWFmMWQ3YTRiZDEzOThiZDA3MmRmZmE4OTYyMzMzOTdhIn0sInByaW5jaXBhbCI6eyJlbWFpbCI6ImJlbkBhZGlkYS5uZXQifX0.MklRRWfQweUwYR2crhFU2EuLyUOZlpY4zJgg9LSWDF1MQIGJtNZAclB_tU4sNWfWyrHBa6ICXGfT9mMbkWwPIZC714clAkCMAQXiL2FhuzZSHlnYRO0_BFLO0LqtxIbwdGAQ0WvmaS5lPCgwHdoJbIHPVupebT1C-nUUu21pBoFI_8sPjzINwGBlE6K6WQQy0KbF2m0VDZY5EAYa4mh4o84xiABCoYZYSEeA9FIzmYRJEVrqYHjQeVucZdqkDDCTEK49nVIR4hi8Mm1EItYDn__HDydZORotzfOHuLmB9xyVgBX_tcKJ9mND7MQJVeOumhDAx9QyXtRUhPhKUTDNgA~eyJhbGciOiJEUzEyOCJ9.eyJleHAiOjEzMzU1NTk0MTU3MzMsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MTAwMDEifQ.BBoFaSGq0UAYDi9vdbsoBegeJ7FHVDxzODiV8MD8pF0emOPp1i_Uzg", + root: {"algorithm":"RS","n":"13717766671510433111303151806101127171813773557424962001210686599690717644398501153133960329815327700526221729490916021955004415636643109524427762578738613915853895591332921269523141755077814022043323454871557827878869765578483437974192481801184235473918125161566266295979176194039841474030846700306142580608077665527626562098429368267997746767380874004089196208403356658867000112308693077043530239627194850786092251128137244380236693014852428390414510793421293487373711079360003639159681004539188014924495483277607084448583613608953997565952445532663265804891482606228128383798830560843667395414521699843061983900619","e":"65537"} + } +]; + +var assertion = VECTORS[0].assertion; +var pk = jwcrypto.loadPublicKeyFromObject(VECTORS[0].root); +var now = new Date(); + +// times +var timeOfCert = 1335562698768; +var timeOfAssertion = 1335559415733; + +// a bit before both cert and assertion +var timeThatShouldWork = new Date(Math.min(timeOfCert, timeOfAssertion) - 1000); + +suite.addBatch( + { + "verifying a test-vector assertion that is expired" : { + topic: function() { + jwcrypto.cert.verifyBundle( + assertion, now, function(issuer, next) { + process.nextTick(function() {next(null, pk);}); + }, + this.callback); + }, + "fails appropriately": function(err, certParamsArray, payload, assertionParams) { + assert.equal(err, "assertion has expired"); + } + } +}); + +suite.addBatch( + { + "verifying a test-vector assertion with appropriate verif time" : { + topic: function() { + jwcrypto.cert.verifyBundle( + assertion, timeThatShouldWork, function(issuer, next) { + process.nextTick(function() {next(null, pk);}); + }, + this.callback); + }, + "succeed": function(err, certParamsArray, payload, assertionParams) { + assert.isNull(err); + } + } +}); + +// run or export the suite. +if (process.argv[1] === __filename) suite.run(); +else suite.export(module); diff --git a/tests/cookie-session-security-test.js b/tests/cookie-session-security-test.js index dd3466090546e782684210debf352d2e15e024e4..b45d4d6a6588636497f9a2da2603b5f0e0b4d0e8 100755 --- a/tests/cookie-session-security-test.js +++ b/tests/cookie-session-security-test.js @@ -10,10 +10,7 @@ const assert = require('assert'), vows = require('vows'), start_stop = require('./lib/start-stop.js'), wsapi = require('./lib/wsapi.js'), -email = require('../lib/email.js'), -jwcert = require('jwcrypto/jwcert'), -jwk = require('jwcrypto/jwk'), -jws = require('jwcrypto/jws'); +email = require('../lib/email.js'); var suite = vows.describe('cookie-session-security'); diff --git a/tests/lib/primary.js b/tests/lib/primary.js index e9825a0c90c71ffd577a774a29bdf9f614f8771e..1ff1b4d779c9d3cf8a3cc417aeca27f55c984b17 100644 --- a/tests/lib/primary.js +++ b/tests/lib/primary.js @@ -3,32 +3,53 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const -jwk = require('jwcrypto/jwk.js'), -jwt = require('jwcrypto/jwt.js'), -vep = require('jwcrypto/vep.js'), -jwcert = require('jwcrypto/jwcert.js'), +jwcrypto = require('jwcrypto'), path = require("path"); -// the private secret of our built-in primary -const g_privKey = jwk.SecretKey.fromSimpleObject( - JSON.parse(require('fs').readFileSync( - path.join(__dirname, '..', '..', 'example', 'primary', 'sample.privatekey')))); +require("jwcrypto/lib/algs/rs"); +require("jwcrypto/lib/algs/ds"); +// the private secret of our built-in primary +const g_privKey = jwcrypto.loadSecretKey( + require('fs').readFileSync( + path.join(__dirname, '..', '..', 'example', 'primary', 'sample.privatekey'))); function User(options) { + this.options = options; +} + +User.prototype.setup = function(cb) { + var self = this; + // upon allocation of a user, we'll gen a keypair and get a signed cert - this._keyPair = jwk.KeyPair.generate("DS", 256); - var expiration = new Date(); - expiration.setTime(new Date().valueOf() + 60 * 60 * 1000); - this._cert = new jwcert.JWCert( - options.domain, expiration, new Date(), - this._keyPair.publicKey, {email: options.email}).sign(g_privKey); + jwcrypto.generateKeypair({algorithm:"DS", keysize:256}, function(err, kp) { + if (err) return cb(err); + + self._keyPair = kp; + + var expiration = new Date(); + expiration.setTime(new Date().valueOf() + 60 * 60 * 1000); + + jwcrypto.cert.sign(self._keyPair.publicKey, {email: self.options.email}, + {expiresAt: expiration, issuer: self.options.domain, issuedAt: new Date()}, + {}, g_privKey, function(err, signedCert) { + if (err) return cb(err); + self._cert = signedCert; + + cb(null); + }); + }); } -User.prototype.getAssertion = function(origin) { +User.prototype.getAssertion = function(origin, cb) { + var self = this; var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000)); - var tok = new jwt.JWT(null, expirationDate, origin); - return vep.bundleCertsAndAssertion([this._cert], tok.sign(this._keyPair.secretKey)); -} + jwcrypto.assertion.sign({}, {audience: origin, issuer: "127.0.0.1", expiresAt: expirationDate}, + this._keyPair.secretKey, function(err, signedObject) { + if (err) return cb(err); + + cb(null, jwcrypto.cert.bundle([self._cert], signedObject)); + }); +}; module.exports = User; \ No newline at end of file diff --git a/tests/primary-then-secondary-test.js b/tests/primary-then-secondary-test.js index c09e023b04c68520d03c5229878611cbed93d32c..2112b3e2a674b6f5f16992d55996c2a30b7e36fa 100755 --- a/tests/primary-then-secondary-test.js +++ b/tests/primary-then-secondary-test.js @@ -38,17 +38,28 @@ var primaryUser = new primary({ domain: TEST_DOMAIN }); +suite.addBatch({ + "set things up": { + topic: function() { + primaryUser.setup(this.callback); + }, + "works": function() { + // nothing to do here + } + } +}); + // now let's generate an assertion using this user suite.addBatch({ "generating an assertion": { topic: function() { - return primaryUser.getAssertion(TEST_ORIGIN); + primaryUser.getAssertion(TEST_ORIGIN, this.callback); }, - "succeeds": function(r) { + "succeeds": function(err, r) { assert.isString(r); }, "and logging in with the assertion succeeds": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/wsapi/auth_with_assertion', { assertion: assertion, ephemeral: true diff --git a/tests/session-duration-test.js b/tests/session-duration-test.js index 232ad6320a6f7979a6f5f5d39f800eb377229a9a..ae63d111655951967ce5dbdcf9870095ca25a63d 100755 --- a/tests/session-duration-test.js +++ b/tests/session-duration-test.js @@ -4,6 +4,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + require('./lib/test_env.js'); const assert = @@ -16,9 +17,7 @@ config = require('../lib/configuration.js'), bcrypt = require('bcrypt'), primary = require('./lib/primary.js'), ca = require('../lib/keysigner/ca.js'), -jwk = require('jwcrypto/jwk'), -jwt = require('jwcrypto/jwt'), -jws = require('jwcrypto/jws'); +jwcrypto = require('jwcrypto'); var suite = vows.describe('session-context'); @@ -40,16 +39,29 @@ var primaryUser = new primary({ domain: PRIMARY_DOMAIN }); +suite.addBatch({ + "setup user": { + topic: function() { + primaryUser.setup(this.callback); + }, + "works": function(err) { + assert.isNull(err); + assert.isObject(primaryUser._keyPair); + assert.isString(primaryUser._cert); + } + } +}); + suite.addBatch({ "generating an assertion": { topic: function() { - return primaryUser.getAssertion(PRIMARY_ORIGIN); + primaryUser.getAssertion(PRIMARY_ORIGIN, this.callback); }, - "succeeds": function(r, err) { + "succeeds": function(err, r) { assert.isString(r); }, "and logging in with the assertion with ephemeral = true": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/wsapi/auth_with_assertion', { assertion: assertion, ephemeral: true @@ -70,13 +82,13 @@ suite.addBatch({ suite.addBatch({ "generating an assertion": { topic: function() { - return primaryUser.getAssertion(PRIMARY_ORIGIN); + primaryUser.getAssertion(PRIMARY_ORIGIN, this.callback); }, - "succeeds": function(r, err) { + "succeeds": function(err, r) { assert.isString(r); }, "and logging in with the assertion with ephemeral = false": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/wsapi/auth_with_assertion', { assertion: assertion, ephemeral: false @@ -176,61 +188,79 @@ suite.addBatch({ // finally, let's verify that ephemeral is properly handled when certifying keys for a user -var kp = jwk.KeyPair.generate("RS", 64); +var kp = null; assert.within = function(got, expected, margin) { assert.ok(got + margin > expected); assert.ok(got - margin < expected); } +suite.addBatch({ + "generate keypair": { + topic: function() { + jwcrypto.generateKeypair({algorithm:"RS", keysize: 64}, this.callback); + }, + "works": function(err, keypair) { + assert.isNull(err); + kp = keypair; + } + } +}); + suite.addBatch({ "cert_key invoked with ephemeral = false": { - topic: wsapi.post('/wsapi/cert_key', { - email: TEST_EMAIL, - pubkey: kp.publicKey.serialize(), - ephemeral: false - }), + topic: function() { + wsapi.post('/wsapi/cert_key', { + email: TEST_EMAIL, + pubkey: kp.publicKey.serialize(), + ephemeral: false + }).call(this); + }, "returns a response with a proper content-type" : function(err, r) { assert.strictEqual(r.code, 200); }, - "returns a valid cert": function(err, r) { - ca.verifyChain('127.0.0.1', [r.body], function(pk) { + "upon validation": { + topic: function(err, r) { + ca.verifyChain('127.0.0.1', [r.body], this.callback); + }, + "works": function(err, pk, principal, certParamsArray) { assert.isTrue(kp.publicKey.equals(pk)); - }); - }, - "has the correct expiration": function(err, r) { - var cert = new jws.JWS(); - cert.parse(r.body); - var pl = JSON.parse(cert.payload); - assert.within(pl.exp - pl.iat, - config.get('certificate_validity_ms'), - 200); + }, + "has the correct expiration": function(err, pk, principal, certParamsArray) { + var params = certParamsArray[certParamsArray.length - 1].assertionParams; + assert.within(params.expiresAt - params.issuedAt, + config.get('certificate_validity_ms'), + 200); + } } } }); suite.addBatch({ "cert_key invoked with ephemeral = true": { - topic: wsapi.post('/wsapi/cert_key', { - email: TEST_EMAIL, - pubkey: kp.publicKey.serialize(), - ephemeral: true - }), + topic: function() { + wsapi.post('/wsapi/cert_key', { + email: TEST_EMAIL, + pubkey: kp.publicKey.serialize(), + ephemeral: true + }).call(this); + }, "returns a response with a proper content-type" : function(err, r) { assert.strictEqual(r.code, 200); }, - "returns a valid cert": function(err, r) { - ca.verifyChain('127.0.0.1', [r.body], function(pk) { + "upon validation": { + topic: function(err, r) { + ca.verifyChain('127.0.0.1', [r.body], this.callback); + }, + "works": function(err, pk, principal, certParamsArray) { assert.isTrue(kp.publicKey.equals(pk)); - }); - }, - "has the correct expiration": function(err, r) { - var cert = new jws.JWS(); - cert.parse(r.body); - var pl = JSON.parse(cert.payload); - assert.within(pl.exp - pl.iat, - config.get('ephemeral_session_duration_ms'), - 200); + }, + "has the correct expiration": function(err, pk, principal, certParamsArray) { + var params = certParamsArray[certParamsArray.length - 1].assertionParams; + assert.within(params.expiresAt - params.issuedAt, + config.get('ephemeral_session_duration_ms'), + 200); + } } } }); diff --git a/tests/stalled-mysql-test.js b/tests/stalled-mysql-test.js index e75fbad678cf03961d3a2a32bafe626768e4b1df..ceeb498e95224f90c8e5d945e85967e1b46bdcdc 100755 --- a/tests/stalled-mysql-test.js +++ b/tests/stalled-mysql-test.js @@ -14,14 +14,14 @@ start_stop = require('./lib/start-stop.js'), wsapi = require('./lib/wsapi.js'), temp = require('temp'), fs = require('fs'), -jwk = require('jwcrypto/jwk.js'), -jwt = require('jwcrypto/jwt.js'), -vep = require('jwcrypto/vep.js'), -jwcert = require('jwcrypto/jwcert.js'), +jwcrypto = require('jwcrypto'), path = require('path'); var suite = vows.describe('forgotten-email'); +require("jwcrypto/lib/algs/ds"); +require("jwcrypto/lib/algs/rs"); + // disable vows (often flakey?) async error behavior suite.options.error = false; @@ -305,9 +305,9 @@ var g_keypair, g_cert, g_assertion; suite.addBatch({ "generating a keypair": { topic: function() { - return jwk.KeyPair.generate("DS", 256) + jwcrypto.generateKeypair({algorithm: "DS", keysize:256}, this.callback); }, - "succeeds": function(r, err) { + "succeeds": function(err, r) { assert.isObject(r); assert.isObject(r.publicKey); assert.isObject(r.secretKey); @@ -316,9 +316,9 @@ suite.addBatch({ } }); -var g_privKey = jwk.SecretKey.fromSimpleObject( - JSON.parse(require('fs').readFileSync( - path.join(__dirname, '..', 'example', 'primary', 'sample.privatekey')))); +var g_privKey = jwcrypto.loadSecretKey( + require('fs').readFileSync( + path.join(__dirname, '..', 'example', 'primary', 'sample.privatekey'))); suite.addBatch({ @@ -328,11 +328,12 @@ suite.addBatch({ var expiration = new Date(); expiration.setTime(new Date().valueOf() + 60 * 60 * 1000); - g_cert = new jwcert.JWCert(TEST_DOMAIN, expiration, new Date(), - g_keypair.publicKey, {email: TEST_EMAIL}).sign(g_privKey); - return g_cert; + jwcrypto.cert.sign(g_keypair.publicKey, {email: TEST_EMAIL}, + {expiresAt: expiration, issuedAt: new Date(), issuer: TEST_DOMAIN}, + g_privKey, this.callback); }, - "works swimmingly": function(cert, err) { + "works swimmingly": function(err, cert) { + g_cert = cert; assert.isString(cert); assert.lengthOf(cert.split('.'), 3); } @@ -342,11 +343,16 @@ suite.addBatch({ suite.addBatch({ "generating an assertion": { topic: function() { + var self = this; var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000)); - var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN); - return vep.bundleCertsAndAssertion([g_cert], tok.sign(g_keypair.secretKey)); + jwcrypto.assertion.sign({}, {audience: TEST_ORIGIN, expiresAt: expirationDate}, + g_keypair.secretKey, function(err, assertion) { + self.callback(err, + err ? undefined : jwcrypto.cert.bundle([g_cert], assertion)); + }); }, - "succeeds": function(r, err) { + "succeeds": function(err, r) { + assert.isNull(err); assert.isString(r); g_assertion = r; } diff --git a/tests/two-level-auth-test.js b/tests/two-level-auth-test.js index 0353d89acc9c5b91cfdbddcfabffeb55b161292a..dc635485964432534be00ffbcf6f4ffa284a6a81 100755 --- a/tests/two-level-auth-test.js +++ b/tests/two-level-auth-test.js @@ -29,17 +29,28 @@ var primaryUser = new primary({ domain: TEST_DOMAIN }); +suite.addBatch({ + "set things up": { + topic: function() { + primaryUser.setup(this.callback); + }, + "works": function() { + // nothing to do here + } + } +}); + // now let's generate an assertion using this user suite.addBatch({ "generating an assertion": { topic: function() { - return primaryUser.getAssertion(TEST_ORIGIN); + primaryUser.getAssertion(TEST_ORIGIN, this.callback); }, - "succeeds": function(r) { + "succeeds": function(err, r) { assert.isString(r); }, "and logging in with the assertion": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/wsapi/auth_with_assertion', { assertion: assertion, ephemeral: true diff --git a/tests/verifier-test.js b/tests/verifier-test.js index efa098a102eec4110daff9b8dc582218e1e2eb6d..c3f708acc268dd61e9c7b05de0d7f702a76fc6f2 100755 --- a/tests/verifier-test.js +++ b/tests/verifier-test.js @@ -13,16 +13,16 @@ start_stop = require('./lib/start-stop.js'), wsapi = require('./lib/wsapi.js'), db = require('../lib/db.js'), config = require('../lib/configuration.js'), -jwk = require('jwcrypto/jwk.js'), -jwt = require('jwcrypto/jwt.js'), -vep = require('jwcrypto/vep.js'), -jwcert = require('jwcrypto/jwcert.js'), +jwcrypto = require('jwcrypto'), http = require('http'), querystring = require('querystring'), path = require('path'); var suite = vows.describe('verifier'); +require("jwcrypto/lib/algs/rs"); +require("jwcrypto/lib/algs/ds"); + // disable vows (often flakey?) async error behavior suite.options.error = false; @@ -81,9 +81,10 @@ var g_keypair, g_cert; suite.addBatch({ "generating a keypair": { topic: function() { - return jwk.KeyPair.generate("DS", 256) + jwcrypto.generateKeypair({algorithm: "DS", keysize: 256}, this.callback); }, - "succeeds": function(r) { + "succeeds": function(err, r) { + assert.isNull(err); assert.isObject(r); assert.isObject(r.publicKey); assert.isObject(r.secretKey); @@ -116,16 +117,21 @@ function make_basic_tests(new_style) { var tests = { topic: function() { var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000)); - var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN); - return vep.bundleCertsAndAssertion([g_cert], - tok.sign(g_keypair.secretKey), - new_style); - }, - "succeeds": function(r) { + var self = this; + jwcrypto.assertion.sign({}, {audience: TEST_ORIGIN, expiresAt: expirationDate}, + g_keypair.secretKey, function(err, assertion) { + if (err) return self.callback(err); + var b = jwcrypto.cert.bundle([g_cert], + assertion, + new_style); // XXX IGNORED + self.callback(null, b); + }); + }, + "succeeds": function(err, r) { assert.isString(r); }, "and verifying that assertion by specifying domain as audience": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/verify', { audience: TEST_DOMAIN, assertion: assertion @@ -144,7 +150,7 @@ function make_basic_tests(new_style) { } }, "and verifying that assertion by specifying origin as audience": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/verify', { audience: TEST_ORIGIN, assertion: assertion @@ -163,7 +169,7 @@ function make_basic_tests(new_style) { } }, "but specifying the wrong audience": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/verify', { audience: "notfakesite.com", assertion: assertion @@ -176,7 +182,7 @@ function make_basic_tests(new_style) { } }, "but specifying the wrong port": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/verify', { audience: "http://fakesite.com:8888", assertion: assertion @@ -189,7 +195,7 @@ function make_basic_tests(new_style) { } }, "but specifying the wrong scheme": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/verify', { audience: "https://fakesite.com:8080", assertion: assertion @@ -202,7 +208,7 @@ function make_basic_tests(new_style) { } }, "and providing just a domain and port": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/verify', { audience: "fakesite.com:8080", assertion: assertion @@ -221,7 +227,7 @@ function make_basic_tests(new_style) { } }, "but providing just a domain and the wrong port": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/verify', { audience: "fakesite.com:8888", assertion: assertion @@ -234,7 +240,7 @@ function make_basic_tests(new_style) { } }, "leaving off the audience": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/verify', { assertion: assertion }).call(this); @@ -246,7 +252,7 @@ function make_basic_tests(new_style) { } }, "leaving off the assertion": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/verify', { audience: TEST_ORIGIN }).call(this); @@ -275,16 +281,21 @@ function make_post_format_tests(new_style) { var tests = { topic: function() { var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000)); - var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN); - return vep.bundleCertsAndAssertion([g_cert], - tok.sign(g_keypair.secretKey), - new_style); - }, - "succeeds": function(r) { + var self = this; + jwcrypto.assertion.sign({}, {audience: TEST_ORIGIN, expiresAt: expirationDate}, + g_keypair.secretKey, function(err, assertion) { + if (err) return self.callback(err); + var b = jwcrypto.cert.bundle([g_cert], + assertion, + new_style); // XXX IGNORED + self.callback(null, b); + }); + }, + "succeeds": function(err, r) { assert.isString(r); }, "posting assertion and audience as get parameters in a post request": { - topic: function(assertion) { + topic: function(err, assertion) { var cb = this.callback; var postArgs = { assertion: assertion, audience: TEST_ORIGIN }; http.request({ @@ -318,7 +329,7 @@ function make_post_format_tests(new_style) { } }, "posting assertion in body and audience as get parameter in a post request": { - topic: function(assertion) { + topic: function(err, assertion) { var cb = this.callback; var postArgs = querystring.stringify({ assertion: assertion }); var getArgs = querystring.stringify({ audience: TEST_ORIGIN }); @@ -355,7 +366,7 @@ function make_post_format_tests(new_style) { } }, "posting audience in body and asssertion as get parameter in a post request": { - topic: function(assertion) { + topic: function(err, assertion) { var cb = this.callback; var getArgs = querystring.stringify({ assertion: assertion }); var postArgs = querystring.stringify({ audience: TEST_ORIGIN }); @@ -406,17 +417,21 @@ function make_post_format_2_tests(new_style) { var tests = { topic: function() { var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000)); - var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN); - return vep.bundleCertsAndAssertion( - [g_cert], - tok.sign(g_keypair.secretKey), - new_style); - }, - "succeeds": function(r, err) { + var self = this; + jwcrypto.assertion.sign({}, {audience: TEST_ORIGIN, expiresAt: expirationDate}, + g_keypair.secretKey, function(err, assertion) { + if (err) return self.callback(err); + var b = jwcrypto.cert.bundle([g_cert], + assertion, + new_style); // XXX IGNORED + self.callback(null, b); + }); + }, + "succeeds": function(err, r) { assert.isString(r); }, "and submitting without proper Content-Type headers": { - topic: function(assertion) { + topic: function(err, assertion) { var cb = this.callback; var postArgs = querystring.stringify({ assertion: assertion, audience: TEST_ORIGIN }); var req = http.request({ @@ -443,7 +458,7 @@ function make_post_format_2_tests(new_style) { } }, "while submitting as application/json": { - topic: function(assertion) { + topic: function(err, assertion) { var cb = this.callback; var postArgs = JSON.stringify({ assertion: assertion, audience: TEST_ORIGIN }); var req = http.request({ @@ -487,30 +502,77 @@ function make_post_format_2_tests(new_style) { suite.addBatch(make_post_format_2_tests(false)); suite.addBatch(make_post_format_2_tests(true)); +var fakeDomainKeypair, newClientKeypair; + +// let's reuse the keys, cause we don't need new ones. +suite.addBatch({ + "set up fake domain key": { + topic: function() { + jwcrypto.generateKeypair({algorithm: "RS", keysize: 64}, this.callback); + }, + "works": function(err, kp) { + assert.isNull(err); + assert.isObject(kp); + fakeDomainKeypair = kp; + } + } +}); + +suite.addBatch({ + "set up user key": { + topic: function() { + jwcrypto.generateKeypair({algorithm: "DS", keysize: 256}, this.callback); + }, + "works": function(err, kp) { + assert.isNull(err); + assert.isObject(kp); + newClientKeypair = kp; + } + } +}); + +var fakeCert; +suite.addBatch({ + "certify the user key": { + topic: function() { + var expiration = new Date(new Date().getTime() + (1000 * 60 * 60 * 6)); + jwcrypto.cert.sign(newClientKeypair.publicKey, {email: TEST_EMAIL}, + {issuedAt: new Date(), issuer: "127.0.0.1", + expiresAt: expiration}, + {}, fakeDomainKeypair.secretKey, this.callback); + }, + "works": function(err, cert) { + assert.isNull(err); + assert.isString(cert); + fakeCert = cert; + } + } +}); + + + // now verify that a incorrectly signed assertion yields a good error message function make_incorrect_assertion_tests(new_style) { var title = "generating an assertion from a bogus cert with " + (new_style? "new style" : "old style"); var tests = { topic: function() { - var fakeDomainKeypair = jwk.KeyPair.generate("RS", 64); - var newClientKeypair = jwk.KeyPair.generate("DS", 256); - expiration = new Date(new Date().getTime() + (1000 * 60 * 60 * 6)); - var cert = new jwcert.JWCert("127.0.0.1", expiration, new Date(), newClientKeypair.publicKey, - {email: TEST_EMAIL}).sign(fakeDomainKeypair.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), - new_style); - }, - "yields a good looking assertion": function (r, err) { + var self = this; + jwcrypto.assertion.sign({}, {audience: TEST_ORIGIN, expiresAt: expirationDate}, + newClientKeypair.secretKey, function(err, assertion) { + if (err) return self.callback(err); + var b = jwcrypto.cert.bundle([fakeCert], + assertion, + new_style); // XXX IGNORED + self.callback(null, b); + }); + }, + "yields a good looking assertion": function (err, r) { assert.isString(r); assert.equal(r.length > 0, true); }, "will cause the verifier": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/verify', { audience: TEST_ORIGIN, assertion: assertion @@ -545,20 +607,23 @@ suite.addBatch({ "fails with a nice error": function(err, r) { var resp = JSON.parse(r.body); assert.strictEqual(resp.status, 'failure'); - assert.strictEqual(resp.reason, 'malformed assertion'); + // the error message here will be that there are no certs + assert.strictEqual(resp.reason, 'no certificates provided'); } }, "using an integer as an assertion (which is bogus)": { topic: function() { wsapi.post('/verify', { audience: TEST_ORIGIN, - assertion: 777 + assertion: 777 }).call(this); }, "fails with a nice error": function(err, r) { var resp = JSON.parse(r.body); assert.strictEqual(resp.status, 'failure'); - assert.strictEqual(resp.reason, 'malformed assertion'); + // this error message has to do with the full (backed) assertion not looking + // like a bundle of certs and assertion + assert.strictEqual(resp.reason, 'malformed backed assertion'); } } }); @@ -568,17 +633,22 @@ function make_crazy_assertion_tests(new_style) { var tests = { topic: function() { var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000)); - var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN); - return vep.bundleCertsAndAssertion( - [g_cert], - tok.sign(g_keypair.secretKey), - new_style); + var self = this; + jwcrypto.assertion.sign({}, {audience: TEST_ORIGIN, expiresAt: expirationDate}, + g_keypair.secretKey, function(err, assertion) { + if (err) return self.callback(err); + var b = jwcrypto.cert.bundle([g_cert], + assertion, + new_style); // XXX IGNORED + self.callback(null, b); + }); }, "and removing the last two chars from it": { - topic: function(assertion) { + topic: function(err, assertion) { // we used to chop off one char, but because of // robustness in base64-decoding, that still worked 25% // of the time. No need to build this knowledge in here. + // also, chopping off 2 characters gives varying error messages assertion = assertion.substr(0, assertion.length - 2); wsapi.post('/verify', { audience: TEST_ORIGIN, @@ -588,16 +658,14 @@ function make_crazy_assertion_tests(new_style) { "fails with a nice error": function(err, r) { var resp = JSON.parse(r.body); assert.strictEqual(resp.status, 'failure'); - // with new assertion format, the error is different - if (new_style) { - assert.strictEqual(resp.reason, 'verification failure'); - } else { - assert.strictEqual(resp.reason, 'malformed assertion'); - } + + // so depending on the assertion, this is going to be invalid or malformed + // so we're not testing the error message for now + // assert.strictEqual(resp.reason, 'invalid signature'); } }, "and removing the first char from it": { - topic: function(assertion) { + topic: function(err, assertion) { assertion = assertion.substr(1); wsapi.post('/verify', { audience: TEST_ORIGIN, @@ -606,18 +674,14 @@ function make_crazy_assertion_tests(new_style) { }, "fails with a nice error": function(err, r) { // XXX this test is failing because there's an exception thrown - // that's revealing too much info about the malformed assertion + // that's revealing too much info about the malformed signature var resp = JSON.parse(r.body); assert.strictEqual(resp.status, 'failure'); - if (new_style) { - assert.strictEqual(resp.reason, 'SyntaxError: Unexpected token Ș'); - } else { - assert.strictEqual(resp.reason, 'malformed assertion'); - } + assert.strictEqual(resp.reason, 'malformed signature'); } }, "and appending gunk to it": { - topic: function(assertion) { + topic: function(err, assertion) { assertion += "gunk"; wsapi.post('/verify', { audience: TEST_ORIGIN, @@ -627,11 +691,7 @@ function make_crazy_assertion_tests(new_style) { "fails with a nice error": function(err, r) { var resp = JSON.parse(r.body); assert.strictEqual(resp.status, 'failure'); - if (new_style) { - assert.strictEqual(resp.reason, 'verification failure'); - } else { - assert.strictEqual(resp.reason, 'malformed assertion'); - } + assert.strictEqual(resp.reason, 'malformed signature'); } } }; @@ -651,92 +711,120 @@ suite.addBatch({ "An assertion that expired a millisecond ago": { topic: function() { var expirationDate = new Date(new Date().getTime() - 10); - var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN); - var assertion = vep.bundleCertsAndAssertion( - [g_cert], - tok.sign(g_keypair.secretKey), - true); - wsapi.post('/verify', { - audience: TEST_ORIGIN, - assertion: assertion - }).call(this); + var self = this; + jwcrypto.assertion.sign({}, {audience: TEST_ORIGIN, expiresAt: expirationDate}, + g_keypair.secretKey, function(err, assertion) { + if (err) return self.callback(err); + var b = jwcrypto.cert.bundle([g_cert], + assertion, + true); // XXX IGNORED + + wsapi.post('/verify', { + audience: TEST_ORIGIN, + assertion: b + }).call(self); + }); }, "fails with a nice error": function(err, r) { var resp = JSON.parse(r.body); assert.strictEqual(resp.status, 'failure'); - // XXX: the verifier should return a clearer error message - assert.strictEqual(resp.reason, 'verification failure'); + assert.strictEqual(resp.reason, 'assertion has expired'); } }, "An assertion with a bundled bogus certificate": { topic: function() { var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000)); - var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN); - var assertion = vep.bundleCertsAndAssertion( - [g_cert, "bogus cert"], - tok.sign(g_keypair.secretKey), - true); - wsapi.post('/verify', { - audience: TEST_ORIGIN, - assertion: assertion - }).call(this); + var self = this; + jwcrypto.assertion.sign({}, {audience: TEST_ORIGIN, expiresAt: expirationDate}, + g_keypair.secretKey, function(err, assertion) { + if (err) return self.callback(err); + var b = jwcrypto.cert.bundle([g_cert, "bogus cert"], + assertion, + true); // XXX IGNORED + + wsapi.post('/verify', { + audience: TEST_ORIGIN, + assertion: b + }).call(self); + }); }, "fails with a nice error": function(err, r) { var resp = JSON.parse(r.body); assert.strictEqual(resp.status, 'failure'); - // XXX: this error should be clearer - assert.strictEqual(resp.reason, 'Malformed JSON web signature: Must have three parts'); + assert.strictEqual(resp.reason, 'malformed signature'); } }, "An assertion with a no certificate": { topic: function() { var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000)); - var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN); - - // XXX this call throws an exception if it's new style - // how to test this? - var assertion = vep.bundleCertsAndAssertion( - [], - tok.sign(g_keypair.secretKey), - false); - wsapi.post('/verify', { - audience: TEST_ORIGIN, - assertion: assertion - }).call(this); + var self = this; + jwcrypto.assertion.sign({}, {audience: TEST_ORIGIN, expiresAt: expirationDate}, + g_keypair.secretKey, function(err, assertion) { + if (err) return self.callback(err); + + // a bundle with no certs is no longer possible, + // so we submit just the assertion + + wsapi.post('/verify', { + audience: TEST_ORIGIN, + assertion: assertion + }).call(self); + }); }, "fails with a nice error": function(err, r) { var resp = JSON.parse(r.body); assert.strictEqual(resp.status, 'failure'); - // XXX: this error should be clearer - assert.strictEqual(resp.reason, "TypeError: Cannot read property 'issuer' of undefined"); + // an error that indicates no certs + assert.strictEqual(resp.reason, "no certificates provided"); } } }); +var otherIssuerCert; + +suite.addBatch({ + "certify the user key for other issuer": { + topic: function() { + var expiration = new Date(new Date().getTime() + (1000 * 60 * 60 * 6)); + jwcrypto.cert.sign(newClientKeypair.publicKey, {email: TEST_EMAIL}, + {issuedAt: new Date(), issuer: "no.such.domain", + expiresAt: expiration}, + {}, fakeDomainKeypair.secretKey, this.callback); + }, + "works": function(err, cert) { + assert.isNull(err); + assert.isString(cert); + otherIssuerCert = cert; + } + } +}); + + // now verify that assertions from a primary who does not have browserid support // will fail to verify function make_other_issuer_tests(new_style) { var title = "generating an assertion from a cert signed by some other domain with " + (new_style ? "new style" : "old style"); var tests = { topic: function() { - var fakeDomainKeypair = jwk.KeyPair.generate("RS", 64); - var newClientKeypair = jwk.KeyPair.generate("DS", 256); - expiration = new Date(new Date().getTime() + (1000 * 60 * 60 * 6)); - var cert = new jwcert.JWCert("no.such.domain", expiration, new Date(), newClientKeypair.publicKey, - {email: TEST_EMAIL}).sign(fakeDomainKeypair.secretKey); - + // keys are already generated + // otherIssuerCert is already generated 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), - new_style); - }, - "yields a good looking assertion": function (r) { + var self = this; + jwcrypto.assertion.sign({}, {audience: TEST_ORIGIN, expiresAt: expirationDate}, + newClientKeypair.secretKey, function(err, assertion) { + if (err) return self.callback(err); + var b = jwcrypto.cert.bundle([otherIssuerCert], + assertion, + new_style); // XXX IGNORED + self.callback(null, b); + }); + }, + "yields a good looking assertion": function (err, r) { assert.isString(r); assert.equal(r.length > 0, true); }, "will cause the verifier": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/verify', { audience: TEST_ORIGIN, assertion: assertion @@ -758,30 +846,52 @@ function make_other_issuer_tests(new_style) { suite.addBatch(make_other_issuer_tests(false)); suite.addBatch(make_other_issuer_tests(true)); +// prepare a cert with example.domain primary +var primaryCert; +suite.addBatch({ + "certify the user key by example.domain for wrong email address": { + topic: function() { + var secretKey = jwcrypto.loadSecretKey( + require('fs').readFileSync( + path.join(__dirname, '..', 'example', 'primary', 'sample.privatekey'))); + + var expiration = new Date(new Date().getTime() + (1000 * 60 * 60 * 6)); + jwcrypto.cert.sign(newClientKeypair.publicKey, {email: TEST_EMAIL}, + {issuedAt: new Date(), issuer: "example.domain", + expiresAt: expiration}, + {}, secretKey, this.callback); + }, + "works": function(err, cert) { + assert.isNull(err); + assert.isString(cert); + primaryCert = cert; + } + } +}); + // 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); - + // newClientKeypair already generated, reusing + // primaryCert already generated 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) { + var self = this; + jwcrypto.assertion.sign({}, {audience: TEST_ORIGIN, expiresAt: expirationDate}, + newClientKeypair.secretKey, function(err, assertion) { + if (err) return self.callback(err); + var b = jwcrypto.cert.bundle([primaryCert], + assertion); + self.callback(null, b); + }); + }, + "yields a good looking assertion": function (err, r) { assert.isString(r); assert.equal(r.length > 0, true); }, "will cause the verifier": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/verify', { audience: TEST_ORIGIN, assertion: assertion @@ -790,42 +900,62 @@ suite.addBatch({ "to return a clear error message": function (err, r) { 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'"); + assert.strictEqual(resp.reason, "issuer 'example.domain' may not speak for emails from 'somedomain.com'"); } } } }); +suite.addBatch({ + "certify the user key by example.domain for right email address": { + topic: function() { + var secretKey = jwcrypto.loadSecretKey( + require('fs').readFileSync( + path.join(__dirname, '..', 'example', 'primary', 'sample.privatekey'))); + + var expiration = new Date(new Date().getTime() + (1000 * 60 * 60 * 6)); + jwcrypto.cert.sign(newClientKeypair.publicKey, {email: "foo@example.domain"}, + {issuedAt: new Date(), issuer: "example.domain", + expiresAt: expiration}, + {}, secretKey, this.callback); + }, + "works": function(err, cert) { + assert.isNull(err); + assert.isString(cert); + primaryCert = cert; + } + } +}); + // 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); - + // primaryCert generated + // newClientKeypair generated 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) { + var self = this; + jwcrypto.assertion.sign({}, {audience: TEST_ORIGIN, expiresAt: expirationDate}, + newClientKeypair.secretKey, function(err, assertion) { + if (err) return self.callback(err); + var b = jwcrypto.cert.bundle([primaryCert], + assertion); + self.callback(null, b); + }); + }, + "yields a good looking assertion": function (err, r) { assert.isString(r); assert.equal(r.length > 0, true); }, "will cause the verifier": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/verify', { audience: TEST_ORIGIN, assertion: assertion }).call(this); }, - "to return a clear error message": function (err, r) { + "to succeed": function (err, r) { var resp = JSON.parse(r.body); assert.strictEqual(resp.status, 'okay'); assert.strictEqual(resp.issuer, "example.domain"); diff --git a/tests/verify-in-different-browser-test.js b/tests/verify-in-different-browser-test.js index 6ebd2fe15cbf02556009865a404aa998d831ea7b..b5a5a0fbb5f5cf6463088a36bba301a3b29e67f1 100755 --- a/tests/verify-in-different-browser-test.js +++ b/tests/verify-in-different-browser-test.js @@ -39,18 +39,29 @@ var primaryUser = new primary({ domain: TEST_DOMAIN }); +suite.addBatch({ + "set things up": { + topic: function() { + primaryUser.setup(this.callback); + }, + "works": function() { + // nothing to do here + } + } +}); + // first we'll create an account without a password by using // a primary address. suite.addBatch({ "generating an assertion": { topic: function() { - return primaryUser.getAssertion(TEST_ORIGIN); + return primaryUser.getAssertion(TEST_ORIGIN, this.callback); }, - "succeeds": function(r) { + "succeeds": function(err, r) { assert.isString(r); }, "and logging in with the assertion": { - topic: function(assertion) { + topic: function(err, assertion) { wsapi.post('/wsapi/auth_with_assertion', { assertion: assertion, ephemeral: true