From ac121c8881108f3f9be7be530d23e173d3e6b268 Mon Sep 17 00:00:00 2001 From: Ben Adida <ben@adida.net> Date: Thu, 3 May 2012 21:12:42 -0700 Subject: [PATCH] upgraded to new jwcrypto API, including backend, frontend, and tests. added conformance tests. tests should now pass updated to jwcrypto that has proper callback delay guarantees and removed unnecessary setTimeouts Updated all calls to jwcrypto to use the more intuitive API. Fixed a front-end test that was failing due to true asynchronicity of jwcrypto. --- bin/verifier | 2 +- lib/keysigner/ca.js | 62 ++- lib/keysigner/keysigner-compute.js | 8 +- lib/primary.js | 73 ++-- lib/secrets.js | 12 +- lib/static_resources.js | 4 +- lib/verifier/certassertion.js | 44 +- package.json | 2 +- resources/static/lib/bidbundle-min.js | 1 + resources/static/lib/vepbundle.js | 1 - resources/static/shared/browserid.js | 6 +- resources/static/shared/network.js | 3 +- resources/static/shared/provisioning.js | 8 +- resources/static/shared/storage.js | 13 +- resources/static/shared/user.js | 49 +-- resources/static/test/cases/shared/user.js | 43 +- resources/static/test/mocks/provisioning.js | 15 +- resources/views/test.ejs | 2 +- scripts/serve_example_primary.js | 19 +- tests/add-email-with-assertion-test.js | 41 +- tests/auth-with-assertion-test.js | 17 +- tests/ca-test.js | 63 ++- tests/cert-emails-test.js | 173 ++++---- tests/conformance-test.js | 299 +++++++++++++ tests/cookie-session-security-test.js | 5 +- tests/lib/primary.js | 57 ++- tests/primary-then-secondary-test.js | 17 +- tests/session-duration-test.js | 114 +++-- tests/stalled-mysql-test.js | 38 +- tests/two-level-auth-test.js | 17 +- tests/verifier-test.js | 450 +++++++++++++------- tests/verify-in-different-browser-test.js | 17 +- 32 files changed, 1135 insertions(+), 540 deletions(-) create mode 120000 resources/static/lib/bidbundle-min.js delete mode 120000 resources/static/lib/vepbundle.js create mode 100644 tests/conformance-test.js diff --git a/bin/verifier b/bin/verifier index 1d9f48e60..449925693 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 bc7be67f6..5e95fef85 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 5a64adb0c..39e761756 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 55861ece6..060d199cc 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 4b854c343..eb674481e 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 1bd4a35f5..456f5ce69 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 b437fe4f9..42816d698 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 e75ed621d..4da780272 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 000000000..1e002cab8 --- /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 99cb32376..000000000 --- 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 34481fb5c..5f6623876 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 45f422fbe..b56ef5684 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 4eba6b156..c0fc73410 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 2233ea73c..0447082ac 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 fb95b2556..5bf059997 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 455736004..c44fe0cd7 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 1b3dc6b48..cdee24b13 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 cde560335..31a0236d8 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 0a5a4617b..e7198487f 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 19a58f0ae..e6e0688ea 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 eda9f1768..94a0e9ff1 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 ecce1e089..43e1ccefc 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 a8f746f56..dab8fa367 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 000000000..205cb6289 --- /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 dd3466090..b45d4d6a6 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 e9825a0c9..1ff1b4d77 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 c09e023b0..2112b3e2a 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 232ad6320..ae63d1116 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 e75fbad67..ceeb498e9 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 0353d89ac..dc6354859 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 efa098a10..c3f708acc 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 6ebd2fe15..b5a5a0fbb 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 -- GitLab