Bug 975144 - Rework RTC identity to use JS sandbox, r=jib
authorMartin Thomson <martin.thomson@gmail.com>
Sun, 22 Feb 2015 10:57:20 +1300
changeset 230196 c516431e7ad73785a946986baa439a1c33d1907a
parent 230195 8fc35500a37b8b6ca2c2f4e62fd1ff645f80fa89
child 230197 1b46f2423156e2aa4041e40c2a50a4b20a63f7a4
push id28311
push userphilringnalda@gmail.com
push dateSun, 22 Feb 2015 18:06:18 +0000
treeherdermozilla-central@d1f1624d615c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjib
bugs975144
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 975144 - Rework RTC identity to use JS sandbox, r=jib
dom/media/IdpProxy.jsm
dom/media/IdpSandbox.jsm
dom/media/PeerConnection.js
dom/media/PeerConnectionIdp.jsm
dom/media/moz.build
deleted file mode 100644
--- a/dom/media/IdpProxy.jsm
+++ /dev/null
@@ -1,272 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["IdpProxy"];
-
-const {
-  classes: Cc,
-  interfaces: Ci,
-  utils: Cu,
-  results: Cr
-} = Components;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Sandbox",
-                                  "resource://gre/modules/identity/Sandbox.jsm");
-
-/**
- * An invisible iframe for hosting the idp shim.
- *
- * There is no visible UX here, as we assume the user has already
- * logged in elsewhere (on a different screen in the web site hosting
- * the RTC functions).
- */
-function IdpChannel(uri, messageCallback) {
-  this.sandbox = null;
-  this.messagechannel = null;
-  this.source = uri;
-  this.messageCallback = messageCallback;
-}
-
-IdpChannel.prototype = {
-  /**
-   * Create a hidden, sandboxed iframe for hosting the IdP's js shim.
-   *
-   * @param callback
-   *                (function) invoked when this completes, with an error
-   *                argument if there is a problem, no argument if everything is
-   *                ok
-   */
-  open: function(callback) {
-    if (this.sandbox) {
-      return callback(new Error("IdP channel already open"));
-    }
-
-    let ready = this._sandboxReady.bind(this, callback);
-    this.sandbox = new Sandbox(this.source, ready);
-  },
-
-  _sandboxReady: function(aCallback, aSandbox) {
-    // Inject a message channel into the subframe.
-    try {
-      this.messagechannel = new aSandbox._frame.contentWindow.MessageChannel();
-      Object.defineProperty(
-        aSandbox._frame.contentWindow.wrappedJSObject,
-        "rtcwebIdentityPort",
-        {
-          value: this.messagechannel.port2,
-          configurable: true
-        }
-      );
-    } catch (e) {
-      this.close();
-      aCallback(e); // oops, the IdP proxy overwrote this.. bad
-      return;
-    }
-    this.messagechannel.port1.onmessage = function(msg) {
-      this.messageCallback(msg.data);
-    }.bind(this);
-    this.messagechannel.port1.start();
-    aCallback();
-  },
-
-  send: function(msg) {
-    this.messagechannel.port1.postMessage(msg);
-  },
-
-  close: function IdpChannel_close() {
-    if (this.sandbox) {
-      if (this.messagechannel) {
-        this.messagechannel.port1.close();
-      }
-      this.sandbox.free();
-    }
-    this.messagechannel = null;
-    this.sandbox = null;
-  }
-};
-
-/**
- * A message channel between the RTC PeerConnection and a designated IdP Proxy.
- *
- * @param domain (string) the domain to load up
- * @param protocol (string) Optional string for the IdP protocol
- */
-function IdpProxy(domain, protocol) {
-  IdpProxy.validateDomain(domain);
-  IdpProxy.validateProtocol(protocol);
-
-  this.domain = domain;
-  this.protocol = protocol || "default";
-
-  this._reset();
-}
-
-/**
- * Checks that the domain is only a domain, and doesn't contain anything else.
- * Adds it to a URI, then checks that it matches perfectly.
- */
-IdpProxy.validateDomain = function(domain) {
-  let message = "Invalid domain for identity provider; ";
-  if (!domain || typeof domain !== "string") {
-    throw new Error(message + "must be a non-zero length string");
-  }
-
-  message += "must only have a domain name and optionally a port";
-  try {
-    let ioService = Components.classes["@mozilla.org/network/io-service;1"]
-                    .getService(Components.interfaces.nsIIOService);
-    let uri = ioService.newURI('https://' + domain + '/', null, null);
-
-    // this should trap errors
-    // we could check uri.userPass, uri.path and uri.ref, but there is no need
-    if (uri.hostPort !== domain) {
-      throw new Error(message);
-    }
-  } catch (e if (e.result === Cr.NS_ERROR_MALFORMED_URI)) {
-    throw new Error(message);
-  }
-};
-
-/**
- * Checks that the IdP protocol is sane.  In particular, we don't want someone
- * adding relative paths (e.g., "../../myuri"), which could be used to move
- * outside of /.well-known/ and into space that they control.
- */
-IdpProxy.validateProtocol = function(protocol) {
-  if (!protocol) {
-    return;  // falsy values turn into "default", so they are OK
-  }
-  let message = "Invalid protocol for identity provider; ";
-  if (typeof protocol !== "string") {
-    throw new Error(message + "must be a string");
-  }
-  if (decodeURIComponent(protocol).match(/[\/\\]/)) {
-    throw new Error(message + "must not include '/' or '\\'");
-  }
-};
-
-IdpProxy.prototype = {
-  _reset: function() {
-    this.channel = null;
-    this.ready = false;
-
-    this.counter = 0;
-    this.tracking = {};
-    this.pending = [];
-  },
-
-  isSame: function(domain, protocol) {
-    return this.domain === domain && ((protocol || "default") === this.protocol);
-  },
-
-  /**
-   * Get a sandboxed iframe for hosting the idp-proxy's js. Create a message
-   * channel down to the frame.
-   *
-   * @param errorCallback (function) a callback that will be invoked if there
-   *                is a fatal error starting the proxy
-   */
-  start: function(errorCallback) {
-    if (this.channel) {
-      return;
-    }
-    let well_known = "https://" + this.domain;
-    well_known += "/.well-known/idp-proxy/" + this.protocol;
-    this.channel = new IdpChannel(well_known, this._messageReceived.bind(this));
-    this.channel.open(function(error) {
-      if (error) {
-        this.close();
-        if (typeof errorCallback === "function") {
-          errorCallback(error);
-        }
-      }
-    }.bind(this));
-  },
-
-  /**
-   * Send a message up to the idp proxy. This should be an RTC "SIGN" or
-   * "VERIFY" message. This method adds the tracking 'id' parameter
-   * automatically to the message so that the callback is only invoked for the
-   * response to the message.
-   *
-   * This enqueues the message to send if the IdP hasn't signaled that it is
-   * "READY", and sends the message when it is.
-   *
-   * The caller is responsible for ensuring that a response is received. If the
-   * IdP doesn't respond, the callback simply isn't invoked.
-   */
-  send: function(message, callback) {
-    this.start();
-    if (this.ready) {
-      message.id = "" + (++this.counter);
-      this.tracking[message.id] = callback;
-      this.channel.send(message);
-    } else {
-      this.pending.push({ message: message, callback: callback });
-    }
-  },
-
-  /**
-   * Handle a message from the IdP. This automatically sends if the message is
-   * 'READY' so there is no need to track readiness state outside of this obj.
-   */
-  _messageReceived: function(message) {
-    if (!message) {
-      return;
-    }
-    if (!this.ready && message.type === "READY") {
-      this.ready = true;
-      this.pending.forEach(function(p) {
-        this.send(p.message, p.callback);
-      }, this);
-      this.pending = [];
-    } else if (this.tracking[message.id]) {
-      var callback = this.tracking[message.id];
-      delete this.tracking[message.id];
-      callback(message);
-    } else {
-      let console = Cc["@mozilla.org/consoleservice;1"].
-        getService(Ci.nsIConsoleService);
-      console.logStringMessage("Received bad message from IdP: " +
-                               message.id + ":" + message.type);
-    }
-  },
-
-  /**
-   * Performs cleanup.  The object should be OK to use again.
-   */
-  close: function() {
-    if (!this.channel) {
-      return;
-    }
-
-    // clear out before letting others know in case they do something bad
-    let trackingCopy = this.tracking;
-    let pendingCopy = this.pending;
-
-    this.channel.close();
-    this._reset();
-
-    // dump a message of type "ERROR" in response to all outstanding
-    // messages to the IdP
-    let error = { type: "ERROR", error: "IdP closed" };
-    Object.keys(trackingCopy).forEach(function(k) {
-      trackingCopy[k](error);
-    });
-    pendingCopy.forEach(function(p) {
-      p.callback(error);
-    });
-  },
-
-  toString: function() {
-    return this.domain + '/.../' + this.protocol;
-  }
-};
-
-this.IdpProxy = IdpProxy;
new file mode 100644
--- /dev/null
+++ b/dom/media/IdpSandbox.jsm
@@ -0,0 +1,238 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+
+const {
+  classes: Cc,
+  interfaces: Ci,
+  utils: Cu,
+  results: Cr
+} = Components;
+
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+/** This little class ensures that redirects maintain an https:// origin */
+function RedirectHttpsOnly() {}
+
+RedirectHttpsOnly.prototype = {
+  asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
+    if (newChannel.URI.scheme !== 'https') {
+      callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT);
+    } else {
+      callback.onRedirectVerifyCallback(Cr.NS_OK);
+    }
+  },
+
+  getInterface: function(iid) {
+    return this.QueryInterface(iid);
+  },
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink])
+};
+
+/** This class loads a resource into a single string. ResourceLoader.load() is
+ * the entry point. */
+function ResourceLoader(res, rej) {
+  this.resolve = res;
+  this.reject = rej;
+  this.data = '';
+}
+
+/** Loads the identified https:// URL.  */
+ResourceLoader.load = function(uri) {
+  return new Promise((resolve, reject) => {
+    let listener = new ResourceLoader(resolve, reject);
+    let ioService = Cc['@mozilla.org/network/io-service;1']
+      .getService(Ci.nsIIOService);
+    let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+    // the '2' identifies this as a script load
+    let ioChannel = ioService.newChannelFromURI2(uri, null, systemPrincipal,
+                                                 systemPrincipal, 0, 2);
+    ioChannel.notificationCallbacks = new RedirectHttpsOnly();
+    ioChannel.asyncOpen(listener, null);
+  });
+};
+
+ResourceLoader.prototype = {
+  onDataAvailable: function(request, context, input, offset, count) {
+    let stream = Cc['@mozilla.org/scriptableinputstream;1']
+      .createInstance(Ci.nsIScriptableInputStream);
+    stream.init(input);
+    this.data += stream.read(count);
+  },
+
+  onStartRequest: function (request, context) {},
+
+  onStopRequest: function(request, context, status) {
+    if (Components.isSuccessCode(status)) {
+      var statusCode = request.QueryInterface(Ci.nsIHttpChannel).responseStatus;
+      if (statusCode === 200) {
+        this.resolve({ request: request, data: this.data });
+      } else {
+        this.reject(new Error('Non-200 response from server: ' + statusCode));
+      }
+    } else {
+      this.reject(new Error('Load failed: ' + status));
+    }
+  },
+
+  getInterface: function(iid) {
+    return this.QueryInterface(iid);
+  },
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener])
+};
+
+/**
+ * A simple implementation of the WorkerLocation interface.
+ */
+function createLocationFromURI(uri) {
+  return {
+    href: uri.spec,
+    protocol: uri.scheme + ':',
+    host: uri.host + ((uri.port >= 0) ?
+                      (':' + uri.port) : ''),
+    port: uri.port,
+    hostname: uri.host,
+    pathname: uri.path.replace(/[#\?].*/, ''),
+    search: uri.path.replace(/^[^\?]*/, '').replace(/#.*/, ''),
+    hash: uri.hasRef ? ('#' + uri.ref) : '',
+    origin: uri.prePath,
+    toString: function() {
+      return uri.spec;
+    }
+  };
+}
+
+/**
+ * A javascript sandbox for running an IdP.
+ *
+ * @param domain (string) the domain of the IdP
+ * @param protocol (string?) the protocol of the IdP [default: 'default']
+ * @throws if the domain or protocol aren't valid
+ */
+function IdpSandbox(domain, protocol) {
+  this.source = IdpSandbox.createIdpUri(domain, protocol || "default");
+  this.active = null;
+  this.sandbox = null;
+}
+
+IdpSandbox.checkDomain = function(domain) {
+  if (!domain || typeof domain !== 'string') {
+    throw new Error('Invalid domain for identity provider: ' +
+                    'must be a non-zero length string');
+  }
+};
+
+/**
+ * Checks that the IdP protocol is superficially sane.  In particular, we don't
+ * want someone adding relative paths (e.g., '../../myuri'), which could be used
+ * to move outside of /.well-known/ and into space that they control.
+ */
+IdpSandbox.checkProtocol = function(protocol) {
+  let message = 'Invalid protocol for identity provider: ';
+  if (!protocol || typeof protocol !== 'string') {
+    throw new Error(message + 'must be a non-zero length string');
+  }
+  if (decodeURIComponent(protocol).match(/[\/\\]/)) {
+    throw new Error(message + "must not include '/' or '\\'");
+  }
+};
+
+/**
+ * Turns a domain and protocol into a URI.  This does some aggressive checking
+ * to make sure that we aren't being fooled somehow.  Throws on fooling.
+ */
+IdpSandbox.createIdpUri = function(domain, protocol) {
+  IdpSandbox.checkDomain(domain);
+  IdpSandbox.checkProtocol(protocol);
+
+  let message = 'Invalid IdP parameters: ';
+  try {
+    let wkIdp = 'https://' + domain + '/.well-known/idp-proxy/' + protocol;
+    let ioService = Components.classes['@mozilla.org/network/io-service;1']
+                    .getService(Ci.nsIIOService);
+    let uri = ioService.newURI(wkIdp, null, null);
+
+    if (uri.hostPort !== domain) {
+      throw new Error(message + 'domain is invalid');
+    }
+    if (uri.path.indexOf('/.well-known/idp-proxy/') !== 0) {
+      throw new Error(message + 'must produce a /.well-known/idp-proxy/ URI');
+    }
+
+    return uri;
+  } catch (e if (typeof e.result !== 'undefined' &&
+                 e.result === Cr.NS_ERROR_MALFORMED_URI)) {
+    throw new Error(message + 'must produce a valid URI');
+  }
+};
+
+IdpSandbox.prototype = {
+  isSame: function(domain, protocol) {
+    return this.source.spec === IdpSandbox.createIdpUri(domain, protocol).spec;
+  },
+
+  start: function() {
+    if (!this.active) {
+      this.active = ResourceLoader.load(this.source)
+        .then(result => this._createSandbox(result));
+    }
+    return this.active;
+  },
+
+  // Provides the sandbox with some useful facilities.  Initially, this is only
+  // a minimal set; it is far easier to add more as the need arises, than to
+  // take them back if we discover a mistake.
+  _populateSandbox: function() {
+    this.sandbox.location = Cu.cloneInto(createLocationFromURI(this.source),
+                                         this.sandbox,
+                                         { cloneFunctions: true });
+  },
+
+  _createSandbox: function(result) {
+    let principal = Services.scriptSecurityManager
+      .getChannelResultPrincipal(result.request);
+
+    this.sandbox = Cu.Sandbox(principal, {
+      sandboxName: 'IdP-' + this.source.host,
+      wantComponents: false,
+      wantExportHelpers: false,
+      wantGlobalProperties: [
+        'indexedDB', 'XMLHttpRequest', 'TextEncoder', 'TextDecoder',
+        'URL', 'URLSearchParams', 'atob', 'btoa', 'Blob', 'crypto',
+        'rtcIdentityProvider'
+      ]
+    });
+    this._populateSandbox();
+
+    let registrar = this.sandbox.rtcIdentityProvider;
+    if (!Cu.isXrayWrapper(registrar)) {
+      throw new Error('IdP setup failed');
+    }
+    // putting a javascript version of 1.8 here seems fragile
+    Cu.evalInSandbox(result.data, this.sandbox,
+                     '1.8', result.request.URI.spec, 1);
+
+    if (!registrar.idp) {
+      throw new Error('IdP failed to call rtcIdentityProvider.register()');
+    }
+    return registrar;
+  },
+
+  stop: function() {
+    if (this.sandbox) {
+      Cu.nukeSandbox(this.sandbox);
+    }
+    this.sandbox = null;
+    this.active = null;
+  },
+
+  toString: function() {
+    return this.source.spec;
+  }
+};
+
+this.EXPORTED_SYMBOLS = ['IdpSandbox'];
+this.IdpSandbox = IdpSandbox;
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -384,21 +384,23 @@ RTCPeerConnection.prototype = {
           "InvalidStateError");
     }
     return this._pc;
   },
 
   _initIdp: function() {
     let prefName = "media.peerconnection.identity.timeout";
     let idpTimeout = Services.prefs.getIntPref(prefName);
-    let warningFunc = this.logWarning.bind(this);
-    this._localIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
-                                           this.dispatchEvent.bind(this));
-    this._remoteIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
-                                            this.dispatchEvent.bind(this));
+    let warn = this.logWarning.bind(this);
+    let idpErrorReport = (type, args) => {
+      this.dispatchEvent(
+        new this._win.RTCPeerConnectionIdentityErrorEvent(type, args));
+    };
+    this._localIdp = new PeerConnectionIdp(idpTimeout, warn, idpErrorReport);
+    this._remoteIdp = new PeerConnectionIdp(idpTimeout, warn, idpErrorReport);
   },
 
   // Add a function to the internal operations chain.
 
   _chain: function(func) {
     this._checkClosed(); // out here DOMException line-numbers work.
     let p = this._operationsChain.then(() => {
       // Don't _checkClosed() inside the chain, because it throws, and spec
@@ -701,61 +703,57 @@ RTCPeerConnection.prototype = {
 
         // Do setRemoteDescription and identity validation in parallel
         let p = new this._win.Promise((resolve, reject) => {
           this._onSetRemoteDescriptionSuccess = resolve;
           this._onSetRemoteDescriptionFailure = reject;
           this._impl.setRemoteDescription(type, desc.sdp);
         });
 
-        let pp = new Promise(resolve =>
-            this._remoteIdp.verifyIdentityFromSDP(desc.sdp, origin, resolve))
-        .then(msg => {
-          // If this pc has an identity already, then identity in sdp must match
-          if (expectedIdentity && (!msg || msg.identity !== expectedIdentity)) {
-            throw new this._win.DOMException(
-                "Peer Identity mismatch, expected: " + expectedIdentity,
-                "IncompatibleSessionDescriptionError");
-          }
-          if (msg) {
-            // Set new identity and generate an event.
-            this._impl.peerIdentity = msg.identity;
-            this._peerIdentity = new this._win.RTCIdentityAssertion(
-              this._remoteIdp.provider, msg.identity);
-            this.dispatchEvent(new this._win.Event("peeridentity"));
-          }
-        });
+        let pp = this._remoteIdp.verifyIdentityFromSDP(desc.sdp, origin)
+            .then(msg => {
+              // If this pc has an identity already, then identity in sdp must match
+              if (expectedIdentity && (!msg || msg.identity !== expectedIdentity)) {
+                throw new this._win.DOMException(
+                  "Peer Identity mismatch, expected: " + expectedIdentity,
+                  "IncompatibleSessionDescriptionError");
+              }
+              if (msg) {
+                // Set new identity and generate an event.
+                this._impl.peerIdentity = msg.identity;
+                this._peerIdentity = new this._win.RTCIdentityAssertion(
+                  this._remoteIdp.provider, msg.identity);
+                this.dispatchEvent(new this._win.Event("peeridentity"));
+              }
+            });
         // Only wait for Idp validation if we need identity matching.
         return expectedIdentity? this._win.Promise.all([p, pp]).then(() => {}) : p;
       });
     });
   },
 
   setIdentityProvider: function(provider, protocol, username) {
     this._checkClosed();
     this._localIdp.setIdentityProvider(provider, protocol, username);
   },
 
-  _gotIdentityAssertion: function(assertion){
+  _gotIdentityAssertion: function(assertion) {
+    if (!assertion) {
+      return;
+    }
     let args = { assertion: assertion };
     let ev = new this._win.RTCPeerConnectionIdentityEvent("identityresult", args);
     this.dispatchEvent(ev);
   },
 
   getIdentityAssertion: function() {
     this._checkClosed();
 
-    var gotAssertion = assertion => {
-      if (assertion) {
-        this._gotIdentityAssertion(assertion);
-      }
-    };
-
-    this._localIdp.getIdentityAssertion(this._impl.fingerprint,
-                                        gotAssertion);
+    this._localIdp.getIdentityAssertion(this._impl.fingerprint)
+      .then(assertion => this._gotIdentityAssertion(assertion));
   },
 
   updateIce: function(config) {
     throw new this._win.DOMException("updateIce not yet implemented",
                                      "NotSupportedError");
   },
 
   addIceCandidate: function(c, onSuccess, onError) {
@@ -883,17 +881,17 @@ RTCPeerConnection.prototype = {
 
   get localDescription() {
     this._checkClosed();
     let sdp = this._impl.localDescription;
     if (sdp.length == 0) {
       return null;
     }
 
-    sdp = this._localIdp.wrapSdp(sdp);
+    sdp = this._localIdp.addIdentityAttribute(sdp);
     return new this._win.mozRTCSessionDescription({ type: this._localType,
                                                     sdp: sdp });
   },
 
   get remoteDescription() {
     this._checkClosed();
     let sdp = this._impl.remoteDescription;
     if (sdp.length == 0) {
@@ -1037,42 +1035,52 @@ PeerConnectionObserver.prototype = {
   },
 
   dispatchEvent: function(event) {
     this._dompc.dispatchEvent(event);
   },
 
   onCreateOfferSuccess: function(sdp) {
     let pc = this._dompc;
-    let fp = pc._impl.fingerprint;
-    let origin = Cu.getWebIDLCallerPrincipal().origin;
-    pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
-      if (assertion) {
-        pc._gotIdentityAssertion(assertion);
-      }
+    let idp = pc._localIdp;
+
+    if (idp.enabled) {
+      idp.getIdentityAssertion(pc._impl.fingerprint)
+        .then(assertion => {
+          pc._gotIdentityAssertion(assertion);
+          sdp = idp.addIdentityAttribute(sdp);
+          pc._onCreateOfferSuccess(new pc._win.mozRTCSessionDescription({ type: "offer",
+                                                                          sdp: sdp }));
+        }, e => {}); // errors are handled in the IdP
+    } else {
       pc._onCreateOfferSuccess(new pc._win.mozRTCSessionDescription({ type: "offer",
                                                                       sdp: sdp }));
-    }.bind(this));
+    }
   },
 
   onCreateOfferError: function(code, message) {
     this._dompc._onCreateOfferFailure(this.newError(message, code));
   },
 
   onCreateAnswerSuccess: function(sdp) {
     let pc = this._dompc;
-    let fp = pc._impl.fingerprint;
-    let origin = Cu.getWebIDLCallerPrincipal().origin;
-    pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
-      if (assertion) {
-        pc._gotIdentityAssertion(assertion);
-      }
+    let idp = pc._localIdp;
+
+    if (idp.enabled) {
+      idp.getIdentityAssertion(pc._impl.fingerprint)
+        .then(assertion => {
+          pc._gotIdentityAssertion(assertion);
+          sdp = idp.addIdentityAttribute(sdp);
+          pc._onCreateAnswerSuccess(new pc._win.mozRTCSessionDescription({ type: "answer",
+                                                                           sdp: sdp }));
+        }, e => {});
+    } else {
       pc._onCreateAnswerSuccess(new pc._win.mozRTCSessionDescription({ type: "answer",
                                                                        sdp: sdp }));
-    }.bind(this));
+    }
   },
 
   onCreateAnswerError: function(code, message) {
     this._dompc._onCreateAnswerFailure(this.newError(message, code));
   },
 
   onSetLocalDescriptionSuccess: function() {
     this._dompc._onSetLocalDescriptionSuccess();
--- a/dom/media/PeerConnectionIdp.jsm
+++ b/dom/media/PeerConnectionIdp.jsm
@@ -1,366 +1,354 @@
 /* jshint moz:true, browser:true */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-this.EXPORTED_SYMBOLS = ["PeerConnectionIdp"];
+this.EXPORTED_SYMBOLS = ['PeerConnectionIdp'];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "IdpProxy",
-  "resource://gre/modules/media/IdpProxy.jsm");
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'IdpSandbox',
+  'resource://gre/modules/media/IdpSandbox.jsm');
+
+function TimerResolver(resolve) {
+  this.notify = resolve;
+}
+TimerResolver.prototype = {
+  getInterface: function(iid) {
+    return this.QueryInterface(iid);
+  },
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+}
+function delay(t) {
+  return new Promise(resolve => {
+    let timer = Cc['@mozilla.org/timer;1'].getService(Ci.nsITimer);
+    timer.initWithCallback(new TimerResolver(resolve), t, 0); // One shot
+  });
+}
 
 /**
  * Creates an IdP helper.
  *
- * @param window (object) the window object to use for miscellaneous goodies
  * @param timeout (int) the timeout in milliseconds
  * @param warningFunc (function) somewhere to dump warning messages
- * @param dispatchEventFunc (function) somewhere to dump error events
+ * @param dispatchErrorFunc (function(string, dict)) somewhere to dump errors
  */
-function PeerConnectionIdp(window, timeout, warningFunc, dispatchEventFunc) {
-  this._win = window;
+function PeerConnectionIdp(timeout, warningFunc, dispatchErrorFunc) {
   this._timeout = timeout || 5000;
   this._warning = warningFunc;
-  this._dispatchEvent = dispatchEventFunc;
+  this._dispatchError = dispatchErrorFunc;
 
   this.assertion = null;
   this.provider = null;
 }
 
 (function() {
-  PeerConnectionIdp._mLinePattern = new RegExp("^m=", "m");
+  PeerConnectionIdp._mLinePattern = new RegExp('^m=', 'm');
   // attributes are funny, the 'a' is case sensitive, the name isn't
-  let pattern = "^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)";
-  PeerConnectionIdp._identityPattern = new RegExp(pattern, "m");
-  pattern = "^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)";
-  PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, "m");
+  let pattern = '^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)';
+  PeerConnectionIdp._identityPattern = new RegExp(pattern, 'm');
+  pattern = '^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)';
+  PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, 'm');
 })();
 
 PeerConnectionIdp.prototype = {
+  get enabled() {
+    return !!this._idp;
+  },
+
   setIdentityProvider: function(provider, protocol, username) {
     this.provider = provider;
-    this.protocol = protocol;
+    this.protocol = protocol || 'default';
     this.username = username;
-    if (this._idpchannel) {
-      if (this._idpchannel.isSame(provider, protocol)) {
-        return;
+    if (this._idp) {
+      if (this._idp.isSame(provider, protocol)) {
+        return; // noop
       }
-      this._idpchannel.close();
+      this._idp.stop();
     }
-    this._idpchannel = new IdpProxy(provider, protocol);
+    this._idp = new IdpSandbox(provider, protocol);
   },
 
   close: function() {
     this.assertion = null;
     this.provider = null;
-    if (this._idpchannel) {
-      this._idpchannel.close();
-      this._idpchannel = null;
+    this.protocol = null;
+    if (this._idp) {
+      this._idp.stop();
+      this._idp = null;
     }
   },
 
   /**
    * Generate an error event of the identified type;
    * and put a little more precise information in the console.
+   *
+   * A little note on error handling in this class: this class reports errors
+   * exclusively through the event handlers that are passed to it
+   * (this._dispatchError, specifically).  That means that all the functions
+   * return resolved promises; promises are never rejected.  This probably isn't
+   * the best design, but the refactor can wait.
    */
   reportError: function(type, message, extra) {
     let args = {
       idp: this.provider,
       protocol: this.protocol
     };
     if (extra) {
       Object.keys(extra).forEach(function(k) {
         args[k] = extra[k];
       });
     }
-    this._warning("RTC identity: " + message, null, 0);
-    let ev = new this._win.RTCPeerConnectionIdentityErrorEvent('idp' + type + 'error', args);
-    this._dispatchEvent(ev);
+    this._warning('RTC identity: ' + message, null, 0);
+    this._dispatchError('idp' + type + 'error', args);
   },
 
   _getFingerprintsFromSdp: function(sdp) {
     let fingerprints = {};
     let m = sdp.match(PeerConnectionIdp._fingerprintPattern);
     while (m) {
       fingerprints[m[0]] = { algorithm: m[1], digest: m[2] };
       sdp = sdp.substring(m.index + m[0].length);
       m = sdp.match(PeerConnectionIdp._fingerprintPattern);
     }
 
     return Object.keys(fingerprints).map(k => fingerprints[k]);
   },
 
+  _isValidAssertion: function(assertion) {
+    return assertion && assertion.idp &&
+      typeof assertion.idp.domain === 'string' &&
+      (!assertion.idp.protocol ||
+       typeof assertion.idp.protocol === 'string') &&
+      typeof assertion.assertion === 'string';
+  },
+
   _getIdentityFromSdp: function(sdp) {
     // a=identity is session level
     let idMatch;
     let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern);
     if (mLineMatch) {
       let sessionLevel = sdp.substring(0, mLineMatch.index);
-      idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
+      let idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
+    }
+    if (!idMatch) {
+      return; // undefined === no identity
     }
-    if (idMatch) {
-      let assertion = {};
-      try {
-        assertion = JSON.parse(atob(idMatch[1]));
-      } catch (e) {
-        this.reportError("validation",
-                         "invalid identity assertion: " + e);
-      } // for JSON.parse
-      if (typeof assertion.idp === "object" &&
-          typeof assertion.idp.domain === "string" &&
-          typeof assertion.assertion === "string") {
-        return assertion;
-      }
 
-      this.reportError("validation", "assertion missing" +
-                       " idp/idp.domain/assertion");
+    let assertion;
+    try {
+      assertion = JSON.parse(atob(idMatch[1]));
+    } catch (e) {
+      this.reportError('validation',
+                       'invalid identity assertion: ' + e);
     }
-    // undefined!
+    if (!this._isValidAssertion(assertion)) {
+      this.reportError('validation', 'assertion missing' +
+                       ' idp/idp.domain/assertion');
+    }
+    return assertion;
   },
 
   /**
-   * Queues a task to verify the a=identity line the given SDP contains, if any.
+   * Verifies the a=identity line the given SDP contains, if any.
    * If the verification succeeds callback is called with the message from the
    * IdP proxy as parameter, else (verification failed OR no a=identity line in
    * SDP at all) null is passed to callback.
+   *
+   * Note that this only verifies that the SDP is coherent.  This relies on the
+   * invariant that the RTCPeerConnection won't connect to a peer if the
+   * fingerprint of the certificate they offer doesn't appear in the SDP.
    */
-  verifyIdentityFromSDP: function(sdp, origin, callback) {
+  verifyIdentityFromSDP: function(sdp, origin) {
     let identity = this._getIdentityFromSdp(sdp);
     let fingerprints = this._getFingerprintsFromSdp(sdp);
-    // it's safe to use the fingerprint we got from the SDP here,
-    // only because we ensure that there is only one
     if (!identity || fingerprints.length <= 0) {
-      callback(null);
-      return;
+      return Promise.resolve();
     }
 
     this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
-    this._verifyIdentity(identity.assertion, fingerprints, origin, callback);
+    return this._verifyIdentity(identity.assertion, fingerprints, origin);
   },
 
   /**
    * Checks that the name in the identity provided by the IdP is OK.
    *
+   * @param error (function) an error function to call
    * @param name (string) the name to validate
-   * @returns (string) an error message, iff the name isn't good
+   * @throws if the name isn't valid
    */
-  _validateName: function(name) {
-    if (typeof name !== "string") {
-      return "name not a string";
+  _validateName: function(error, name) {
+    if (typeof name !== 'string') {
+      return error('name not a string');
     }
-    let atIdx = name.indexOf("@");
-    if (atIdx > 0) {
-      // no third party assertions... for now
-      let tail = name.substring(atIdx + 1);
+    let atIdx = name.indexOf('@');
+    if (atIdx <= 0) {
+      return error('missing authority in name from IdP');
+    }
 
-      // strip the port number, if present
-      let provider = this.provider;
-      let providerPortIdx = provider.indexOf(":");
-      if (providerPortIdx > 0) {
-        provider = provider.substring(0, providerPortIdx);
-      }
-      let idnService = Components.classes["@mozilla.org/network/idn-service;1"].
-        getService(Components.interfaces.nsIIDNService);
-      if (idnService.convertUTF8toACE(tail) !==
-          idnService.convertUTF8toACE(provider)) {
-        return "name '" + identity.name +
-            "' doesn't match IdP: '" + this.provider + "'";
-      }
-      return null;
+    // no third party assertions... for now
+    let tail = name.substring(atIdx + 1);
+
+    // strip the port number, if present
+    let provider = this.provider;
+    let providerPortIdx = provider.indexOf(':');
+    if (providerPortIdx > 0) {
+      provider = provider.substring(0, providerPortIdx);
     }
-    return "missing authority in name from IdP";
+    let idnService = Components.classes['@mozilla.org/network/idn-service;1'].
+      getService(Components.interfaces.nsIIDNService);
+    if (idnService.convertUTF8toACE(tail) !==
+        idnService.convertUTF8toACE(provider)) {
+      return error('name "' + identity.name +
+            '" doesn\'t match IdP: "' + this.provider + '"');
+    }
+    return true;
   },
 
-  // we are very defensive here when handling the message from the IdP
-  // proxy so that broken IdPs can only do as little harm as possible.
-  _checkVerifyResponse: function(message, fingerprints) {
-    let warn = msg => {
-      this.reportError("validation",
-                       "assertion validation failure: " + msg);
+  /**
+   * Check the validation response.  We are very defensive here when handling
+   * the message from the IdP proxy.  That way, broken IdPs aren't likely to
+   * cause catastrophic damage.
+   */
+  _isValidVerificationResponse: function(validation, sdpFingerprints) {
+    let error = msg => {
+      this.reportError('validation', 'assertion validation failure: ' + msg);
+      return false;
     };
 
-    let isSubsetOf = (outer, inner, cmp) => {
-      return inner.some(i => {
-        return !outer.some(o => cmp(i, o));
+    if (typeof validation !== 'object' ||
+        typeof validation.contents !== 'string' ||
+        typeof validation.identity !== 'string') {
+      return error('no payload in validation response');
+    }
+
+    let fingerprints;
+    try {
+      fingerprints = JSON.parse(validation.contents).fingerprint;
+    } catch (e) {
+      return error('idp returned invalid JSON');
+    }
+
+    let isFingerprint = f =>
+        (typeof f.digest === 'string') &&
+        (typeof f.algorithm === 'string');
+    if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) {
+      return error('fingerprints must be an array of objects' +
+                   ' with digest and algorithm attributes');
+    }
+
+    let isSubsetOf = (outerSet, innerSet, comparator) => {
+      return innerSet.every(i => {
+        return outerSet.some(o => comparator(i, o));
       });
     };
     let compareFingerprints = (a, b) => {
       return (a.digest === b.digest) && (a.algorithm === b.algorithm);
     };
-
-    try {
-      let contents = JSON.parse(message.contents);
-      if (!Array.isArray(contents.fingerprint)) {
-        warn("fingerprint is not an array");
-      } else if (isSubsetOf(contents.fingerprint, fingerprints,
-                            compareFingerprints)) {
-        warn("fingerprints in SDP aren't a subset of those in the assertion");
-      } else {
-        let error = this._validateName(message.identity);
-        if (error) {
-          warn(error);
-        } else {
-          return true;
-        }
-      }
-    } catch(e) {
-      warn("invalid JSON in content");
+    if (!isSubsetOf(fingerprints, sdpFingerprints, compareFingerprints)) {
+      return error('the fingerprints in SDP aren\'t covered by the assertion');
     }
-    return false;
+    return this._validateName(error, validation.identity);
   },
 
   /**
-   * Asks the IdP proxy to verify an identity.
+   * Asks the IdP proxy to verify an identity assertion.
    */
-  _verifyIdentity: function(assertion, fingerprints, origin, callback) {
-    function onVerification(message) {
-      if (message && this._checkVerifyResponse(message, fingerprints)) {
-        callback(message);
-      } else {
-        this._warning("RTC identity: assertion validation failure", null, 0);
-        callback(null);
-      }
-    }
+  _verifyIdentity: function(assertion, fingerprints, origin) {
+    let validationPromise = this._idp.start()
+        .then(idp => idp.validateAssertion(assertion, origin));
 
-    let request = {
-      type: "VERIFY",
-      message: assertion,
-      origin: origin
-    };
-    this._sendToIdp(request, "validation", onVerification.bind(this));
+    return this._safetyNet('validation', validationPromise)
+      .then(validation => {
+        if (validation &&
+            this._isValidVerificationResponse(validation, fingerprints)) {
+          return validation;
+        }
+      });
   },
 
   /**
-   * Asks the IdP proxy for an identity assertion and, on success, enriches the
-   * given SDP with an a=identity line and calls callback with the new SDP as
-   * parameter. If no IdP is configured the original SDP (without a=identity
-   * line) is passed to the callback.
+   * Enriches the given SDP with an `a=identity` line.  getIdentityAssertion()
+   * must have already run successfully, otherwise this does nothing to the sdp.
    */
-  appendIdentityToSDP: function(sdp, fingerprint, origin, callback) {
-    let onAssertion = function() {
-      callback(this.wrapSdp(sdp), this.assertion);
-    }.bind(this);
-
-    if (!this._idpchannel || this.assertion) {
-      onAssertion();
-      return;
-    }
-
-    this._getIdentityAssertion(fingerprint, origin, onAssertion);
-  },
-
-  /**
-   * Inserts an identity assertion into the given SDP.
-   */
-  wrapSdp: function(sdp) {
+  addIdentityAttribute: function(sdp) {
     if (!this.assertion) {
       return sdp;
     }
 
     // yes, we assume that this matches; if it doesn't something is *wrong*
     let match = sdp.match(PeerConnectionIdp._mLinePattern);
     return sdp.substring(0, match.index) +
-      "a=identity:" + this.assertion + "\r\n" +
+      'a=identity:' + this.assertion + '\r\n' +
       sdp.substring(match.index);
   },
 
-  getIdentityAssertion: function(fingerprint, callback) {
-    if (!this._idpchannel) {
-      this.reportError("assertion", "IdP not set");
-      callback(null);
-      return;
+  /**
+   * Asks the IdP proxy for an identity assertion.  Don't call this unless you
+   * have checked .enabled, or you really like exceptions.
+   */
+  getIdentityAssertion: function(fingerprint) {
+    if (!this.enabled) {
+      this.reportError('assertion', 'no IdP set,' +
+                       ' call setIdentityProvider() to set one');
+      return Promise.resolve();
     }
 
-    let origin = Cu.getWebIDLCallerPrincipal().origin;
-    this._getIdentityAssertion(fingerprint, origin, callback);
-  },
-
-  _getIdentityAssertion: function(fingerprint, origin, callback) {
-    let [algorithm, digest] = fingerprint.split(" ", 2);
-    let message = {
+    let [algorithm, digest] = fingerprint.split(' ', 2);
+    let content = {
       fingerprint: [{
         algorithm: algorithm,
         digest: digest
       }]
     };
-    let request = {
-      type: "SIGN",
-      message: JSON.stringify(message),
-      username: this.username,
-      origin: origin
-    };
+    let origin = Cu.getWebIDLCallerPrincipal().origin;
+
+    let assertionPromise = this._idp.start()
+        .then(idp => idp.generateAssertion(JSON.stringify(content),
+                                           origin, this.username));
 
-    // catch the assertion, clean it up, warn if absent
-    function trapAssertion(assertion) {
-      if (!assertion) {
-        this._warning("RTC identity: assertion generation failure", null, 0);
-        this.assertion = null;
-      } else {
-        this.assertion = btoa(JSON.stringify(assertion));
-      }
-      callback(this.assertion);
-    }
-
-    this._sendToIdp(request, "assertion", trapAssertion.bind(this));
+    return this._safetyNet('assertion', assertionPromise)
+      .then(assertion => {
+        if (this._isValidAssertion(assertion)) {
+          // save the base64+JSON assertion, since that is all that is used
+          this.assertion = btoa(JSON.stringify(assertion));
+        } else {
+          if (assertion) {
+            // only report an error for an invalid assertion
+            // other paths generate more specific error reports
+            this.reportError('assertion', 'invalid assertion generated');
+          }
+          this.assertion = null;
+        }
+        return this.assertion;
+      });
   },
 
   /**
-   * Packages a message and sends it to the IdP.
-   * @param request (dictionary) the message to send
-   * @param type (DOMString) the type of message (assertion/validation)
-   * @param callback (function) the function to call with the results
+   * Wraps a promise, adding a timeout guard on it so that it can't take longer
+   * than the specified time.  Returns a promise that always resolves; if there
+   * is a problem the resolved value is undefined.
    */
-  _sendToIdp: function(request, type, callback) {
-    this._idpchannel.send(request, this._wrapCallback(type, callback));
-  },
-
-  _reportIdpError: function(type, message) {
-    let args = {};
-    let msg = "";
-    if (message.type === "ERROR") {
-      msg = message.error;
-    } else {
-      msg = JSON.stringify(message.message);
-      if (message.type === "LOGINNEEDED") {
-        args.loginUrl = message.loginUrl;
-      }
-    }
-    this.reportError(type, "received response of type '" +
-                     message.type + "' from IdP: " + msg, args);
-  },
-
-  /**
-   * Wraps a callback, adding a timeout and ensuring that the callback doesn't
-   * receive any message other than one where the IdP generated a "SUCCESS"
-   * response.
-   */
-  _wrapCallback: function(type, callback) {
-    let timeout = this._win.setTimeout(function() {
-      this.reportError(type, "IdP timeout for " + this._idpchannel + " " +
-                       (this._idpchannel.ready ? "[ready]" : "[not ready]"));
-      timeout = null;
-      callback(null);
-    }.bind(this), this._timeout);
-
-    return function(message) {
-      if (!timeout) {
-        return;
-      }
-      this._win.clearTimeout(timeout);
-      timeout = null;
-
-      let content = null;
-      if (message.type === "SUCCESS") {
-        content = message.message;
-      } else {
-        this._reportIdpError(type, message);
-      }
-      callback(content);
-    }.bind(this);
+  _safetyNet: function(type, p) {
+    let done = false; // ... all because Promises don't expose state
+    let timeoutPromise = delay(this._timeout)
+        .then(() => {
+          if (!done) {
+            this.reportError(type, 'IdP timed out');
+          }
+        });
+    let realPromise = p
+        .catch(e => this.reportError(type, 'error reported by IdP: ' + e.message))
+        .then(result => {
+          done = true;
+          return result;
+        });
+    // If timeoutPromise completes first, the returned value will be undefined,
+    // just like when there is an error.
+    return Promise.race([realPromise, timeoutPromise]);
   }
 };
 
 this.PeerConnectionIdp = PeerConnectionIdp;
--- a/dom/media/moz.build
+++ b/dom/media/moz.build
@@ -233,17 +233,17 @@ if CONFIG['GNU_CC'] or CONFIG['CLANG_CL'
   SOURCES['DecoderTraits.cpp'].flags += ['-Wno-error=multichar']
 
 EXTRA_COMPONENTS += [
     'PeerConnection.js',
     'PeerConnection.manifest',
 ]
 
 EXTRA_JS_MODULES.media += [
-    'IdpProxy.jsm',
+    'IdpSandbox.jsm',
     'PeerConnectionIdp.jsm',
     'RTCStatsReport.jsm',
 ]
 
 FAIL_ON_WARNINGS = True
 
 MSVC_ENABLE_PGO = True