diff --git a/.gitmodules b/.gitmodules
index 04bc465bac6f9b7ed1ca3eb345907e61e61783dd..65d99631ec153269e6407459382fcb3548d94bf1 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
 [submodule "lib/doctest"]
 	path = lib/doctest
 	url = git@github.com:ianb/doctestjs
+[submodule "lib/jwcrypto"]
+	path = lib/jwcrypto
+	url = https://github.com/mozilla/jwcrypto.git
diff --git a/ChangeLog b/ChangeLog
index 75af1b3664ec5bdb23e0efd74b1905161221f0f0..b2fa00024eb913dc4cf6251fc8db3d6354fe4a23 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,9 @@
+train-2011.09.01:
+  * /ws_api/set_key always returns returns value instead of HTTP 204 response: #219
+  * update javascript mvc to 3.1.0.
+  * major interframe/window communication change using a hidden relay iframe to facilitate IE: #97(still open)
+  * link colors on browserid.org are consistent: #227
+
 train-2011.08.25:
   * created command line load generation tool and performance analysis work: #125
   * beginning unit/functional tests for front end: #183
@@ -14,7 +20,8 @@ train-2011.08.25:
   * minify include.js by default: #206
   * more than one email address can be added per dialog lifespan: #215
   * verifyier no longer verifies assertions issued by another server.
-
+  * (2011.08.31) no error message displayed if you try to authenticate with an invalid u/p: #222
+	
 train-2011.08.18:
   * upon clickthrough of the email link, don't have the browser window close itself: #162
   * passwords must be between 8 and 80 chars: #155
diff --git a/browserid/app.js b/browserid/app.js
index 3c1890fb25f3f3642860f6790425c70f91f3956c..b87a79dd00c5a38b627279876295a2a8f6f33254 100644
--- a/browserid/app.js
+++ b/browserid/app.js
@@ -38,6 +38,7 @@ fs = require('fs'),
 path = require('path'),
 url = require('url'),
 wsapi = require('./lib/wsapi.js'),
+ca = require('./lib/ca.js'),
 httputils = require('./lib/httputils.js'),
 webfinger = require('./lib/webfinger.js'),
 sessions = require('connect-cookie-session'),
@@ -57,8 +58,11 @@ db.open(configuration.get('database'));
 const COOKIE_SECRET = secrets.hydrateSecret('browserid_cookie', configuration.get('var_path'));
 const COOKIE_KEY = 'browserid_state';
 
-function internal_redirector(new_url) {
+function internal_redirector(new_url, suppress_noframes) {
   return function(req, resp, next) {
+    if (suppress_noframes)
+      resp.removeHeader('x-frame-options');
+    
     req.url = new_url;
     return next();
   };
@@ -83,10 +87,10 @@ function router(app) {
   });
 
   // simple redirects (internal for now)
-  app.get('/register_iframe', internal_redirector('/dialog/register_iframe.html'));
+  app.get('/register_iframe', internal_redirector('/dialog/register_iframe.html',true));
 
   // Used for a relay page for communication.
-  app.get('/relay', function(req, res, next ) {
+  app.get("/relay", function(req,res, next) {
     // Allow the relay to be run within a frame
     res.removeHeader('x-frame-options');
     res.render('relay.ejs', {
@@ -127,6 +131,27 @@ function router(app) {
   // register all the WSAPI handlers
   wsapi.setup(app);
 
+  // the public key
+  app.get("/pk", function(req, res) {
+    res.json(ca.PUBLIC_KEY.toSimpleObject());
+  });
+
+  // vep bundle of JavaScript
+  app.get("/vepbundle", function(req, res) {
+    fs.readFile(__dirname + "/../lib/jwcrypto/vepbundle.js", function(error, content) {
+      if (error) {
+        res.writeHead(500);
+        res.end("oops");
+        console.log(error);
+      } else {
+        res.writeHead(200, {'Content-Type': 'text/javascript'});
+        res.write(content);
+        res.end();
+      }
+    });
+  });
+
+  // FIXME: remove this call
   app.get('/users/:identity.xml', function(req, resp, next) {
     webfinger.renderUserPage(req.params.identity, function (resultDocument) {
       if (resultDocument === undefined) {
diff --git a/browserid/compress.sh b/browserid/compress.sh
index c23c6bb7fc1a3c151f66dda3c4c67deea65e6a5e..01bf4b36d9422d8f4658bef299dcf52012c1cb05 100755
--- a/browserid/compress.sh
+++ b/browserid/compress.sh
@@ -45,7 +45,7 @@ echo ''
 
 cd ../js
 # re-minimize everything together
-cat jquery-1.6.2.min.js ../dialog/resources/underscore-min.js ../dialog/resources/browserid-network.js ../dialog/resources/browserid-identities.js ../dialog/resources/storage.js browserid.js > lib.js
+cat jquery-1.6.2.min.js json2.js ../dialog/resources/underscore-min.js ../dialog/resources/browserid-network.js ../dialog/resources/browserid-identities.js ../dialog/resources/storage.js browserid.js > lib.js
 $UGLIFY < lib.js > lib.min.js
 
 cd ../css
diff --git a/browserid/lib/ca.js b/browserid/lib/ca.js
new file mode 100644
index 0000000000000000000000000000000000000000..d6da4f979306a43d54ef6f428c72a9854b88c300
--- /dev/null
+++ b/browserid/lib/ca.js
@@ -0,0 +1,79 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *     Ben Adida <benadida@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// certificate authority
+
+var jwcert = require('../../lib/jwcrypto/jwcert'),
+    jwk = require('../../lib/jwcrypto/jwk'),
+    jws = require('../../lib/jwcrypto/jws'),
+    configuration = require('../../libs/configuration'),
+    secrets = require('../../libs/secrets'),
+    path = require("path"),
+    fs = require("fs");
+
+var HOSTNAME = configuration.get('hostname');
+
+function parsePublicKey(serializedPK) {
+  return jwk.PublicKey.deserialize(serializedPK);
+}
+
+function parseCert(serializedCert) {
+  var cert = new jwcert.JWCert();
+  cert.parse(serializedCert);
+  return cert;
+}
+
+function certify(email, publicKey, expiration) {
+  return new jwcert.JWCert(HOSTNAME, new Date(), publicKey, {email: email}).sign(secrets.SECRET_KEY);
+}
+
+function verifyChain(certChain, cb) {
+  // raw certs
+  return jwcert.JWCert.verifyChain(certChain, function(issuer, next) {
+    // for now we only do browserid.org issued keys
+    if (issuer != HOSTNAME)
+      return next(null);
+
+    next(secrets.PUBLIC_KEY);
+  }, cb);
+}
+
+// exports, not the key stuff
+exports.certify = certify;
+exports.verifyChain = verifyChain;
+exports.parsePublicKey = parsePublicKey;
+exports.parseCert = parseCert;
+exports.PUBLIC_KEY = secrets.PUBLIC_KEY;
\ No newline at end of file
diff --git a/browserid/lib/db.js b/browserid/lib/db.js
index ef91aa612fc5aa91a021daa67b3a1a469df469ea..058c26f71be5d063302cb84df0d98697967637bf 100644
--- a/browserid/lib/db.js
+++ b/browserid/lib/db.js
@@ -101,6 +101,7 @@ exports.onReady = function(f) {
   'stageEmail',
   'gotVerificationSecret',
   'checkAuth',
+  'listEmails',
   'getSyncResponse',
   'pubkeysForEmail',
   'removeEmail',
diff --git a/browserid/lib/db_json.js b/browserid/lib/db_json.js
index b4518ac95714f0bf27c2239a39594bd599359fe5..9a48d498b239a8907e04142a0ae1a460fffd383d 100644
--- a/browserid/lib/db_json.js
+++ b/browserid/lib/db_json.js
@@ -210,7 +210,6 @@ exports.stageUser = function(obj, cb) {
   staged[secret] = {
     type: "add_account",
     email: obj.email,
-    pubkey: obj.pubkey,
     pass: obj.hash
   };
 
@@ -218,14 +217,13 @@ exports.stageUser = function(obj, cb) {
   setTimeout(function() { cb(secret); }, 0);
 };
 
-exports.stageEmail = function(existing_email, new_email, pubkey, cb) {
+exports.stageEmail = function(existing_email, new_email, cb) {
   var secret = secrets.generate(48);
   // overwrite previously staged users
   staged[secret] = {
     type: "add_email",
     existing_email: existing_email,
-    email: new_email,
-    pubkey: pubkey
+    email: new_email
   };
   stagedEmails[new_email] = secret;
   setTimeout(function() { cb(secret); }, 0);
@@ -313,6 +311,22 @@ function emailToUserID(email, cb) {
   setTimeout(function() { cb(id); }, 0);
 }
 
+exports.listEmails = function(email, cb) {
+  // get the user id associated with this account
+  emailToUserID(email, function(userID) {
+    if (userID === undefined) {
+      cb("no such email: " + email);
+      return;
+    }
+    var email_list = jsel.match(".address", db[userID]);
+    var emails = {};
+    for (var i=0; i < email_list.length; i++)
+      emails[email_list[i]] = {};
+
+    cb(null, emails);
+  });
+};
+
 exports.getSyncResponse = function(email, identities, cb) {
   var respBody = {
     unknown_emails: [ ],
diff --git a/browserid/lib/db_mysql.js b/browserid/lib/db_mysql.js
index 61bfc2cac4063adb1fe5483e7eb117174cd63072..8392d359737c0746f84d3b5db322ed40c83f35ed 100644
--- a/browserid/lib/db_mysql.js
+++ b/browserid/lib/db_mysql.js
@@ -318,11 +318,11 @@ exports.addKeyToEmail = function(existing_email, email, pubkey, cb) {
     });
 }
 
-exports.stageEmail = function(existing_email, new_email, pubkey, cb) {
+exports.stageEmail = function(existing_email, new_email, cb) {
   var secret = secrets.generate(48);
   // overwrite previously staged users
-  client.query('INSERT INTO staged (secret, new_acct, existing, email, pubkey) VALUES(?,FALSE,?,?,?) ' +
-               'ON DUPLICATE KEY UPDATE secret=?, existing=?, new_acct=FALSE, pubkey=?, passwd=""',
+  client.query('INSERT INTO staged (secret, new_acct, existing, email) VALUES(?,FALSE,?,?) ' +
+               'ON DUPLICATE KEY UPDATE secret=?, existing=?, new_acct=FALSE, passwd=""',
                [ secret, existing_email, new_email, pubkey, secret, existing_email, pubkey],
                function(err) {
                  if (err) {
@@ -353,6 +353,30 @@ function emailHasPubkey(email, pubkey, cb) {
     });
 }
 
+/*
+ * a simpler action than syncResponse, just list the user's emails.
+ * this is more appropriate for the certs approach.
+ *
+ * returns an object keyed by email address with properties for each email
+ */
+exports.listEmails = function(email, cb) {
+  client.query(
+    'SELECT address FROM email WHERE user = ( SELECT user FROM email WHERE address = ? ) ',
+      [ email ],
+      function (err, rows) {
+        if (err) cb(err);
+        else {
+          var emails = {};
+
+          // eventually we'll have fields in here
+          for (var i = 0; i < rows.length; i++)
+            emails[rows[i].address] = {};
+
+          cb(null,emails);
+        }
+      });
+};
+
 /* a high level operation that attempts to sync a client's view with that of the
  * server.  email is the identity of the authenticated channel with the user,
  * identities is a map of email -> pubkey.
diff --git a/browserid/lib/wsapi.js b/browserid/lib/wsapi.js
index 831736a17c70735128fb70b5ef09ddfe2870192e..fd3a89ca71821b9250f51038ea4cf81ba8c99fad 100644
--- a/browserid/lib/wsapi.js
+++ b/browserid/lib/wsapi.js
@@ -41,11 +41,12 @@
 const
 db = require('./db.js'),
 url = require('url'),
-httputils = require('./httputils.js');
+httputils = require('./httputils.js'),
 email = require('./email.js'),
 bcrypt = require('bcrypt'),
 crypto = require('crypto'),
-logger = require('../../libs/logging.js').logger;
+logger = require('../../libs/logging.js').logger,
+ca = require('./ca.js');
 
 function checkParams(params) {
   return function(req, resp, next) {
@@ -126,8 +127,10 @@ function setup(app) {
   /* First half of account creation.  Stages a user account for creation.
    * this involves creating a secret url that must be delivered to the
    * user via their claimed email address.  Upon timeout expiry OR clickthrough
-   * the staged user account transitions to a valid user account */
-  app.post('/wsapi/stage_user', checkParams([ "email", "pass", "pubkey", "site" ]), function(req, resp) {
+   * the staged user account transitions to a valid user account
+   * MODIFICATIONS for Certs: no more pubkey in params. Null is passed to DB layer for now.
+   */
+  app.post('/wsapi/stage_user', checkParams([ "email", "pass", "site" ]), function(req, resp) {
 
     // we should be cloning this object here.
     var stageParams = req.body;
@@ -251,10 +254,11 @@ function setup(app) {
     });
   });
 
-  app.post('/wsapi/add_email', checkAuthed, checkParams(["email", "pubkey", "site"]), function (req, resp) {
+  // MODIFICATIONS for cert: remove pubkey
+  app.post('/wsapi/add_email', checkAuthed, checkParams(["email", "site"]), function (req, resp) {
     try {
-      // on failure stageEmail may throw
-      db.stageEmail(req.session.authenticatedUser, req.body.email, req.body.pubkey, function(secret) {
+      // on failure stageEmail may throw, null pubkey
+      db.stageEmail(req.session.authenticatedUser, req.body.email, function(secret) {
 
         // store the email being added in session data
         req.session.pendingAddition = req.body.email;
@@ -292,6 +296,22 @@ function setup(app) {
       }});
   });
 
+  app.post('/wsapi/cert_key', checkAuthed, checkParams(["email", "pubkey"]), function(req, resp) {
+    db.emailsBelongToSameAccount(req.session.authenticatedUser, req.body.email, function(sameAccount) {
+      // not same account? big fat error
+      if (!sameAccount) return httputils.badRequest(resp, "that email does not belong to you");
+
+      // parse the pubkey
+      var pk = ca.parsePublicKey(req.body.pubkey);
+      
+      // same account, we certify the key
+      var cert = ca.certify(req.body.email, pk);
+      resp.writeHead(200, {'Content-Type': 'text/plain'});
+      resp.write(cert);
+      resp.end();
+    });
+  });
+  
   app.post('/wsapi/set_key', checkAuthed, checkParams(["email", "pubkey"]), function (req, resp) {
     db.emailsBelongToSameAccount(req.session.authenticatedUser, req.body.email, function(sameAccount) {
       // not same account? big fat error
@@ -330,6 +350,21 @@ function setup(app) {
     resp.json('ok');
   });
 
+  // in the cert world, syncing is not necessary,
+  // just get a list of emails.
+  // returns:
+  // {
+  //   "foo@foo.com" : {..properties..}
+  //   ...
+  // }
+  app.get('/wsapi/list_emails', checkAuthed, function(req, resp) {
+    logger.debug('listing emails for ' + req.session.authenticatedUser);
+    db.listEmails(req.session.authenticatedUser, function(err, emails) {
+      if (err) httputils.serverError(resp, err);
+      else resp.json(emails);
+    });
+  });
+  
   app.post('/wsapi/sync_emails', checkAuthed, function(req,resp) {
     // validate that the post body contains an object with an .emails
     // property that is an array of strings.
@@ -338,7 +373,8 @@ function setup(app) {
       req.body.emails = JSON.parse(req.body.emails);
       Object.keys(req.body.emails).forEach(function(k) {
         if (typeof req.body.emails[k] !== 'string') {
-          throw "bogus value for key " + k;
+          // for certs, this is changing
+          // throw "bogus value for key " + k;
         }
       });
     } catch (e) {
@@ -347,7 +383,7 @@ function setup(app) {
                                   "post argument");
     }
 
-    logger.debug('sync emails called.  client provides: ' + JSON.stringify(Object.keys(req.body.emails))); 
+    logger.debug('sync emails called.  client provides: ' + JSON.stringify(req.body.emails)); 
     db.getSyncResponse(req.session.authenticatedUser, req.body.emails, function(err, syncResponse) {
       if (err) httputils.serverError(resp, err);
       else resp.json(syncResponse);
diff --git a/browserid/static/.well-known/host-meta b/browserid/static/.well-known/host-meta
index b577fea39d310044f4715813065b888767ea898d..eab9ec759cdac622c273f7fad74d08e690596002 100644
--- a/browserid/static/.well-known/host-meta
+++ b/browserid/static/.well-known/host-meta
@@ -5,6 +5,8 @@
 
   <hm:Host xmlns='http://host-meta.net/xrd/1.0'>browserid.org</hm:Host>
 
+  <Link rel="https://browserid.org/vocab#publicKey" href="/pk"></Link>
+
   <Link rel='lrdd' template='https://browserid.org/users/{uri}.xml'></Link>
 
   <Link rel='other' value='something-different'></Link>
diff --git a/browserid/static/css/style.css b/browserid/static/css/style.css
index 6cd4cf1663e90ec7843ecc716a06f41fed757e78..dcb365ad387868405f2668875ffa3ed353336cac 100644
--- a/browserid/static/css/style.css
+++ b/browserid/static/css/style.css
@@ -22,6 +22,30 @@ body {
   overflow-y: scroll;
 }
 
+/*header {
+    border-top: 4px solid #333;
+    background-color: #333;
+    padding: 0 20% 0 20%;
+    margin: 0;
+    background: #008;
+    height: 230px;
+    color #fff;
+    min-width: 800px;
+    display: block;
+}
+
+footer {
+    background-color: #F1F1F1;
+    border-top: 2px solid #ddd;
+    margin: 0;
+    margin-top: 100px;
+    padding: 0;
+    height: 200px;
+    font-size: 1.1em;
+    display: block;
+}
+
+*/
 .sans {
   font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
 }
diff --git a/browserid/static/css/style.css.bak b/browserid/static/css/style.css.bak
deleted file mode 100644
index 1afd0ae2d434477e9a279c1b030c38b6b3f7904b..0000000000000000000000000000000000000000
--- a/browserid/static/css/style.css.bak
+++ /dev/null
@@ -1,322 +0,0 @@
-@font-face {
-  font-family: 'Shadows Into Light';
-  font-style: normal;
-  font-weight: normal;
-/*  src: local('Shadows Into Light'), local('ShadowsIntoLight'), url('sil.ttf')
- *  format('truetype');*/
-}
-
-@font-face {
-  font-family: 'Tenor Sans';
-  font-style: normal;
-  font-weight: normal;
- /* src: local('Tenor Sans'), local('TenorSans'), url('ts.ttf')
-  * format('truetype');*/
-}
-
-body {
-    padding: 0;
-    margin: 0;
-    font-size: 12px;
-    font-family: 'Tenor Sans', arial, serif;
-}
-
-header {
-    border-top: 4px solid #333;
-    background-color: #333;
-    padding: 0 20% 0 20%;
-    margin: 0;
-    background: #008;
-    height: 230px;
-    color #fff;
-    min-width: 800px;
-}
-
-header.quarter {
-    height: 50px;
-}
-
-#labslogo  {
-    margin: auto;
-    text-align: left;
-    background: url("/i/labs-logo.png") 0 0 no-repeat;
-    width: 161px;
-    height: 34px;
-    margin: 10px 0 0 110px;
-    color: transparent;
-    display: inline-block;
-}
-
-.half h1 {
-    padding-top: .2em;
-    font-size: 3em;
-    font-weight: bold;
-    background: url("/i/browserid_logo.png") 0 0 no-repeat;
-    width: 366px;
-    height: 72px;
-    margin: 43px auto 0 auto;
-    clear: left;
-    color: transparent;
-}
-
-.quarter h1 {
-    float: left;
-    width: 135px;
-    height: 35px;
-    margin: 7px 0 0 10px;
-}
-
-.quarter h1 > a {
-    color: transparent;
-}
-
-
-h2 {
-    font-family: 'Shadows Into Light', arial, serif;
-    font-size: 3em;
-    font-weight: normal;
-    color: #fff;
-    margin: -4px auto 0 auto;
-    text-align: center;
-}
-
-.quarter h2 {
-    font-size: 2em;
-    margin: .2em 0 0 .5em;
-    float: left;
-}
-
-
-header > #manageLink {
-    float: right;
-    background-color: #333;
-    color: #fff;
-    padding: 1px 10px 5px 10px;
-    -webkit-border-radius: 0 0 10px 10px;
-    -moz-border-radius: 0 0 10px 10px;
-    border-radius: 0 0 10px 10px;
-    display: none;
-}
-
-.authenticated header > #manageLink {
-    display: block;
-}
-
-header > #manageLink:hover {
-    opacity:1;
-    color: #AAA;
-}
-
-.why {
-    width: 600px;
-    height: 130px;
-    padding: 10px 0;
-    margin: 0 auto;
-}
-
-.why p {
-    font-size: 2em;
-    text-align: center;
-}
-
-.why a, #cancellink {
-    color: #666;
-    text-decoration: none;
-    border-bottom: 1px dotted black;
-}
-
-.why a:hover {
-    color: #000;
-    opacity: 1;
-}
-
-a {
-    color: #fff;
-    text-decoration: none;
-}
-
-a:hover {
-    opacity: .6;
-}
-
-footer {
-    background-color: #F1F1F1;
-    border-top: 2px solid #ddd;
-    margin: 0;
-    margin-top: 100px;
-    padding: 0;
-    height: 200px;
-    font-size: 1.1em;
-}
-
-footer > div {
-    width: 800px;
-    margin: auto;
-}
-
-footer > div > div {
-    width: 400px;
-    padding: 10px;
-}
-
-footer .right {
-    padding-top: 13px;
-    float: right;
-}
-
-footer .right img {
-    margin-bottom: -5px;
-}
-
-footer a {
-    color: #666;
-}
-
-footer .right p {
-    text-align: right;
-}
-
-footer .copyright {
-    font-weight: bold;
-    font-size: .8em;
-}
-
-#steps {
-    list-style: none;
-    width: 800px;
-    margin: 0 auto;
-    padding: 0;
-}
-
-#steps a {
-    color: #666;
-    text-decoration: none;
-    border-bottom: 1px dotted #666;
-}
-
-.step  {
-    margin: 1em 0 2em 0;
-    padding: 0 0 0 50px;
-    font-size: 14px;
-    position: relative;
-}
-
-.step .number {
-    position: absolute;
-    top: -5px;
-    left: 0;
-    font-family: 'Shadows Into Light', arial, serif;
-    font-size: 4em;
-    font-weight: bold;
-    line-height: 1em;
-}
-
-.step > h3 {
-    font-size: 1em;
-    margin: 0;
-    display: inline;
-}
-
-.step > pre {
-    clear: both;
-}
-
-.step > p,  .step > ol {
-    margin-left: 50px;
-}
-
-.prose {
-    font-size: 1.5em;
-}
-
-.status {
-    margin: 0 auto;
-    width: 600px;
-    font-size: 1.2em;
-}
-
-pre code {
-    padding: 10px 15px 10px 15px;
-    margin: .75em;
-    -webkit-border-radius: 10px;
-    -moz-border-radius: 10px;
-    border-radius: 10px 10px 10px 10px;
-    width: 650px;
-}
-
-#emailList {
-  font-size: 1.0em;
-  width: 4x00px;
-  margin: auto;
-  font-weight:bold;
-  margin-top:32px;
-}
-
-#cancelaccount {
-  font-size: 1.0em;
-  width: 500px;
-  margin: auto;
-  margin-top:35px;
-}
-
-.email {
-  display:inline-block;
-}
-.emailblock a {
-  font-size:0.7em;
-  color:#405090;
-}
-.emailblock {
-  border: 1px solid #ddd;
-  -moz-border-radius: 4px;
-  -webkit-border-radius: 4px;
-  background-color:#f0f0f0;
-  width:500px;
-  padding:8px;
-  min-height:48px;
-  margin:16px auto;
-}
-.meta {
-  display:inline-block;
-  float:right;
-  font:8pt Arial;
-}
-.meta a {
-  cursor:pointer;
-}
-.keyblock {
-  font:8pt Arial;
-}
-.date {
-  font:8pt Arial;
-}
-
-.buttonbox {
-    height: 25px;
-    width: 460px;
-    margin: auto;
-}
-.buttonbox > div {
-    float: left;
-    margin-right: 10px;
-}
-
-.legal {
-   font-size: 1.3em;
-   width: 800px;
-   margin: 0 auto;
-}
-
-.legal li {
-  margin-bottom: 7px;
-}
-
-.legal h5 {
-  margin-bottom: 1px;
-  padding-bottom: 1px;
-  line-height: 100%;
-}
-
-.legal p {
-    margin-top : 1px;
-}
diff --git a/browserid/static/dialog/qunit.html b/browserid/static/dialog/qunit.html
index a22393863716be5e9f6b976a45a791fbd411f96e..8b7142e7746ea2a5a0b69dc5350c7f27ac6f3699 100644
--- a/browserid/static/dialog/qunit.html
+++ b/browserid/static/dialog/qunit.html
@@ -5,6 +5,7 @@
 		<script type='text/javascript'>
 			steal = {ignoreControllers: true}
 		</script>
+		<script type='text/javascript' src='/vepbundle'></script>
 		<script type='text/javascript' src='/steal/steal.js?/dialog/test/qunit'></script>
 	</head>
 	<body>
diff --git a/browserid/static/dialog/register_iframe.html b/browserid/static/dialog/register_iframe.html
index 0365a0de3b05ad4f96102cf237c9f1ddd52c57c4..390bf33901afdb3ae314b4d01847f40504636ca3 100644
--- a/browserid/static/dialog/register_iframe.html
+++ b/browserid/static/dialog/register_iframe.html
@@ -1,4 +1,6 @@
-<script src="../dialog/jschannel.js"></script>
-<script src="../dialog/crypto.js"></script>
-<script src="../dialog/crypto-api.js"></script>
+<head><title>non-interactive iframe</title>
+<script src="/vepbundle"></script>
+<script src="../dialog/resources/jschannel.js"></script>
+<script src="../dialog/resources/storage.js"></script>
 <script src="../dialog/register_iframe.js"></script>
+</head><body></body>
diff --git a/browserid/static/dialog/register_iframe.js b/browserid/static/dialog/register_iframe.js
index 42494702a9e5792f36c20e8630bfdf67a2f0bddb..8e8404598115a449bb4ab6ae6b1556c516dc208c 100644
--- a/browserid/static/dialog/register_iframe.js
+++ b/browserid/static/dialog/register_iframe.js
@@ -35,6 +35,11 @@
 
 // this is the picker code!  it runs in the identity provider's domain, and
 // fiddles the dom expressed by picker.html
+
+var jwk = require("./jwk"),
+    jwcert = require("./jwcert"),
+    vep = require("./vep");
+
 (function() {
   var chan = Channel.build(
     {
@@ -42,35 +47,50 @@
       origin: "*",
       scope: "mozid"
     });
+  
+  // primary requests a keygen to certify  
+  chan.bind("generateKey", function(trans, args) {
+    // keygen
+    var keypair = jwk.KeyPair.generate(vep.params.algorithm, 64);
 
-    function persistAddressAndKeyPair(email, keypair, issuer)
-    {
-        var emails = {};
-        if (window.localStorage.emails) {
-            emails = JSON.parse(window.localStorage.emails);
-        }
+    // save it in a special place for now
+    storeTemporaryKeypair(keypair);
+    
+    // serialize and return
+    return keypair.publicKey.serialize();
+  });
 
-        emails[email] = {
-            created: new Date(),
-            pub: keypair.pub,
-            priv: keypair.priv
-        };
-        if (issuer) {
-            emails[email].issuer = issuer;
-        }
-        window.localStorage.emails = JSON.stringify(emails);
-    }
+  // add the cert 
+  chan.bind("registerVerifiedEmailCertificate", function(trans, args) {
+    var keypair = retrieveTemporaryKeypair();
 
-    chan.bind("registerVerifiedEmail", function(trans, args) {
-        // This is a primary registration - the persisted
-        // identity does not have an issuer because it 
-        // was directly asserted by the controlling domain.
+    // parse the cert
+    var raw_cert = args.cert;
+    var cert = new jwcert.JWCert();
+    cert.parse(raw_cert);
+    var email = cert.principal.email;
+    var pk = cert.pk;
 
-        var keypair = CryptoStubs.genKeyPair();
-        persistAddressAndKeyPair(args.email, keypair);
-        return keypair.pub;
-    });
+    // check if the pk's match
+    if (!pk.equals(keypair.publicKey)) {
+      trans.error("bad cert");
+      return;
+    }
+    
+    var new_email_obj= {
+      created: new Date(),
+      pub: keypair.publicKey.toSimpleObject(),
+      priv: keypair.secretKey.toSimpleObject(),
+      cert: raw_cert,
+      issuer: cert.issuer,
+      isPrimary: true
+    };
+
+    addEmail(email, new_email_obj);
+  });
 
+  // reenable this once we're ready
+  /*
     function isSuperDomain(domain) {
         return true;
     }
@@ -158,4 +178,5 @@
         // if we get here, we've failed
         trans.error("X", "not a proper token-based call");
     });
+    */
 })();
diff --git a/browserid/static/dialog/resources/browserid-errors.js b/browserid/static/dialog/resources/browserid-errors.js
index e4ed574ee58c214ceff0ccc18ffa25d2febae21b..fe8703706f9028bf9abcb0cae4b054cf18547758 100644
--- a/browserid/static/dialog/resources/browserid-errors.js
+++ b/browserid/static/dialog/resources/browserid-errors.js
@@ -51,7 +51,7 @@ var BrowserIDErrors = (function(){
     checkAuthentication: {
       type: "serverError",
       message: "Error Checking Authentication",
-      description: "There was a tenical problem while trying to log you in.  Yucky!"
+      description: "There was a technical problem while trying to log you in.  Yucky!"
     },
 
     createAccount: {
diff --git a/browserid/static/dialog/resources/browserid-identities.js b/browserid/static/dialog/resources/browserid-identities.js
index aa97a191c5c0c58d3989aea06bfd9f5943ddd8e7..50dc0b96ddceb53c37a2ccc027f5a48628096b7a 100644
--- a/browserid/static/dialog/resources/browserid-identities.js
+++ b/browserid/static/dialog/resources/browserid-identities.js
@@ -34,16 +34,28 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
+
+var jwk = require("./jwk");
+var jwt = require("./jwt");
+var vep = require("./vep");
+
 var BrowserIDIdentities = (function() {
   "use strict";
   function getIssuedIdentities() {
       var emails = getEmails();
       var issued_identities = {};
       _(emails).each(function(email_obj, email_address) {
-        issued_identities[email_address] = email_obj.pub;
+        try {
+          email_obj.pub = jwk.PublicKey.fromSimpleObject(email_obj.pub);
+        } catch (x) {
+          delete emails[email_address];
+        }
+
+        if (!email_obj.cert)
+          delete emails[email_address];
       });
 
-      return issued_identities;
+      return emails;
   }
 
   function removeUnknownIdentities(unknown_emails) {
@@ -78,6 +90,8 @@ var BrowserIDIdentities = (function() {
     syncIdentities: function(onSuccess, onFailure) {
       var issued_identities = getIssuedIdentities();
 
+      // FIXME for certs
+      
       // send up all email/pubkey pairs to the server, it will response with a
       // list of emails that need new keys.  This may include emails in the
       // sent list, and also may include identities registered on other devices.
@@ -87,6 +101,39 @@ var BrowserIDIdentities = (function() {
       // and we don't need to worry about rekeying them.
 
       var self = this;
+
+      network.listEmails(function(emails) {
+        // lists of emails
+        var client_emails = _.keys(issued_identities);
+        var server_emails = _.keys(emails);
+
+        var emails_to_add = _.difference(server_emails, client_emails);
+        var emails_to_remove = _.difference(client_emails, server_emails);
+        
+        // remove emails
+        _.each(emails_to_remove, function(email) {
+          // if it's not a primary
+          if (!issued_identities[email].isPrimary)
+            removeEmail(email);
+        });
+
+        // keygen for new emails
+        // asynchronous
+        function addNextEmail() {
+          if (!emails_to_add || !emails_to_add.length) {
+            onSuccess();
+            return;
+          }
+
+          var email = emails_to_add.shift();
+
+          self.syncIdentity(email, addNextEmail, onFailure);
+        }
+
+        addNextEmail();
+      });
+
+      /*
       network.syncEmails(issued_identities, function(resp) {
         removeUnknownIdentities(resp.unknown_emails);
 
@@ -106,25 +153,28 @@ var BrowserIDIdentities = (function() {
 
         addNextEmail();
       }, onFailure);
+      */
     },
 
     /**
      * Stage an identity - this creates an identity that must be verified.  
      * Used when creating a new account or resetting the password of an 
      * existing account.
+     * FIXME: rename to indicate new account
      * @method stageIdentity
      * @param {string} email - Email address.
      * @param {function} [onSuccess] - Called on successful completion. 
      * @param {function} [onFailure] - Called on error.
      */
     stageIdentity: function(email, password, onSuccess, onFailure) {
-      var self=this,
-          keypair = CryptoStubs.genKeyPair();
+      var self=this;
+      // FIXME: keysize
+      var keypair = jwk.KeyPair.generate(vep.params.algorithm, 64);
 
       self.stagedEmail = email;
       self.stagedKeypair = keypair;
 
-      network.stageUser(email, password, keypair, function() {
+      network.stageUser(email, password, function() {
         if (onSuccess) {
           onSuccess(keypair);
         }
@@ -141,10 +191,16 @@ var BrowserIDIdentities = (function() {
     confirmIdentity: function(email, onSuccess, onFailure) {
       var self = this;
       if (email === self.stagedEmail) {
+        var keypair = self.stagedKeypair;
+        
         self.stagedEmail = null;
-        self.persistIdentity(self.stagedEmail, self.stagedKeypair, "browserid.org:443", function() {
+        self.stagedKeypair = null;
+
+        // certify
+        Identities.certifyIdentity(email, keypair, function() {
           self.syncIdentities(onSuccess, onFailure);
-        }, onFailure);
+        });
+
       }
       else if (onFailure) {
         onFailure();
@@ -213,6 +269,19 @@ var BrowserIDIdentities = (function() {
       }, onFailure);
     },
 
+    /**
+     * Certify an identity
+     */
+    certifyIdentity: function(email, keypair, onSuccess, onFailure) {
+      network.certKey(email, keypair.publicKey, function(cert) {
+        Identities.persistIdentity(email, keypair, cert, function() {
+          if (onSuccess) {
+            onSuccess();
+          }
+        }, onFailure);
+      }, onFailure);      
+    },
+    
     /**
      * Sync an identity with the server.  Creates and stores locally and on the 
      * server a keypair for the given email address.
@@ -222,15 +291,11 @@ var BrowserIDIdentities = (function() {
      * @param {function} [onSuccess] - Called on successful completion. 
      * @param {function} [onFailure] - Called on error.
      */
-    syncIdentity: function(email, issuer, onSuccess, onFailure) {
-      var keypair = CryptoStubs.genKeyPair();
-      network.setKey(email, keypair, function() {
-        Identities.persistIdentity(email, keypair, issuer, function() {
-          if (onSuccess) {
-            onSuccess(keypair);
-          }
-        }, onFailure);
-      }, onFailure);
+    syncIdentity: function(email, onSuccess, onFailure) {
+      // FIXME use true key sizes
+      //var keypair = jwk.KeyPair.generate(vep.params.algorithm, vep.params.keysize);
+      var keypair = jwk.KeyPair.generate(vep.params.algorithm, 64);
+      Identities.certifyIdentity(email, keypair, onSuccess, onFailure);
     },
 
     /**
@@ -244,13 +309,14 @@ var BrowserIDIdentities = (function() {
      * @param {function} [onFailure] - Called on error.
      */
     addIdentity: function(email, onSuccess, onFailure) {
-      var self=this, 
-          keypair = CryptoStubs.genKeyPair();
+      var self = this;
+      var keypair = jwk.KeyPair.generate(vep.params.algorithm, 64);
 
       self.stagedEmail = email;
       self.stagedKeypair = keypair;
 
-      network.addEmail(email, keypair, function() {
+      // we no longer send the keypair, since we will certify it later.
+      network.addEmail(email, function() {
         if (onSuccess) {
           onSuccess(keypair);
         }
@@ -265,17 +331,14 @@ var BrowserIDIdentities = (function() {
      * @param {function} [onSuccess] - Called on successful completion. 
      * @param {function} [onFailure] - Called on error.
      */
-    persistIdentity: function(email, keypair, issuer, onSuccess, onFailure) {
+    persistIdentity: function(email, keypair, cert, onSuccess, onFailure) {
       var new_email_obj= {
         created: new Date(),
-        pub: keypair.pub,
-        priv: keypair.priv
+        pub: keypair.publicKey.toSimpleObject(),
+        priv: keypair.secretKey.toSimpleObject(),
+        cert: cert
       };
 
-      if (issuer) {
-        new_email_obj.issuer = issuer;
-      }
-      
       addEmail(email, new_email_obj);
 
       if (onSuccess) {
@@ -307,11 +370,14 @@ var BrowserIDIdentities = (function() {
      * @param {function} [onFailure] - Called on failure.
      */
     getIdentityAssertion: function(email, onSuccess, onFailure) {
-      var storedID = getEmails()[email],
+      var storedID = Identities.getStoredIdentities()[email],
           assertion;
 
       if (storedID) {
-          assertion = CryptoStubs.createAssertion(network.origin, email, storedID.priv, storedID.issuer);
+        // parse the secret key
+        var sk = jwk.SecretKey.fromSimpleObject(storedID.priv);
+        var tok = new jwt.JWT(null, new Date(), network.origin);
+        assertion = vep.bundleCertsAndAssertion([storedID.cert], tok.sign(sk));
       }
 
       if (onSuccess) {
diff --git a/browserid/static/dialog/resources/browserid-network.js b/browserid/static/dialog/resources/browserid-network.js
index 247f32b60109098399aa8048b5d91b754795c4f4..7a1546330e615cf080be603d3982ec950ddbb6eb 100644
--- a/browserid/static/dialog/resources/browserid-network.js
+++ b/browserid/static/dialog/resources/browserid-network.js
@@ -134,6 +134,7 @@ var BrowserIDNetwork = (function() {
     /**
      * Create a new user or reset a current user's password.  Requires a user 
      * to verify identity.
+     * changes for certs: removed keypair.
      * @method stageUser
      * @param {string} email - Email address to prepare.
      * @param {string} password - Password for user.
@@ -141,7 +142,7 @@ var BrowserIDNetwork = (function() {
      * @param {function} [onSuccess] - Callback to call when complete.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    stageUser: function(email, password, keypair, onSuccess, onFailure) {
+    stageUser: function(email, password, onSuccess, onFailure) {
       withCSRF(function() { 
         $.ajax({
           type: "post",
@@ -149,7 +150,6 @@ var BrowserIDNetwork = (function() {
           data: {
             email: email,
             pass: password,
-            pubkey : keypair.pub,
             site : BrowserIDNetwork.origin || document.location.host,
             csrf : csrf_token
           },
@@ -205,18 +205,16 @@ var BrowserIDNetwork = (function() {
      * Add an email to the current user's account.
      * @method addEmail
      * @param {string} email - Email address to add.
-     * @param {object} keypair - Email's public/private key pair.
      * @param {function} [onSuccess] - Called when complete.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    addEmail: function(email, keypair, onSuccess, onFailure) {
+    addEmail: function(email, onSuccess, onFailure) {
       withCSRF(function() { 
         $.ajax({
           type: 'POST',
           url: '/wsapi/add_email',
           data: {
             email: email,
-            pubkey: keypair.pub,
             site: BrowserIDNetwork.origin || document.location.host,
             csrf: csrf_token
           },
@@ -308,6 +306,39 @@ var BrowserIDNetwork = (function() {
       });
     },
 
+    /**
+     * Certify the public key for the email address.
+     * @method certKey
+     */
+    certKey: function(email, pubkey, onSuccess, onError) {
+      withCSRF(function() { 
+        $.ajax({
+          type: 'POST',
+          url: '/wsapi/cert_key',
+          data: {
+            email: email,
+            pubkey: pubkey.serialize(),
+            csrf: csrf_token
+          },
+          success: onSuccess,
+          error: onError
+        });
+      });
+    },
+
+    /**
+     * List emails
+     * @method listEmails
+     */
+    listEmails: function(onSuccess, onFailure) {
+      $.ajax({
+        type: "GET",
+        url: "/wsapi/list_emails",
+        success: onSuccess,
+        error: onFailure
+      });
+    },
+    
     /**
      * Sync emails
      * @method syncEmails
diff --git a/browserid/static/dialog/resources/storage.js b/browserid/static/dialog/resources/storage.js
index 0a97ee8149984de75bbe5463ec270fa6bbb57944..8a57a426dc56bf8d9818d3f45a33143a3ab4fa10 100644
--- a/browserid/static/dialog/resources/storage.js
+++ b/browserid/static/dialog/resources/storage.js
@@ -33,6 +33,8 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
+var jwk = require("./jwk");
+
 var getEmails = function() {
   try {
     var emails = JSON.parse(window.localStorage.emails);
@@ -64,4 +66,24 @@ var removeEmail = function(email) {
 
 var clearEmails = function() {
   _storeEmails({});
+};
+
+var storeTemporaryKeypair = function(keypair) {
+  window.localStorage.tempKeypair = JSON.stringify({
+    publicKey: keypair.publicKey.toSimpleObject(),
+    secretKey: keypair.secretKey.toSimpleObject()
+  });
+};
+
+var retrieveTemporaryKeypair = function() {
+  var raw_kp = JSON.parse(window.localStorage.tempKeypair);
+  window.localStorage.tempKeypair = null;
+  if (raw_kp) {
+    var kp = new jwk.KeyPair();
+    kp.publicKey = jwk.PublicKey.fromSimpleObject(raw_kp.publicKey);
+    kp.secretKey = jwk.SecretKey.fromSimpleObject(raw_kp.secretKey);
+    return kp;
+  } else {
+    return null;
+  }
 };
\ No newline at end of file
diff --git a/browserid/static/dialog/style.css b/browserid/static/dialog/style.css
index 31a0c8a78fae8e1078d1b9b5f9849058d4426605..43d9c88e3ef48945867d8b361c2089b37a78da49 100644
--- a/browserid/static/dialog/style.css
+++ b/browserid/static/dialog/style.css
@@ -250,7 +250,7 @@ div.dialog div.attention_lame {
 
 #logo {
     background-position: 0 -26px;;
-    color: transparent;
+    font-size: 0px;
     width: 135px;
     height: 35px;
     margin: 2px 16px 0 16px;
@@ -259,7 +259,7 @@ div.dialog div.attention_lame {
 
 #subtitle {
     margin: 8px 0 0;
-    color: transparent;
+    font-size: 0px;
     background-position: 0 0;
     width: 146px;
     height: 25px;
diff --git a/browserid/static/dialog/test/qunit/browserid-identities_unit_test.js b/browserid/static/dialog/test/qunit/browserid-identities_unit_test.js
index 045e7d50451844a575129e1d1e243bd189e0e15c..39cebdbdc534a13d709a58cc45de61111b3bc7e7 100644
--- a/browserid/static/dialog/test/qunit/browserid-identities_unit_test.js
+++ b/browserid/static/dialog/test/qunit/browserid-identities_unit_test.js
@@ -38,16 +38,19 @@
  * This test assumes for authentication that there is a user named 
  * "testuser@testuser.com" with the password "testuser"
  */
+var jwk = require("./jwk");
+
 steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid-identities", function() {
-  var credentialsValid, unknownEmails, keyRefresh, syncValid;
+  var credentialsValid, unknownEmails, keyRefresh, syncValid, userEmails;
   var netStub = {
     reset: function() {
       credentialsValid = syncValid = true;
       unknownEmails = [];
       keyRefresh = [];
+      userEmails = {"testuser@testuser.com": {}};
     },
 
-    stageUser: function(email, password, keypair, onSuccess) {
+    stageUser: function(email, password, onSuccess) {
       onSuccess();
     },
 
@@ -59,7 +62,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid-iden
       onSuccess(credentialsValid);
     },
 
-    addEmail: function(email, keypair, onSuccess, onFailure) {
+    addEmail: function(email, onSuccess, onFailure) {
       onSuccess();
     },
 
@@ -67,6 +70,19 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid-iden
       onSuccess();
     },
 
+    listEmails: function(onSuccess, onFailure) {
+      onSuccess(userEmails);
+    },
+
+    certKey: function(email, pubkey, onSuccess, onFailure) {
+      if (syncValid) {
+        onSuccess("foocert");
+      }
+      else {
+        onFailure();
+      }
+    },
+    
     syncEmails: function(issued_identities, onSuccess, onFailure) {
       onSuccess({
         unknown_emails: unknownEmails,
@@ -285,7 +301,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid-iden
     clearEmails();
 
     syncValid = true;
-    BrowserIDIdentities.syncIdentity("testemail@testemail.com", "issuer", function(keypair) {
+    BrowserIDIdentities.syncIdentity("testemail@testemail.com", function(keypair) {
       var identities = BrowserIDIdentities.getStoredIdentities();
       ok("testemail@testemail.com" in identities, "Valid email is synced");
 
@@ -300,7 +316,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid-iden
     clearEmails();
 
     syncValid = false;
-    BrowserIDIdentities.syncIdentity("testemail@testemail.com", "issuer", function(keypair) {
+    BrowserIDIdentities.syncIdentity("testemail@testemail.com", function(keypair) {
       ok(false, "sync was invalid, this should have failed");
       start();
     }, function() {
@@ -316,7 +332,8 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid-iden
 
 
   test("persistIdentity", function() {
-    BrowserIDIdentities.persistIdentity("testemail2@testemail.com", { pub: "pub", priv: "priv" }, undefined, function onSuccess() {
+    var user_kp = jwk.KeyPair.generate("RS",64);
+    BrowserIDIdentities.persistIdentity("testemail2@testemail.com", user_kp, undefined, function onSuccess() {
       var identities = BrowserIDIdentities.getStoredIdentities();
       ok("testemail2@testemail.com" in identities, "Our new email is added");
       start(); 
@@ -357,6 +374,8 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid-iden
 
   test("syncIdentities with no pre-loaded identities and no identities to add", function() {
     clearEmails();
+    userEmails = {};
+
     BrowserIDIdentities.syncIdentities(function onSuccess() {
       var identities = BrowserIDIdentities.getStoredIdentities();
       ok(true, "we have synced identities");
@@ -369,7 +388,8 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid-iden
 
   test("syncIdentities with no pre-loaded identities and identities to add", function() {
     clearEmails();
-    keyRefresh = ["testuser@testuser.com"];
+    userEmails = {"testuser@testuser.com": {}};
+
     BrowserIDIdentities.syncIdentities(function onSuccess() {
       var identities = BrowserIDIdentities.getStoredIdentities();
       ok("testuser@testuser.com" in identities, "Our new email is added");
@@ -382,6 +402,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid-iden
 
   test("syncIdentities with identities preloaded and none to add", function() {
     clearEmails();
+    userEmails = {"testuser@testuser.com": {}};
     addEmail("testuser@testuser.com", {});
 
     BrowserIDIdentities.syncIdentities(function onSuccess() {
@@ -398,7 +419,8 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid-iden
   test("syncIdentities with identities preloaded and one to add", function() {
     clearEmails();
     addEmail("testuser@testuser.com", {});
-    keyRefresh = ["testuser2@testuser.com"];
+    userEmails = {"testuser@testuser.com": {},
+                  "testuser2@testuser.com": {}};
 
     BrowserIDIdentities.syncIdentities(function onSuccess() {
       var identities = BrowserIDIdentities.getStoredIdentities();
@@ -416,8 +438,8 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid-iden
     clearEmails();
     addEmail("testuser@testuser.com", {});
     addEmail("testuser2@testuser.com", {});
-    unknownEmails = ["testuser2@testuser.com"];
-
+    userEmails = {"testuser@testuser.com": {}};
+    
     BrowserIDIdentities.syncIdentities(function onSuccess() {
       var identities = BrowserIDIdentities.getStoredIdentities();
       ok("testuser@testuser.com" in identities, "Our old email address is still there");
@@ -432,13 +454,13 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid-iden
 
   test("getIdentityAssertion with known email", function() {
     clearEmails();
-    var keypair = CryptoStubs.genKeyPair();
-    addEmail("testuser@testuser.com", { priv: keypair.priv, issuer: "issuer" });
-
-    BrowserIDIdentities.getIdentityAssertion("testuser@testuser.com", function onSuccess(assertion) {
-      equal("string", typeof assertion, "we have an assertion!");
-      start();
-    });
+    var keypair = jwk.KeyPair.generate("RS",64);
+    BrowserIDIdentities.certifyIdentity("testuser@testuser.com", keypair, function() {
+      BrowserIDIdentities.getIdentityAssertion("testuser@testuser.com", function onSuccess(assertion) {
+        equal("string", typeof assertion, "we have an assertion!");
+        start();
+      });
+    }, failure("getIdentityAssertion failure"));
 
     stop();
   });
@@ -446,14 +468,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid-iden
 
   test("getIdentityAssertion with unknown email", function() {
     clearEmails();
-    var keypair = CryptoStubs.genKeyPair();
-    addEmail("testuser@testuser.com", { priv: keypair.priv, issuer: "issuer" });
-
-    BrowserIDIdentities.getIdentityAssertion("testuser2@testuser.com", function onSuccess(assertion) {
-      equal("undefined", typeof assertion, "email was unknown, we do not have an assertion");
-      start();
-    });
-
+    var keypair = jwk.KeyPair.generate("RS",64);
+    BrowserIDIdentities.certifyIdentity("testuser@testuser.com", keypair, function() {
+      BrowserIDIdentities.getIdentityAssertion("testuser2@testuser.com", function onSuccess(assertion) {
+        equal("undefined", typeof assertion, "email was unknown, we do not have an assertion");
+        start();
+      });
+    }, failure("getIdentityAssertion failure"));
+    
     stop();
   });
 
diff --git a/browserid/static/ietest.html b/browserid/static/ietest.html
new file mode 100644
index 0000000000000000000000000000000000000000..92de8c770a4db098b4d20741336937001d41ca84
--- /dev/null
+++ b/browserid/static/ietest.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>IE test</title>
+  </head>
+  <body>
+    some content
+    <iframe src="https://dev.diresworb.org/relay.html"></iframe>
+
+    <button id="addFrame">Add Frame</button>
+      
+    <script type="text/javascript">
+      var addFrame = document.querySelector('#addFrame');
+      addFrame.addEventListener("click", function(event) {
+        event.preventDefault();
+
+        var iframe = document.createElement('iframe');
+        iframe.src = "https://dev.diresworb.org/relay";
+        document.body.appendChild(iframe);
+
+      }, false);
+    </script>
+  </body>
+</html>
diff --git a/browserid/static/include.js b/browserid/static/include.js
index e899d6b475e1acfad4765b4121ef639c1cda28e7..644f7a978eb57ddd5a9779cdd0cc7295ea484e5d 100644
--- a/browserid/static/include.js
+++ b/browserid/static/include.js
@@ -44,7 +44,7 @@ if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed)
 {
   var ipServer = "https://browserid.org";
   var isMobile = navigator.userAgent.indexOf('Fennec/') != -1;
-
+  
   // local embedded copy of jschannel: http://github.com/mozilla/jschannel
   var Channel = (function() {
     // current transaction id, start out at a random *odd* number between 1 and a million
@@ -52,7 +52,7 @@ if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed)
     // channel instances.  That means of all messages posted from a single javascript
     // evaluation context, we'll never have two with the same id.
     var s_curTranId = Math.floor(Math.random()*1000001);
-
+    
     // no two bound channels in the same javascript evaluation context may have the same origin & scope.
     // futher if two bound channels have the same scope, they may not have *overlapping* origins
     // (either one or both support '*').  This restriction allows a single onMessage handler to efficiently
@@ -412,7 +412,7 @@ if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed)
               // what can we do?  Also, here we'll ignore return values
             }
           }
-        }
+        };
 
         // now register our bound channel for msg routing
         s_addBoundChan(cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''), onMessage);
@@ -421,7 +421,7 @@ if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed)
         var scopeMethod = function(m) {
           if (typeof cfg.scope === 'string' && cfg.scope.length) m = [cfg.scope, m].join("::");
           return m;
-        }
+        };
 
         // a small wrapper around postmessage whose primary function is to handle the
         // case that clients start sending messages before the other end is "ready"
@@ -444,7 +444,7 @@ if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed)
 
             cfg.window.postMessage(JSON.stringify(msg), cfg.origin);
           }
-        }
+        };
 
         var onReady = function(trans, type) {
           debug('ready msg received');
@@ -564,27 +564,30 @@ if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed)
 
   var chan = undefined;
 
-  function _create_iframe(doc) {
-      var iframe = doc.createElement("iframe");
-      iframe.style.display = "none";
-      doc.body.appendChild(iframe);
-      iframe.src = ipServer + "/register_iframe";
-      return iframe;
+  // this is for calls that are non-interactive
+  function _open_hidden_iframe(doc) {
+    var iframe = doc.createElement("iframe");
+    // iframe.style.display = "none";
+    doc.body.appendChild(iframe);
+    iframe.src = ipServer + "/register_iframe";
+    return iframe;
   }
-
+  
   function _open_relay_frame(doc) {
-      var iframe = doc.createElement("iframe");
-      iframe.setAttribute('name', 'browserid_relay');
-      iframe.setAttribute('src', ipServer + "/relay");
-      iframe.style.display = "none";
-      doc.body.appendChild(iframe);
-      return iframe;
+    var iframe = doc.createElement("iframe");
+    iframe.setAttribute('name', 'browserid_relay');
+    iframe.setAttribute('src', ipServer + "/relay");
+    iframe.style.display = "none";
+    doc.body.appendChild(iframe);
+    return iframe;
   }
-
+  
   function _open_window() {
-      return window.open(
-          ipServer + "/sign_in#host=" + document.location.host, "_mozid_signin",
-          isMobile ? undefined : "menubar=0,location=0,resizable=0,scrollbars=0,status=0,dialog=1,width=520,height=350");
+    // FIXME: need to pass the location in a more trustworthy fashion
+    // HOW? set up a direct reference to the open window
+    return window.open(
+      ipServer + "/sign_in#host=" + document.location.host, "_mozid_signin",
+      isMobile ? undefined : "menubar=0,location=0,resizable=0,scrollbars=0,status=0,dialog=1,width=520,height=350");
   }
 
   navigator.id.getVerifiedEmail = function(callback) {
@@ -595,7 +598,7 @@ if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed)
     // clean up a previous channel that never was reaped
     if (chan) chan.destroy();
     chan = Channel.build({window: iframe.contentWindow, origin: ipServer, scope: "mozid"});
-
+    
     function cleanup() {
       chan.destroy();
       chan = undefined;
@@ -620,6 +623,7 @@ if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed)
     });
   };
 
+/*
   // preauthorize a particular email
   // FIXME: lots of cut-and-paste code here, need to refactor
   // not refactoring now because experimenting and don't want to break existing code
@@ -650,8 +654,11 @@ if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed)
       }
     });
   };
+  */
 
   // get a particular verified email
+  // FIXME: needs to ditched for now until fixed
+  /*
   navigator.id.getSpecificVerifiedEmail = function(email, token, onsuccess, onerror) {
     var doc = window.document;
 
@@ -697,24 +704,27 @@ if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed)
       }
     });
   };
+  */
 
-  navigator.id.registerVerifiedEmail = function(email, onsuccess, onerror) {
+  function _noninteractiveCall(method, args, onsuccess, onerror) {
     var doc = window.document;
-    iframe = _create_iframe(doc);
+    iframe = _open_hidden_iframe(doc);
+
+    // clean up channel
     if (chan) chan.destroy();
     chan = Channel.build({window: iframe.contentWindow, origin: ipServer, scope: "mozid"});
-
+    
     function cleanup() {
       chan.destroy();
       chan = undefined;
       doc.body.removeChild(iframe);
     }
-
+    
     chan.call({
-      method: "registerVerifiedEmail",
-      params: {email:email},
+      method: method,
+      params: args,
       success: function(rv) {
-        console.log("registerVerifiedEmail channel returned: rv is " + rv);
+        console.log(method + " channel returned: rv is " + rv);
         if (onsuccess) {
           onsuccess(rv);
         }
@@ -724,8 +734,28 @@ if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed)
         if (onerror) onerror(code, msg);
         cleanup();
       }
-    });
+    });    
+  }
+
+  // check if a valid cert exists for this verified email
+  // calls back with true or false
+  // FIXME: implement it for real, but
+  // be careful here because this needs to be limited
+  navigator.id.checkVerifiedEmail = function(email, onsuccess, onerror) {
+    onsuccess(false);
   };
 
+  // generate a keypair
+  navigator.id.generateKey = function(onsuccess, onerror) {
+    _noninteractiveCall("generateKey", {},
+                        onsuccess, onerror);
+  };
+  
+  navigator.id.registerVerifiedEmailCertificate = function(certificate, updateURL, onsuccess, onerror) {
+    _noninteractiveCall("registerVerifiedEmailCertificate",
+                        {cert:certificate, updateURL: updateURL},
+                        onsuccess, onerror);
+  };
+  
   navigator.id._getVerifiedEmailIsShimmed = true;
 }
diff --git a/browserid/static/js/browserid.js b/browserid/static/js/browserid.js
index 12d0fed2f7aa62d546b2271d667174c06d04aa47..f3e750d0bf972de51ec76d1a48c92c0632f51b9f 100644
--- a/browserid/static/js/browserid.js
+++ b/browserid/static/js/browserid.js
@@ -1,4 +1,4 @@
-/*globals: BrowserIDNetwork: true */
+/*globals BrowserIDNetwork: true, BrowserIDIdentities: true, _: true, confirm: true, getEmails: true, display_saved_ids: true, displayEmails: true, removeEmail: true*/
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -35,6 +35,7 @@
  * ***** END LICENSE BLOCK ***** */
 
 $(function() {
+  "use strict";
 
   if ($('#vAlign').length) {
     $(window).bind('resize', function() { $('#vAlign').css({'height' : $(window).height() }); }).trigger('resize');
@@ -208,3 +209,86 @@ function display_saved_ids() {
   }
 }
 
+/*
+=======
+  $(function() {
+    BrowserIDNetwork.checkAuth(function(authenticated) {
+      if (authenticated) {
+        $("body").addClass("authenticated");
+        if ($('#emailList').length) {
+          display_saved_ids();
+        }
+      }
+    });
+  });
+
+  function display_saved_ids()
+  {
+    var emails = {};
+    BrowserIDIdentities.syncIdentities(function() {
+      emails = getEmails();
+      displayEmails();
+    });
+
+
+    function displayEmails() {
+      $('#cancellink').click(function() {
+        if (confirm('Are you sure you want to cancel your account?')) {
+          BrowserIDNetwork.cancelUser(function() {
+            document.location="/";
+          });
+        }
+      });
+
+      $("#emailList").empty();
+        _(emails).each(function(data, e) {
+          var block = $("<div>").addClass("emailblock");
+          var label = $("<div>").addClass("email").text(e);
+          var meta = $("<div>").addClass("meta");
+
+          var pub = $("<div class='keyblock'>").hide();
+          
+          var keyText = data.pub.value;
+          pub.text(keyText);
+
+          var linkblock = $("<div>");
+          var puba = $("<a>").text("[show public key]");
+          // var priva = $("<a>").text("[show private key]");
+          puba.click(function() {pub.show();});
+          // priva.click(function() {priv.show()});
+          linkblock.append(puba);
+          // linkblock.append(" / ");
+          // linkblock.append(priva);
+          
+          var deauth = $("<button>").text("Forget this Email");
+          meta.append(deauth);
+          deauth.click(function(data) {
+            // If it is a primary, we do not have to go back to the server.
+            // XXX put this into the BrowserIDIdentities abstraction
+            if (data.isPrimary) {
+              removeEmail(e);
+              display_saved_ids();
+            }
+            else {
+              // remove email from server
+              BrowserIDNetwork.removeEmail(e, display_saved_ids);
+            }
+          }.bind(null, data));
+        
+          var d = new Date(data.created);
+          var datestamp = $("<div class='date'>").text("Signed in at " + d.toLocaleString());
+
+          meta.append(datestamp);
+          meta.append(linkblock);
+                      
+          block.append(label);
+          block.append(meta);
+          // block.append(priv);
+          block.append(pub);
+          
+          $("#emailList").append(block);
+      });
+    }
+  }
+}());
+  */
diff --git a/browserid/static/dialog/html5shim.js b/browserid/static/js/html5shim.js
similarity index 100%
rename from browserid/static/dialog/html5shim.js
rename to browserid/static/js/html5shim.js
diff --git a/browserid/static/js/json2.js b/browserid/static/js/json2.js
new file mode 100644
index 0000000000000000000000000000000000000000..b4c02d3f08be98fc3bf745bc4382bc886c27ce1a
--- /dev/null
+++ b/browserid/static/js/json2.js
@@ -0,0 +1,480 @@
+/*
+    http://www.JSON.org/json2.js
+    2011-02-23
+
+    Public Domain.
+
+    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+    See http://www.JSON.org/js.html
+
+
+    This code should be minified before deployment.
+    See http://javascript.crockford.com/jsmin.html
+
+    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
+    NOT CONTROL.
+
+
+    This file creates a global JSON object containing two methods: stringify
+    and parse.
+
+        JSON.stringify(value, replacer, space)
+            value       any JavaScript value, usually an object or array.
+
+            replacer    an optional parameter that determines how object
+                        values are stringified for objects. It can be a
+                        function or an array of strings.
+
+            space       an optional parameter that specifies the indentation
+                        of nested structures. If it is omitted, the text will
+                        be packed without extra whitespace. If it is a number,
+                        it will specify the number of spaces to indent at each
+                        level. If it is a string (such as '\t' or '&nbsp;'),
+                        it contains the characters used to indent at each level.
+
+            This method produces a JSON text from a JavaScript value.
+
+            When an object value is found, if the object contains a toJSON
+            method, its toJSON method will be called and the result will be
+            stringified. A toJSON method does not serialize: it returns the
+            value represented by the name/value pair that should be serialized,
+            or undefined if nothing should be serialized. The toJSON method
+            will be passed the key associated with the value, and this will be
+            bound to the value
+
+            For example, this would serialize Dates as ISO strings.
+
+                Date.prototype.toJSON = function (key) {
+                    function f(n) {
+                        // Format integers to have at least two digits.
+                        return n < 10 ? '0' + n : n;
+                    }
+
+                    return this.getUTCFullYear()   + '-' +
+                         f(this.getUTCMonth() + 1) + '-' +
+                         f(this.getUTCDate())      + 'T' +
+                         f(this.getUTCHours())     + ':' +
+                         f(this.getUTCMinutes())   + ':' +
+                         f(this.getUTCSeconds())   + 'Z';
+                };
+
+            You can provide an optional replacer method. It will be passed the
+            key and value of each member, with this bound to the containing
+            object. The value that is returned from your method will be
+            serialized. If your method returns undefined, then the member will
+            be excluded from the serialization.
+
+            If the replacer parameter is an array of strings, then it will be
+            used to select the members to be serialized. It filters the results
+            such that only members with keys listed in the replacer array are
+            stringified.
+
+            Values that do not have JSON representations, such as undefined or
+            functions, will not be serialized. Such values in objects will be
+            dropped; in arrays they will be replaced with null. You can use
+            a replacer function to replace those with JSON values.
+            JSON.stringify(undefined) returns undefined.
+
+            The optional space parameter produces a stringification of the
+            value that is filled with line breaks and indentation to make it
+            easier to read.
+
+            If the space parameter is a non-empty string, then that string will
+            be used for indentation. If the space parameter is a number, then
+            the indentation will be that many spaces.
+
+            Example:
+
+            text = JSON.stringify(['e', {pluribus: 'unum'}]);
+            // text is '["e",{"pluribus":"unum"}]'
+
+
+            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
+            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
+
+            text = JSON.stringify([new Date()], function (key, value) {
+                return this[key] instanceof Date ?
+                    'Date(' + this[key] + ')' : value;
+            });
+            // text is '["Date(---current time---)"]'
+
+
+        JSON.parse(text, reviver)
+            This method parses a JSON text to produce an object or array.
+            It can throw a SyntaxError exception.
+
+            The optional reviver parameter is a function that can filter and
+            transform the results. It receives each of the keys and values,
+            and its return value is used instead of the original value.
+            If it returns what it received, then the structure is not modified.
+            If it returns undefined then the member is deleted.
+
+            Example:
+
+            // Parse the text. Values that look like ISO date strings will
+            // be converted to Date objects.
+
+            myData = JSON.parse(text, function (key, value) {
+                var a;
+                if (typeof value === 'string') {
+                    a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+                    if (a) {
+                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+                            +a[5], +a[6]));
+                    }
+                }
+                return value;
+            });
+
+            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
+                var d;
+                if (typeof value === 'string' &&
+                        value.slice(0, 5) === 'Date(' &&
+                        value.slice(-1) === ')') {
+                    d = new Date(value.slice(5, -1));
+                    if (d) {
+                        return d;
+                    }
+                }
+                return value;
+            });
+
+
+    This is a reference implementation. You are free to copy, modify, or
+    redistribute.
+*/
+
+/*jslint evil: true, strict: false, regexp: false */
+
+/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
+    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
+    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
+    lastIndex, length, parse, prototype, push, replace, slice, stringify,
+    test, toJSON, toString, valueOf
+*/
+
+
+// Create a JSON object only if one does not already exist. We create the
+// methods in a closure to avoid creating global variables.
+
+var JSON;
+if (!JSON) {
+    JSON = {};
+}
+
+(function () {
+    "use strict";
+
+    function f(n) {
+        // Format integers to have at least two digits.
+        return n < 10 ? '0' + n : n;
+    }
+
+    if (typeof Date.prototype.toJSON !== 'function') {
+
+        Date.prototype.toJSON = function (key) {
+
+            return isFinite(this.valueOf()) ?
+                this.getUTCFullYear()     + '-' +
+                f(this.getUTCMonth() + 1) + '-' +
+                f(this.getUTCDate())      + 'T' +
+                f(this.getUTCHours())     + ':' +
+                f(this.getUTCMinutes())   + ':' +
+                f(this.getUTCSeconds())   + 'Z' : null;
+        };
+
+        String.prototype.toJSON      =
+            Number.prototype.toJSON  =
+            Boolean.prototype.toJSON = function (key) {
+                return this.valueOf();
+            };
+    }
+
+    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+        gap,
+        indent,
+        meta = {    // table of character substitutions
+            '\b': '\\b',
+            '\t': '\\t',
+            '\n': '\\n',
+            '\f': '\\f',
+            '\r': '\\r',
+            '"' : '\\"',
+            '\\': '\\\\'
+        },
+        rep;
+
+
+    function quote(string) {
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe escape
+// sequences.
+
+        escapable.lastIndex = 0;
+        return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
+            var c = meta[a];
+            return typeof c === 'string' ? c :
+                '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+        }) + '"' : '"' + string + '"';
+    }
+
+
+    function str(key, holder) {
+
+// Produce a string from holder[key].
+
+        var i,          // The loop counter.
+            k,          // The member key.
+            v,          // The member value.
+            length,
+            mind = gap,
+            partial,
+            value = holder[key];
+
+// If the value has a toJSON method, call it to obtain a replacement value.
+
+        if (value && typeof value === 'object' &&
+                typeof value.toJSON === 'function') {
+            value = value.toJSON(key);
+        }
+
+// If we were called with a replacer function, then call the replacer to
+// obtain a replacement value.
+
+        if (typeof rep === 'function') {
+            value = rep.call(holder, key, value);
+        }
+
+// What happens next depends on the value's type.
+
+        switch (typeof value) {
+        case 'string':
+            return quote(value);
+
+        case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+            return isFinite(value) ? String(value) : 'null';
+
+        case 'boolean':
+        case 'null':
+
+// If the value is a boolean or null, convert it to a string. Note:
+// typeof null does not produce 'null'. The case is included here in
+// the remote chance that this gets fixed someday.
+
+            return String(value);
+
+// If the type is 'object', we might be dealing with an object or an array or
+// null.
+
+        case 'object':
+
+// Due to a specification blunder in ECMAScript, typeof null is 'object',
+// so watch out for that case.
+
+            if (!value) {
+                return 'null';
+            }
+
+// Make an array to hold the partial results of stringifying this object value.
+
+            gap += indent;
+            partial = [];
+
+// Is the value an array?
+
+            if (Object.prototype.toString.apply(value) === '[object Array]') {
+
+// The value is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+                length = value.length;
+                for (i = 0; i < length; i += 1) {
+                    partial[i] = str(i, value) || 'null';
+                }
+
+// Join all of the elements together, separated with commas, and wrap them in
+// brackets.
+
+                v = partial.length === 0 ? '[]' : gap ?
+                    '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
+                    '[' + partial.join(',') + ']';
+                gap = mind;
+                return v;
+            }
+
+// If the replacer is an array, use it to select the members to be stringified.
+
+            if (rep && typeof rep === 'object') {
+                length = rep.length;
+                for (i = 0; i < length; i += 1) {
+                    if (typeof rep[i] === 'string') {
+                        k = rep[i];
+                        v = str(k, value);
+                        if (v) {
+                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
+                        }
+                    }
+                }
+            } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+                for (k in value) {
+                    if (Object.prototype.hasOwnProperty.call(value, k)) {
+                        v = str(k, value);
+                        if (v) {
+                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
+                        }
+                    }
+                }
+            }
+
+// Join all of the member texts together, separated with commas,
+// and wrap them in braces.
+
+            v = partial.length === 0 ? '{}' : gap ?
+                '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
+                '{' + partial.join(',') + '}';
+            gap = mind;
+            return v;
+        }
+    }
+
+// If the JSON object does not yet have a stringify method, give it one.
+
+    if (typeof JSON.stringify !== 'function') {
+        JSON.stringify = function (value, replacer, space) {
+
+// The stringify method takes a value and an optional replacer, and an optional
+// space parameter, and returns a JSON text. The replacer can be a function
+// that can replace values, or an array of strings that will select the keys.
+// A default replacer method can be provided. Use of the space parameter can
+// produce text that is more easily readable.
+
+            var i;
+            gap = '';
+            indent = '';
+
+// If the space parameter is a number, make an indent string containing that
+// many spaces.
+
+            if (typeof space === 'number') {
+                for (i = 0; i < space; i += 1) {
+                    indent += ' ';
+                }
+
+// If the space parameter is a string, it will be used as the indent string.
+
+            } else if (typeof space === 'string') {
+                indent = space;
+            }
+
+// If there is a replacer, it must be a function or an array.
+// Otherwise, throw an error.
+
+            rep = replacer;
+            if (replacer && typeof replacer !== 'function' &&
+                    (typeof replacer !== 'object' ||
+                    typeof replacer.length !== 'number')) {
+                throw new Error('JSON.stringify');
+            }
+
+// Make a fake root object containing our value under the key of ''.
+// Return the result of stringifying the value.
+
+            return str('', {'': value});
+        };
+    }
+
+
+// If the JSON object does not yet have a parse method, give it one.
+
+    if (typeof JSON.parse !== 'function') {
+        JSON.parse = function (text, reviver) {
+
+// The parse method takes a text and an optional reviver function, and returns
+// a JavaScript value if the text is a valid JSON text.
+
+            var j;
+
+            function walk(holder, key) {
+
+// The walk method is used to recursively walk the resulting structure so
+// that modifications can be made.
+
+                var k, v, value = holder[key];
+                if (value && typeof value === 'object') {
+                    for (k in value) {
+                        if (Object.prototype.hasOwnProperty.call(value, k)) {
+                            v = walk(value, k);
+                            if (v !== undefined) {
+                                value[k] = v;
+                            } else {
+                                delete value[k];
+                            }
+                        }
+                    }
+                }
+                return reviver.call(holder, key, value);
+            }
+
+
+// Parsing happens in four stages. In the first stage, we replace certain
+// Unicode characters with escape sequences. JavaScript handles many characters
+// incorrectly, either silently deleting them, or treating them as line endings.
+
+            text = String(text);
+            cx.lastIndex = 0;
+            if (cx.test(text)) {
+                text = text.replace(cx, function (a) {
+                    return '\\u' +
+                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+                });
+            }
+
+// In the second stage, we run the text against regular expressions that look
+// for non-JSON patterns. We are especially concerned with '()' and 'new'
+// because they can cause invocation, and '=' because it can cause mutation.
+// But just to be safe, we want to reject all unexpected forms.
+
+// We split the second stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+// open brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+            if (/^[\],:{}\s]*$/
+                    .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
+                        .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
+                        .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the third stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+                j = eval('(' + text + ')');
+
+// In the optional fourth stage, we recursively walk the new structure, passing
+// each name/value pair to a reviver function for possible transformation.
+
+                return typeof reviver === 'function' ?
+                    walk({'': j}, '') : j;
+            }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+            throw new SyntaxError('JSON.parse');
+        };
+    }
+}());
diff --git a/browserid/static/relay/relay.js b/browserid/static/relay/relay.js
index b5fd0fdec481c83e5facfe38c0e772497696eaa4..4788aa531d8d74f2afa249259ecb9210dcafbc9c 100644
--- a/browserid/static/relay/relay.js
+++ b/browserid/static/relay/relay.js
@@ -39,9 +39,7 @@ window.console = window.console || {
   log: function() {}
 };
 
-steal.plugins()
-
-	.resources('../../dialog/resources/jschannel')
+steal.resources('../../dialog/resources/jschannel')
 
           .then(function($) {
             // XXX get rid of this setTimeout.  It is in so that the build 
diff --git a/browserid/tests/ca-test.js b/browserid/tests/ca-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..a5d5a7e356321772eefa0b08da201425136016b4
--- /dev/null
+++ b/browserid/tests/ca-test.js
@@ -0,0 +1,85 @@
+#!/usr/bin/env node
+
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+require('./lib/test_env.js');
+
+const assert = require('assert'),
+vows = require('vows'),
+start_stop = require('./lib/start-stop.js'),
+wsapi = require('./lib/wsapi.js'),
+email = require('../lib/email.js'),
+ca = require('../lib/ca.js'),
+jwcert = require('../../lib/jwcrypto/jwcert'),
+jwk = require('../../lib/jwcrypto/jwk'),
+jws = require('../../lib/jwcrypto/jws');
+
+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";
+
+// create a new account via the api with (first address)
+suite.addBatch({
+  "certify a public key": {
+    topic: function() {
+      return ca.certify(email_addr, kp.publicKey);
+    },
+    "parses" : function(cert_raw, err) {
+      var cert = ca.parseCert(cert_raw);
+      assert.notEqual(cert, null);
+    },
+    "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([cert_raw], function(pk) {
+        assert.isTrue(kp.publicKey.equals(pk));
+      });
+    }
+  },
+  "certify a chain of keys": {
+  }
+});
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);
diff --git a/browserid/tests/cert-emails-test.js b/browserid/tests/cert-emails-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..0a3241717399f5c0500d574b91d5f644061598b3
--- /dev/null
+++ b/browserid/tests/cert-emails-test.js
@@ -0,0 +1,193 @@
+#!/usr/bin/env node
+
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+require('./lib/test_env.js');
+
+const assert = require('assert'),
+vows = require('vows'),
+start_stop = require('./lib/start-stop.js'),
+wsapi = require('./lib/wsapi.js'),
+email = require('../lib/email.js'),
+ca = require('../lib/ca.js'),
+jwcert = require('../../lib/jwcrypto/jwcert'),
+jwk = require('../../lib/jwcrypto/jwk'),
+jws = require('../../lib/jwcrypto/jws'),
+jwt = require('../../lib/jwcrypto/jwt');
+
+var suite = vows.describe('cert-emails');
+
+// disable vows (often flakey?) async error behavior
+suite.options.error = false;
+
+start_stop.addStartupBatches(suite);
+
+// ever time a new token is sent out, let's update the global
+// var 'token'
+var token = undefined;
+email.setInterceptor(function(email, site, secret) { token = secret; });
+
+// INFO: some of these tests are repeat of sync-emails... to set
+// things up properly for key certification
+
+// create a new account via the api with (first address)
+suite.addBatch({
+  "stage an account": {
+    topic: wsapi.post('/wsapi/stage_user', {
+      email: 'syncer@somehost.com',
+      pass: 'fakepass',
+      pubkey: 'fakekey',
+      site:'fakesite.com'
+    }),
+    "yields a sane token": function(r, err) {
+      assert.strictEqual(typeof token, 'string');
+    }
+  }
+});
+
+suite.addBatch({
+  "verifying account ownership": {
+    topic: function() {
+      wsapi.get('/wsapi/prove_email_ownership', { token: token }).call(this);
+    },
+    "works": function(r, err) {
+      assert.equal(r.code, 200);
+      assert.strictEqual(true, JSON.parse(r.body));
+    }
+  }
+});
+
+suite.addBatch({
+  "calling registration_status after a registration is complete": {
+    topic: wsapi.get("/wsapi/registration_status"),
+    "yields a HTTP 200": function (r, err) {
+      assert.strictEqual(r.code, 200);
+    },
+    "returns a json encoded string - `complete`": function (r, err) {
+      assert.strictEqual(JSON.parse(r.body), "complete");
+    }
+  }
+});
+
+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);
+
+suite.addBatch({
+  "check the public key": {
+    topic: wsapi.get("/pk"),
+    "returns a 200": function(r, err) {
+      assert.strictEqual(r.code, 200);
+    },
+    "returns the right public key": function(r, err) {
+      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(r, err) {
+      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(r, err) {
+      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() }),
+    "returns a response with a proper content-type" : function(r, err) {
+      assert.strictEqual(r.code, 200);
+    },
+    "returns a proper cert": function(r, err) {
+      ca.verifyChain([r.body], function(pk) {
+        assert.isTrue(kp.publicKey.equals(pk));
+      });
+    },
+    "generate an assertion": {
+      topic: function(r) {
+        var serializedCert = r.body.toString();
+        var assertion = new jwt.JWT(null, new Date(), "rp.com");
+        var full_assertion = {
+          certificates: [serializedCert],
+          assertion: assertion.sign(kp.secretKey)
+        };
+
+        return full_assertion;
+      },
+      "full assertion looks good": function(full_assertion) {
+        assert.equal(full_assertion.certificates[0].split(".").length, 3);
+        assert.equal(full_assertion.assertion.split(".").length, 3);
+      },
+      "assertion verifies": {
+        topic: function(full_assertion) {
+          var cb = this.callback;
+          // extract public key at the tail of the chain
+          ca.verifyChain(full_assertion.certificates, function(pk) {
+            if (!pk)
+              cb(false);
+            
+            var assertion = new jwt.JWT();
+            assertion.parse(full_assertion.assertion);
+            cb(assertion.verify(pk));
+          });
+        },
+        "verifies": function(result, err) {
+          assert.isTrue(result);
+        }
+      }
+    }
+  },
+  "cert key invoked proper arguments but incorrect email address": {  
+    topic: wsapi.post(cert_key_url, { email: 'syncer2@somehost.com', pubkey: kp.publicKey.serialize() }),
+    "returns a response with a proper error content-type" : function(r, err) {
+      assert.strictEqual(r.code, 400);
+    }
+  }
+
+
+  // NOTE: db-test has more thorough tests of the algorithm behind the sync_emails API  
+});
+
+start_stop.addShutdownBatches(suite);
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);
diff --git a/browserid/tests/db-test.js b/browserid/tests/db-test.js
index 8d0d7ab49ac5727da86dd5f3ad234e878c6b459d..fb090d52578d00e46b04c5f09ee58e666115ad95 100755
--- a/browserid/tests/db-test.js
+++ b/browserid/tests/db-test.js
@@ -210,7 +210,7 @@ suite.addBatch({
 suite.addBatch({
   "staging an email": {
     topic: function() {
-      db.stageEmail('lloyd@nowhe.re', 'lloyd@somewhe.re', 'fakepubkey4', this.callback);
+      db.stageEmail('lloyd@nowhe.re', 'lloyd@somewhe.re', this.callback);
     },
     "yields a valid secret": function(secret) {
       assert.isString(secret);
@@ -264,6 +264,7 @@ suite.addBatch({
   }
 });
 
+/*
 // exports.getSyncResponse
 suite.addBatch({
   "sync responses": {  
@@ -361,6 +362,7 @@ suite.addBatch({
     }
   }
 });
+*/
 
 suite.addBatch({
   "removing an existing email": {
diff --git a/browserid/tests/forgotten-email-test.js b/browserid/tests/forgotten-email-test.js
index 1b4187f71251b7ecad44bd091ef474781d3c20f8..b4c3200ea20bb3589751e07fac81748eeb524ef6 100755
--- a/browserid/tests/forgotten-email-test.js
+++ b/browserid/tests/forgotten-email-test.js
@@ -97,14 +97,13 @@ suite.addBatch({
   "add a new email address to our account": {
     topic: wsapi.post('/wsapi/add_email', {
       email: 'second@fakeemail.com',
-      pubkey: 'fakepubkey',
       site:'fakesite.com'
     }),
     "the token is sane": function(r, err) {
       assert.strictEqual('string', typeof token);
     }
   },
-  "set the key again": {
+/*  "set the key again": {
     topic: wsapi.post('/wsapi/set_key', {
       email: 'second@fakeemail.com',
       pubkey: 'fakepubkey2'
@@ -112,7 +111,7 @@ suite.addBatch({
     "the token is sane": function(r, err) {
       assert.strictEqual('string', typeof token);
     }
-  }
+  }*/
 });
 
 // confirm second email email address to the account
diff --git a/browserid/tests/set-key-wsapi-test.js b/browserid/tests/set-key-wsapi-test.js
index b04579c6860ad7f9d129121b711c07def98245cc..f154870db36497117af6731615b57adabdd89582 100755
--- a/browserid/tests/set-key-wsapi-test.js
+++ b/browserid/tests/set-key-wsapi-test.js
@@ -94,13 +94,14 @@ suite.addBatch({
   }
 });
 
+/*
 suite.addBatch({
   "setting a key that is already set": {
     topic: wsapi.post('/wsapi/set_key', {
       email: 'setkeyabuser@somehost.com',
       pubkey: 'fakekey'
     }),
-    "fails with a false return0" : function(r, err) {
+    "fails with a false return" : function(r, err) {
       assert.strictEqual(r.code, 200);
       assert.strictEqual(r.body, 'false');
     }
@@ -115,6 +116,7 @@ suite.addBatch({
     }
   }
 });
+*/
 
 start_stop.addShutdownBatches(suite);
 
diff --git a/browserid/tests/sync-emails-wsapi-test.js b/browserid/tests/sync-emails-wsapi-test.js
index c3651df59ac8dd879d2d8ee6230c4cadd0828805..1375a5cb990604d5ae335a11099ec10e96122109 100755
--- a/browserid/tests/sync-emails-wsapi-test.js
+++ b/browserid/tests/sync-emails-wsapi-test.js
@@ -61,7 +61,6 @@ suite.addBatch({
     topic: wsapi.post('/wsapi/stage_user', {
       email: 'syncer@somehost.com',
       pass: 'fakepass',
-      pubkey: 'fakekey',
       site:'fakesite.com'
     }),
     "yields a sane token": function(r, err) {
@@ -94,6 +93,21 @@ suite.addBatch({
   }
 });
 
+suite.addBatch({
+  "list emails API": {
+    topic: wsapi.get('/wsapi/list_emails', {}),
+    "succeeds with HTTP 200" : function(r, err) {
+      assert.strictEqual(r.code, 200);
+    },
+    "returns an object with proper email": function(r, err) {
+      var emails = Object.keys(JSON.parse(r.body));
+      assert.equal(emails[0], "syncer@somehost.com");
+      assert.equal(emails.length, 1);
+    }
+  }
+});
+
+/*
 suite.addBatch({
   "the sync emails API invoked without a proper argument": {
     topic: wsapi.post('/wsapi/sync_emails', {}),
@@ -105,7 +119,7 @@ suite.addBatch({
     topic: wsapi.post('/wsapi/sync_emails', { emails: '{}' }),
     "returns a response with a proper content-type" : function(r, err) {
       assert.strictEqual(r.code, 200);
-      assert.strictEqual(r.headers['content-type'], 'application/json; charset=utf-8');
+      //assert.strictEqual(r.headers['content-type'], 'application/json; charset=utf-8');
     }
   },
   "the sync emails API invoked without a empty emails argument": {  
@@ -148,6 +162,7 @@ suite.addBatch({
   }
   // NOTE: db-test has more thorough tests of the algorithm behind the sync_emails API
 });
+*/
 
 start_stop.addShutdownBatches(suite);
 
diff --git a/browserid/views/dialog.ejs b/browserid/views/dialog.ejs
index 3a8a1882243fa1970d38244365ce5506c7a3ba12..769a2dc367addd9e3dad69e36c76313ff9124b71 100644
--- a/browserid/views/dialog.ejs
+++ b/browserid/views/dialog.ejs
@@ -2,11 +2,12 @@
 <html>
 <head>
   <!--[if lt IE 9]>
-    <script src="/dialog/html5shim.js"></script>
+    <script src="/js/html5shim.js"></script>
   <![endif]-->
   <title>Browser ID</title>
 </head>
   <body>
 	</body>
 </html>
+<script type='text/javascript' src='/vepbundle'></script>
 <script type='text/javascript' src='steal/steal<%= production ? ".production" : "" %>.js?dialog'></script>
diff --git a/browserid/views/layout.ejs b/browserid/views/layout.ejs
index 6606c515558a44a2ffddf1550bb53edead1951c0..228690b85affbcf25ae4aa464edef3043003444c 100644
--- a/browserid/views/layout.ejs
+++ b/browserid/views/layout.ejs
@@ -1,18 +1,23 @@
 <!DOCTYPE html>
 <html>
 <head>
-  <meta charset='utf-8'>
+  <meta charset="utf-8">
   <meta name="viewport" content="initial-scale=1.0; maximum-scale=1.0; width=device-width;">
   <title><%- title %></title>
+  <!--[if lt IE 9]>
+    <script src="/js/html5shim.js"></script>
+  <![endif]-->
 
+  <script src="/vepbundle" type="text/javascript"></script>
   <% if (production) { %>
     <link rel="stylesheet" type="text/css" href="/css/browserid.min.css">
     <script src="/js/lib.min.js" type="text/javascript"></script>
   <% } else { %>
-    <link href='http://fonts.googleapis.com/css?family=Droid+Serif:400,400italic,700,700italic' rel='stylesheet' type='text/css'>
+    <link href='//fonts.googleapis.com/css?family=Droid+Serif:400,400italic,700,700italic' rel='stylesheet' type='text/css'>
     <link rel="stylesheet" href="/css/style.css" type="text/css" media="screen">
 
     <script src="/js/jquery-1.6.2.min.js" type="text/javascript"></script>
+    <script src="/js/json2.js" type="text/javascript"></script>
     <script src="/dialog/resources/storage.js" type="text/javascript"></script>
     <script src="/dialog/resources/crypto-api.js" type="text/javascript"></script>
     <script src="/dialog/resources/crypto.js" type="text/javascript"></script>
diff --git a/browserid/views/relay.ejs b/browserid/views/relay.ejs
index f743bf73729326c4aabc4c5014776012cd654562..203d0c6c7854cd84ee1e0b430e214661882adffd 100644
--- a/browserid/views/relay.ejs
+++ b/browserid/views/relay.ejs
@@ -1,12 +1,44 @@
 <!doctype html>
 <html>
 <head>
-	<head>
-		<title>Browser ID</title>
-	</head>
-    <body>
+  <meta charset="utf-8"> 
+  <title>Browser ID</title>
+</head>
+  <body>
       Relay iframe.  Woohoo!
+  <script type="text/javascript" src="https://browserid.org/dialog/resources/jschannel.js"></script>
+  <script type="text/javascript">
+    var ipServer = "https://browserid.org";
+
+    var chan = Channel.build( {
+      window: window.parent,
+      origin: "*",
+      scope: "mozid"
+    } );
+
+    var transaction;
+
+    chan.bind("getVerifiedEmail", function(trans, s) {
+      trans.delayReturn(true);
+
+      transaction = trans;
+    });
+
+    window.browserid_relay = function(status, error) {
+        if(error) {
+          errorOut(transaction, error);
+        }
+        else {
+          try {
+            transaction.complete(status);
+          } catch(e) {
+            // The relay function is called a second time after the 
+            // initial success, when the window is closing.
+          }
+        }
+    }
+
+  </script>
+  <!--script type='text/javascript' src='https://browserid.org/steal/steal<%= production ? ".production" : "" %>.js?relay'></script-->
 	</body>
-  <script type='text/javascript' src='/steal/steal<%= production ? ".production"
-  : "" %>.js?relay'></script>
 </html>
diff --git a/lib/jwcrypto b/lib/jwcrypto
new file mode 160000
index 0000000000000000000000000000000000000000..eaaebbce8a32d309200f6d79c405d525b549dd68
--- /dev/null
+++ b/lib/jwcrypto
@@ -0,0 +1 @@
+Subproject commit eaaebbce8a32d309200f6d79c405d525b549dd68
diff --git a/libs/secrets.js b/libs/secrets.js
index b57bf0ea3a0e57fcba6487ec13e118348bec6336..73350d2e39f6eafdf958d3fb022bd512a6e4a54b 100644
--- a/libs/secrets.js
+++ b/libs/secrets.js
@@ -35,7 +35,9 @@
 
 const
 path = require('path'),
-fs = require('fs');
+fs = require('fs'),
+jwk = require('../lib/jwcrypto/jwk'),
+configuration = require("./configuration");
 
 exports.generate = function(chars) {
   var str = "";
@@ -59,3 +61,38 @@ exports.hydrateSecret = function(name, dir) {
   }
   return secret;
 };
+
+function loadSecretKey(name, dir) {
+  var p = path.join(dir, name + ".secretkey");
+  var fileExists = false;
+  var secret = undefined;
+
+  try{ secret = fs.readFileSync(p).toString(); } catch(e) {};
+
+  if (secret === undefined) {
+    return null;
+  }
+
+  // parse it
+  return jwk.SecretKey.deserialize(secret);
+}
+
+function loadPublicKey(name, dir) {
+  var p = path.join(dir, name + ".publickey");
+  var fileExists = false;
+  var secret = undefined;
+
+  try{ secret = fs.readFileSync(p).toString(); } catch(e) {};
+
+  if (secret === undefined) {
+    return null;
+  }
+
+  // parse it
+  // it should be a JSON structure with alg and serialized key
+  // {alg: <ALG>, value: <SERIALIZED_KEY>}
+  return jwk.PublicKey.deserialize(secret);
+}
+
+exports.SECRET_KEY = loadSecretKey('root', configuration.get('var_path'));
+exports.PUBLIC_KEY = loadPublicKey('root', configuration.get('var_path'));
diff --git a/run.js b/run.js
index a801931299ec64f1b7cfb05010d1cc411bf95da3..0dcb383d4746d488dc9712dd05d41a9b60253864 100755
--- a/run.js
+++ b/run.js
@@ -51,7 +51,7 @@ require('./libs/logging.js').enableConsoleLogging();
 var configuration = require('./libs/configuration.js');
 
 
-var PRIMARY_HOST = "127.0.0.1";
+var PRIMARY_HOST = process.env.IP_ADDRESS || "127.0.0.1";
 
 var boundServers = [ ];
 
diff --git a/verifier/app.js b/verifier/app.js
index 19c7ed9cd946c4361019ecfb7b4a493ad99e44a1..a04d74fb008d43942477ee623637e0e53b79ecb8 100644
--- a/verifier/app.js
+++ b/verifier/app.js
@@ -39,25 +39,27 @@ const   path = require('path'),
           fs = require('fs'),
    httputils = require('./lib/httputils.js'),
  idassertion = require('./lib/idassertion.js'),
-         jwt = require('./lib/jwt.js'),
+certassertion = require('./lib/certassertion.js'),
      express = require('express'),
      metrics = require('../libs/metrics.js'),
      logger = require('../libs/logging.js').logger;
 
 logger.info("verifier server starting up");
 
+// updating this call for certs now (Ben - 2011-09-06)
+// assertion is the single assertion of email
+// audience is the intended audience
+// certificates is the list of chained certificates, CSV-style
 function doVerify(req, resp, next) {
   req.body = req.body || {}
+  
   var assertion = (req.query && req.query.assertion) ? req.query.assertion : req.body.assertion;
   var audience = (req.query && req.query.audience) ? req.query.audience : req.body.audience;
 
   if (!(assertion && audience))
     return resp.json({ status: "failure", reason: "need assertion and audience" });
 
-  // allow client side XHR to access this WSAPI, see
-  // https://developer.mozilla.org/en/http_access_control
-  // for details
-  // FIXME: should we really allow this? It might encourage the wrong behavior
+  // FIXME: remove this eventually
   resp.setHeader('Access-Control-Allow-Origin', '*');
   if (req.method === 'OPTIONS') {
     resp.setHeader('Access-Control-Allow-Methods', 'POST, GET');
@@ -66,6 +68,32 @@ function doVerify(req, resp, next) {
     return;
   }
 
+  certassertion.verify(
+    assertion, audience,
+    function(email, audience, expires) {
+      resp.json({
+        status : "okay",
+        email : email,
+        audience : audience,
+        expires : expires
+      });
+      
+      metrics.report('verify', {
+        result: 'success',
+        rp: audience
+      });
+    },
+    function(error) {
+      resp.json({"status":"failure", reason: error.toString()});
+      metrics.report('verify', {
+        result: 'failure',
+        rp: audience
+      });
+    });
+  
+  // old verification code. Still here in case we want to enable backwards compatibility
+  // for some period of time (doubt it.)
+  /*
   try {
     var assertionObj = new idassertion.IDAssertion(assertion);
     assertionObj
@@ -103,7 +131,7 @@ function doVerify(req, resp, next) {
       rp: audience
     });
     resp.json({ status: "failure", reason: e.toString() });
-  }
+  } */
 }
 
 exports.setup = function(app) {
diff --git a/verifier/lib/certassertion.js b/verifier/lib/certassertion.js
new file mode 100644
index 0000000000000000000000000000000000000000..bb8d2569c5625dd8abfff3e40836c1f9f221c39c
--- /dev/null
+++ b/verifier/lib/certassertion.js
@@ -0,0 +1,177 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *      Ben Adida <benadida@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+//
+// rewritten idassertion for certificates
+
+const xml2js = require("xml2js/lib/xml2js"),
+http = require("http"),
+https = require("https"),
+url = require("url"),
+jwk = require("../../lib/jwcrypto/jwk"),
+jwt = require("../../lib/jwcrypto/jwt"),
+jwcert = require("../../lib/jwcrypto/jwcert"),
+vep = require("../../lib/jwcrypto/vep"),
+configuration = require('../../libs/configuration'),
+secrets = require('../../libs/secrets'),
+logger = require("../../libs/logging.js").logger;
+
+// configuration information to check the issuer
+const config = require("../../libs/configuration.js");
+
+const HOSTMETA_URL = "/.well-known/host-meta";
+
+var publicKeys = {};
+
+// set up some default public keys
+publicKeys[configuration.get('hostname')] = secrets.PUBLIC_KEY;
+
+// FIXME: hard-wired key for mozilla.com
+publicKeys['mozilla.com'] = jwk.PublicKey.fromSimpleObject({"algorithm":"RS","value":"-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIkB8pmT0Zf2gLW5oplYL22vjj6UIuXx\n9CfosFDy8DYTOVA6Z0wBBfQyaUcAdcQ4BzkV2zb7ik+f4WKdch2nwWkCAwEAAQ==\n-----END PUBLIC KEY-----\n"});
+
+function https_complete_get(host, url, successCB, errorCB) {
+  https.get({host: host,path: url}, function(res) {
+    var allData = "";
+    res.on('data', function(d) {
+      allData += d;
+    });
+
+    res.on('end', function() {
+      successCB(allData);
+    });
+    
+  }).on('error', function(e) {
+    console.log(e.toString());
+    errorCB(e);
+  });
+}
+
+// only over SSL
+function retrieveHostPublicKey(host, successCB, errorCB) {
+  // cached?
+  var cached = publicKeys[host];
+  if (cached)
+    return successCB(cached);
+  
+  https_complete_get(host, HOSTMETA_URL, function(hostmeta) {
+    // find the location of the public key
+    var parser = new xml2js.Parser();
+
+    parser.addListener('end', function(parsedDoc) {
+      // FIXME do we need to check hm:Host?
+
+      var pk_location = null;
+      
+      // get the public key location
+      var links = parsedDoc["Link"];
+      if (links instanceof Array) {
+        for (var i in links) {
+          var link = links[i];
+          var rel = link["@"]["rel"];
+          if (rel) {
+            if (rel.toLowerCase() == "https://browserid.org/vocab#publicKey") {
+              pk_location = link["@"]["href"];
+              break;
+            }
+          }
+        }
+      }
+
+      // if we don't have a pk
+      if (!pk_location)
+        return errorCB("no public key in host-meta");
+
+      // go fetch the public key
+      https_complete_get(host, pk_location, function(raw_pk) {
+        // parse the key
+        var pk = jwk.PublicKey.deserialize(raw_pk);
+
+        // cache it
+        publicKeys[host] = pk;
+        
+        return successCB(pk);
+      });
+    });
+    
+    parser.parseString(hostmeta);
+  }, errorCB);
+}
+
+// verify the tuple certList, assertion, audience
+//
+// assertion is a bundle of the underlying assertion and the cert list
+// audience is a web origin, e.g. https://foo.com or http://foo.org:81
+//
+// pkRetriever should be sent in only by code that really understands
+// what it's doing, e.g. testing code.
+function verify(assertion, audience, successCB, errorCB, pkRetriever) {
+  // assertion is bundle
+  var bundle = vep.unbundleCertsAndAssertion(assertion);
+
+  var theIssuer;
+  jwcert.JWCert.verifyChain(bundle.certificates, function(issuer, next) {
+    theIssuer = issuer;
+    // allow other retrievers for testing
+    if (pkRetriever)
+      pkRetriever(issuer, next);
+    else
+      retrieveHostPublicKey(issuer, next, function(err) {next(null);});
+  }, function(pk, principal) {
+    // primary?
+    if (theIssuer != configuration.get('hostname')) {
+      // then the email better match the issuer
+      if (!principal.email.match("@" + theIssuer + "$"))
+        return errorCB();
+    }
+
+    var tok = new jwt.JWT();
+    tok.parse(bundle.assertion);
+
+    // audience must match!
+    if (tok.audience != audience)
+      return errorCB();
+    
+    if (tok.verify(pk)) {
+      successCB(principal.email, tok.audience, tok.expires);
+    } else {
+      errorCB();
+    }
+  }, errorCB);
+}
+  
+
+exports.retrieveHostPublicKey = retrieveHostPublicKey;
+exports.verify = verify;
\ No newline at end of file
diff --git a/verifier/test/certassertion-test.js b/verifier/test/certassertion-test.js
new file mode 100644
index 0000000000000000000000000000000000000000..87d111051cbc0dde56e940fa2eca1ed7ca732426
--- /dev/null
+++ b/verifier/test/certassertion-test.js
@@ -0,0 +1,76 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *     Ben Adida <benadida@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var vows = require("vows"),
+    assert = require("assert"),
+    certassertion = require("../lib/certassertion"),
+    jwk = require("../../lib/jwcrypto/jwk"),
+    jwt = require("../../lib/jwcrypto/jwt"),
+    jwcert = require("../../lib/jwcrypto/jwcert"),
+    vep = require("../../lib/jwcrypto/vep"),
+    events = require("events");
+
+vows.describe('certassertion').addBatch({
+  "generate and certify key + assertion" : {
+    topic: function() {
+      // generate a key
+      var root_kp = jwk.KeyPair.generate("RS", 64);
+      var user_kp = jwk.KeyPair.generate("RS", 64);
+      var cert = new jwcert.JWCert("fakeroot.com", new Date(), user_kp.publicKey, {email:"user@fakeroot.com"}).sign(root_kp.secretKey);
+      var assertion = new jwt.JWT(null, new Date(), "rp.com").sign(user_kp.secretKey);
+
+      var self = this;
+      var bundle = vep.bundleCertsAndAssertion([cert],assertion);
+      
+      // verify it
+      certassertion.verify(
+        bundle, "rp.com",
+        function(email, audience, expires) {
+          self.callback({email:email, audience: audience, expires:expires});
+        },
+        function(msg) {},
+        function(issuer, next) {
+          if (issuer == "fakeroot.com")
+            next(root_kp.publicKey);
+          else
+            next(null);
+        });
+    },
+    "is successful": function(res, err) {
+      assert.notEqual(res.email, null);
+    }
+  }
+}).export(module);
\ No newline at end of file