Bug 884573 - Identity assertion generation and verification for WebRTC. r=abr
☠☠ backed out by c4aadf8ea4e0 ☠ ☠
authorMartin Thomson <martin.thomson@gmail.com>
Fri, 20 Dec 2013 14:55:59 -0800
changeset 163684 5e6c7217e92a0a98b100150d3a6b762256094bd7
parent 163683 6779278f87dfb5f21b91a6b326c156a5000c4d06
child 163685 bb7097d122565ad01149a298a8f3c580ea8dd96b
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewersabr
bugs884573
milestone29.0a1
Bug 884573 - Identity assertion generation and verification for WebRTC. r=abr
content/events/test/test_all_synthetic_events.html
dom/media/IdpProxy.jsm
dom/media/PeerConnection.js
dom/media/PeerConnection.manifest
dom/media/PeerConnectionIdp.jsm
dom/media/moz.build
dom/media/tests/mochitest/mochitest.ini
dom/media/tests/mochitest/pc.js
dom/media/tests/mochitest/test_getIdentityAssertion.html
dom/media/tests/mochitest/test_setIdentityProvider.html
dom/media/tests/mochitest/test_setIdentityProviderWithErrors.html
dom/tests/mochitest/general/test_interfaces.html
dom/webidl/RTCIdentityAssertion.webidl
dom/webidl/RTCPeerConnection.webidl
dom/webidl/RTCPeerConnectionIdentityEvent.webidl
dom/webidl/moz.build
modules/libpref/src/init/all.js
--- a/content/events/test/test_all_synthetic_events.html
+++ b/content/events/test/test_all_synthetic_events.html
@@ -325,16 +325,20 @@ const kEventConstructors = {
   RTCDataChannelEvent:                       { create: function (aName, aProps) {
                                                          return new RTCDataChannelEvent(aName, aProps);
                                                        },
                                              },
   RTCPeerConnectionIceEvent:                 { create: function (aName, aProps) {
                                                          return new RTCPeerConnectionIceEvent(aName, aProps);
                                                        },
                                              },
+  RTCPeerConnectionIdentityEvent:            { create: function (aName, aProps) {
+                                                         return new RTCPeerConnectionIdentityEvent(aName, aProps);
+                                                       },
+                                             },
   ScrollAreaEvent:                           { create: function (aName, aProps) {
                                                          var e = document.createEvent("scrollareaevent");
                                                          e.initScrollAreaEvent(aName, aProps.bubbles, aProps.cancelable,
                                                                                aProps.view, aProps.details,
                                                                                aProps.x || 0.0, aProps.y || 0.0,
                                                                                aProps.width || 0.0, aProps.height || 0.0);
                                                          return e;
                                                        },
--- a/dom/media/IdpProxy.jsm
+++ b/dom/media/IdpProxy.jsm
@@ -42,17 +42,17 @@ IdpChannel.prototype = {
    *                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"));
     }
 
-    var ready = this._sandboxReady.bind(this, callback);
+    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.
     this.messagechannel = new aSandbox._frame.contentWindow.MessageChannel();
     try {
       Object.defineProperty(
@@ -185,16 +185,19 @@ IdpProxy.prototype = {
   },
 
   /**
    * 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;
@@ -207,17 +210,17 @@ IdpProxy.prototype = {
   /**
    * 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 (message.type === "READY") {
+    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];
@@ -233,28 +236,33 @@ IdpProxy.prototype = {
   /**
    * 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" };
-    Object.keys(this.tracking).forEach(function(k) {
-      this.tracking[k](error);
+    let error = { type: "ERROR", message: "IdP closed" };
+    Object.keys(trackingCopy).forEach(function(k) {
+      this.trackingCopy[k](error);
     }, this);
-    this.pending.forEach(function(p) {
+    pendingCopy.forEach(function(p) {
       p.callback(error);
     }, this);
 
-    this.channel.close();
-    this._reset();
   },
 
   toString: function() {
     return this.domain + '/' + this.protocol;
   }
 };
 
 this.IdpProxy = IdpProxy;
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -3,33 +3,38 @@
  * 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/PopupNotifications.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PeerConnectionIdp",
+  "resource://gre/modules/media/PeerConnectionIdp.jsm");
 
 const PC_CONTRACT = "@mozilla.org/dom/peerconnection;1";
 const WEBRTC_GLOBAL_CONTRACT = "@mozilla.org/dom/webrtcglobalinformation1";
 const PC_OBS_CONTRACT = "@mozilla.org/dom/peerconnectionobserver;1";
 const PC_ICE_CONTRACT = "@mozilla.org/dom/rtcicecandidate;1";
 const PC_SESSION_CONTRACT = "@mozilla.org/dom/rtcsessiondescription;1";
 const PC_MANAGER_CONTRACT = "@mozilla.org/dom/peerconnectionmanager;1";
 const PC_STATS_CONTRACT = "@mozilla.org/dom/rtcstatsreport;1";
+const PC_IDENTITY_CONTRACT = "@mozilla.org/dom/rtcidentityassertion;1";
 
 const PC_CID = Components.ID("{00e0e20d-1494-4776-8e0e-0f0acbea3c79}");
 const WEBRTC_GLOBAL_CID = Components.ID("{f6063d11-f467-49ad-9765-e7923050dc08}");
 const PC_OBS_CID = Components.ID("{d1748d4c-7f6a-4dc5-add6-d55b7678537e}");
 const PC_ICE_CID = Components.ID("{02b9970c-433d-4cc2-923d-f7028ac66073}");
 const PC_SESSION_CID = Components.ID("{1775081b-b62d-4954-8ffe-a067bbf508a7}");
 const PC_MANAGER_CID = Components.ID("{7293e901-2be3-4c02-b4bd-cbef6fc24f78}");
 const PC_STATS_CID = Components.ID("{7fe6e18b-0da3-4056-bf3b-440ef3809e06}");
+const PC_IDENTITY_CID = Components.ID("{1abc7499-3c54-43e0-bd60-686e2703f072}");
 
 // Global list of PeerConnection objects, so they can be cleaned up when
 // a page is torn down. (Maps inner window ID to an array of PC objects).
 function GlobalPCList() {
   this._list = {};
   this._networkdown = false; // XXX Need to query current state somehow
   Services.obs.addObserver(this, "inner-window-destroyed", true);
   Services.obs.addObserver(this, "profile-change-net-teardown", true);
@@ -95,28 +100,28 @@ GlobalPCList.prototype = {
     };
 
     if (topic == "inner-window-destroyed") {
       cleanupWinId(this._list, subject.QueryInterface(Ci.nsISupportsPRUint64).data);
     } else if (topic == "profile-change-net-teardown" ||
                topic == "network:offline-about-to-go-offline") {
       // Delete all peerconnections on shutdown - mostly synchronously (we
       // need them to be done deleting transports and streams before we
-      // return)!  All socket operations must be queued to STS thread
+      // return)! All socket operations must be queued to STS thread
       // before we return to here.
       // Also kill them if "Work Offline" is selected - more can be created
       // while offline, but attempts to connect them should fail.
       for (let winId in this._list) {
         cleanupWinId(this._list, winId);
       }
       this._networkdown = true;
     }
     else if (topic == "network:offline-status-changed") {
       if (data == "offline") {
-	// this._list shold be empty here
+        // this._list shold be empty here
         this._networkdown = true;
       } else if (data == "online") {
         this._networkdown = false;
       }
     }
   },
 
   getStatsForEachPC: function(callback, errorCallback) {
@@ -273,16 +278,32 @@ RTCStatsReport.prototype = {
 
   has: function(key) {
     return this._report[key] !== undefined;
   },
 
   get mozPcid() { return this._pcid; }
 };
 
+function RTCIdentityAssertion() {}
+RTCIdentityAssertion.prototype = {
+  classDescription: "RTCIdentityAssertion",
+  classID: PC_IDENTITY_CID,
+  contractID: PC_IDENTITY_CONTRACT,
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
+                                         Ci.nsIDOMGlobalPropertyInitializer]),
+
+  init: function(win) { this._win = win; },
+
+  __init: function(idp, name) {
+    this.idp = idp;
+    this.name  = name;
+  }
+};
+
 function RTCPeerConnection() {
   this._queue = [];
 
   this._pc = null;
   this._observer = null;
   this._closed = false;
 
   this._onCreateOfferSuccess = null;
@@ -293,24 +314,25 @@ function RTCPeerConnection() {
   this._onGetStatsFailure = null;
   this._onGetLoggingSuccess = null;
   this._onGetLoggingFailure = null;
 
   this._pendingType = null;
   this._localType = null;
   this._remoteType = null;
   this._trickleIce = false;
+  this._peerIdentity = null;
 
   /**
-   * Everytime we get a request from content, we put it in the queue. If
-   * there are no pending operations though, we will execute it immediately.
-   * In PeerConnectionObserver, whenever we are notified that an operation
-   * has finished, we will check the queue for the next operation and execute
-   * if neccesary. The _pending flag indicates whether an operation is currently
-   * in progress.
+   * Everytime we get a request from content, we put it in the queue. If there
+   * are no pending operations though, we will execute it immediately. In
+   * PeerConnectionObserver, whenever we are notified that an operation has
+   * finished, we will check the queue for the next operation and execute if
+   * neccesary. The _pending flag indicates whether an operation is currently in
+   * progress.
    */
   this._pending = false;
 
   // States
   this._iceGatheringState = this._iceConnectionState = "new";
 }
 RTCPeerConnection.prototype = {
   classDescription: "mozRTCPeerConnection",
@@ -338,53 +360,63 @@ RTCPeerConnection.prototype = {
     this.makeGetterSetterEH("onicecandidate");
     this.makeGetterSetterEH("onnegotiationneeded");
     this.makeGetterSetterEH("onsignalingstatechange");
     this.makeGetterSetterEH("onremovestream");
     this.makeGetterSetterEH("ondatachannel");
     this.makeGetterSetterEH("onconnection");
     this.makeGetterSetterEH("onclosedconnection");
     this.makeGetterSetterEH("oniceconnectionstatechange");
+    this.makeGetterSetterEH("onidentityresult");
 
     this._pc = new this._win.PeerConnectionImpl();
 
     this.__DOM_IMPL__._innerObject = this;
     this._observer = new this._win.PeerConnectionObserver(this.__DOM_IMPL__);
-    this._winID = this._win.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
 
     // Add a reference to the PeerConnection to global list (before init).
+    this._winID = this._win.QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
     _globalPCList.addPC(this);
 
     this._queueOrRun({
       func: this._initialize,
       args: [rtcConfig],
       // If not trickling, suppress start.
       wait: !this._trickleIce
     });
   },
 
   _initialize: function(rtcConfig) {
-    this._getPC().initialize(this._observer, this._win, rtcConfig,
-                             Services.tm.currentThread);
+    this._impl.initialize(this._observer, this._win, rtcConfig,
+                          Services.tm.currentThread);
+    this._initIdp();
   },
 
-  _getPC: function() {
+  get _impl() {
     if (!this._pc) {
       throw new this._win.DOMError("",
           "RTCPeerConnection is gone (did you enter Offline mode?)");
     }
     return this._pc;
   },
 
+  _initIdp: function() {
+    let prefName = "media.peerconnection.identity.timeout";
+    let idpTimeout = Services.prefs.getIntPref(prefName);
+    let warningFunc = this.reportWarning.bind(this);
+    this._localIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc);
+    this._remoteIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc);
+  },
+
   /**
    * Add a function to the queue or run it immediately if the queue is empty.
    * Argument is an object with the func, args and wait properties; wait should
-   * be set to true if the function has a success/error callback that will
-   * call _executeNext, false if it doesn't have a callback.
+   * be set to true if the function has a success/error callback that will call
+   * _executeNext, false if it doesn't have a callback.
    */
   _queueOrRun: function(obj) {
     this._checkClosed();
     if (!this._pending) {
       if (obj.type !== undefined) {
         this._pendingType = obj.type;
       }
       obj.func.apply(this, obj.args);
@@ -463,18 +495,18 @@ RTCPeerConnection.prototype = {
    * MediaConstraints look like this:
    *
    * {
    *   mandatory: {"OfferToReceiveAudio": true, "OfferToReceiveVideo": true },
    *   optional: [{"VoiceActivityDetection": true}, {"FooBar": 10}]
    * }
    *
    * WebIDL normalizes the top structure for us, but the mandatory constraints
-   * member comes in as a raw object so we can detect unknown constraints.
-   * We compare its members against ones we support, and fail if not found.
+   * member comes in as a raw object so we can detect unknown constraints. We
+   * compare its members against ones we support, and fail if not found.
    */
   _mustValidateConstraints: function(constraints, errorMsg) {
     if (constraints.mandatory) {
       let supported;
       try {
         // Passing the raw constraints.mandatory here validates its structure
         supported = this._observer.getSupportedConstraints(constraints.mandatory);
       } catch (e) {
@@ -582,17 +614,17 @@ RTCPeerConnection.prototype = {
       args: [onSuccess, onError, constraints],
       wait: true
     });
   },
 
   _createOffer: function(onSuccess, onError, constraints) {
     this._onCreateOfferSuccess = onSuccess;
     this._onCreateOfferFailure = onError;
-    this._getPC().createOffer(constraints);
+    this._impl.createOffer(constraints);
   },
 
   _createAnswer: function(onSuccess, onError, constraints, provisional) {
     this._onCreateAnswerSuccess = onSuccess;
     this._onCreateAnswerFailure = onError;
 
     if (!this.remoteDescription) {
 
@@ -605,17 +637,17 @@ RTCPeerConnection.prototype = {
 
       this._observer.onCreateAnswerError(Ci.IPeerConnection.kInvalidState,
                                          "No outstanding offer");
       return;
     }
 
     // TODO: Implement provisional answer.
 
-    this._getPC().createAnswer(constraints);
+    this._impl.createAnswer(constraints);
   },
 
   createAnswer: function(onSuccess, onError, constraints, provisional) {
     if (!constraints) {
       constraints = {};
     }
 
     this._mustValidateConstraints(constraints, "createAnswer passed invalid constraints");
@@ -653,17 +685,17 @@ RTCPeerConnection.prototype = {
       wait: true,
       type: desc.type
     });
   },
 
   _setLocalDescription: function(type, sdp, onSuccess, onError) {
     this._onSetLocalDescriptionSuccess = onSuccess;
     this._onSetLocalDescriptionFailure = onError;
-    this._getPC().setLocalDescription(type, sdp);
+    this._impl.setLocalDescription(type, sdp);
   },
 
   setRemoteDescription: function(desc, onSuccess, onError) {
     let type;
     switch (desc.type) {
       case "offer":
         type = Ci.IPeerConnection.kActionOffer;
         break;
@@ -672,28 +704,116 @@ RTCPeerConnection.prototype = {
         break;
       case "pranswer":
         throw new this._win.DOMError("", "pranswer not yet implemented");
       default:
         throw new this._win.DOMError("",
             "Invalid type " + desc.type + " provided to setRemoteDescription");
     }
 
+    try {
+      let showUX = this._showIdentityUx.bind(this);
+      this._remoteIdp.verifyIdentityFromSDP(desc.sdp, showUX);
+    } catch (e) {
+      this.reportWarning(e.message, e.fileName, e.lineNumber);
+      // only happens if processing the SDP for identity doesn't work
+      this._showIdentityUx(null);
+      // let _setRemoteDescription do the error reporting
+    }
+
     this._queueOrRun({
       func: this._setRemoteDescription,
       args: [type, desc.sdp, onSuccess, onError],
       wait: true,
       type: desc.type
     });
   },
 
+  _showIdentityUx: function(message) {
+    let browser = this._win.QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIWebNavigation)
+      .QueryInterface(Ci.nsIDocShell)
+      .chromeEventHandler
+      .ownerDocument
+      .defaultView
+      .gBrowser;
+    let notificationBox = browser.getNotificationBox();
+
+    if (message) {
+      this._peerIdentity = new this._win.RTCIdentityAssertion(
+          this._remoteIdp.provider, message.identity.name);
+
+      // TODO (:mt) - ultimately, these notifications need to go away
+      // for one, they are highly spoofable, which is very bad
+      // but they are also ugly and unnecessary
+      // Bug 942372 should provide a better approach
+      notificationBox.appendNotification(
+          "Identity of your WebRTC peer is " + this._peerIdentity.name,
+          "webrtc-auth",
+          null,
+          notificationBox.PRIORITY_INFO_HIGH,
+          []
+      );
+
+      let args = { peerIdentity: this._peerIdentity };
+      let ev = new this._win.RTCPeerConnectionIdentityEvent("identityresult",
+                                                            args);
+      this.dispatchEvent(ev);
+    } else {
+      notificationBox.appendNotification(
+          "Identity of your WebRTC peer is not verified",
+          "webrtc-auth",
+          null,
+          notificationBox.PRIORITY_WARNING_HIGH,
+          []
+      );
+    }
+  },
+
   _setRemoteDescription: function(type, sdp, onSuccess, onError) {
     this._onSetRemoteDescriptionSuccess = onSuccess;
     this._onSetRemoteDescriptionFailure = onError;
-    this._getPC().setRemoteDescription(type, sdp);
+    this._impl.setRemoteDescription(type, sdp);
+  },
+
+  setIdentityProvider: function(provider, protocol, username) {
+    this._checkClosed();
+    this._localIdp.setIdentityProvider(provider, protocol, username);
+  },
+
+  // we're going off spec with the error callback here.
+  getIdentityAssertion: function(errorCallback) {
+    this._checkClosed();
+    if (typeof errorCallback !== "function") {
+      if (errorCallback) {
+        let message ="getIdentityAssertion argument must be a function";
+        throw new this._win.DOMError("", message);
+      }
+      errorCallback = function() {
+        this.reportWarning("getIdentityAssertion: no error callback set");
+      }.bind(this);
+    }
+
+    function gotAssertion(assertion) {
+      if (assertion) {
+        let args = { assertion: assertion };
+        let ev = new this._win.RTCPeerConnectionIdentityEvent("identityresult", args);
+        this.dispatchEvent(ev);
+      } else {
+        errorCallback("IdP did not produce an assertion");
+      }
+    }
+
+    try {
+      this._localIdp.getIdentityAssertion(this._impl.fingerprint,
+          gotAssertion.bind(this));
+    }
+    catch (e) {
+      errorCallback("Could not get identity assertion: " + e.message);
+    }
   },
 
   updateIce: function(config, constraints) {
     throw new this._win.DOMError("", "updateIce not yet implemented");
   },
 
   addIceCandidate: function(cand, onSuccess, onError) {
     if (!cand.candidate && !cand.sdpMLineIndex) {
@@ -702,19 +822,19 @@ RTCPeerConnection.prototype = {
     }
     this._onAddIceCandidateSuccess = onSuccess || null;
     this._onAddIceCandidateError = onError || null;
 
     this._queueOrRun({ func: this._addIceCandidate, args: [cand], wait: true });
   },
 
   _addIceCandidate: function(cand) {
-    this._getPC().addIceCandidate(cand.candidate, cand.sdpMid || "",
-                                  (cand.sdpMLineIndex === null)? 0 :
-                                      cand.sdpMLineIndex + 1);
+    this._impl.addIceCandidate(cand.candidate, cand.sdpMid || "",
+                               (cand.sdpMLineIndex === null) ? 0 :
+                                 cand.sdpMLineIndex + 1);
   },
 
   addStream: function(stream, constraints) {
     if (!constraints) {
       constraints = {};
     }
     this._mustValidateConstraints(constraints,
                                   "addStream passed invalid constraints");
@@ -722,86 +842,91 @@ RTCPeerConnection.prototype = {
       throw new this._win.DOMError("", "Invalid stream passed to addStream!");
     }
     this._queueOrRun({ func: this._addStream,
                        args: [stream, constraints],
                        wait: false });
   },
 
   _addStream: function(stream, constraints) {
-    this._getPC().addStream(stream, constraints);
+    this._impl.addStream(stream, constraints);
   },
 
   removeStream: function(stream) {
-     //Bug 844295: Not implementing this functionality.
+     // Bug 844295: Not implementing this functionality.
      throw new this._win.DOMError("", "removeStream not yet implemented");
   },
 
   getStreamById: function(id) {
     throw new this._win.DOMError("", "getStreamById not yet implemented");
   },
 
   close: function() {
     this._queueOrRun({ func: this._close, args: [false], wait: false });
     this._closed = true;
     this.changeIceConnectionState("closed");
   },
 
   _close: function() {
-    this._getPC().close();
+    this._localIdp.close();
+    this._remoteIdp.close();
+    this._impl.close();
   },
 
   getLocalStreams: function() {
     this._checkClosed();
-    return this._getPC().getLocalStreams();
+    return this._impl.getLocalStreams();
   },
 
   getRemoteStreams: function() {
     this._checkClosed();
-    return this._getPC().getRemoteStreams();
+    return this._impl.getRemoteStreams();
   },
 
   get localDescription() {
     this._checkClosed();
-    let sdp = this._getPC().localDescription;
+    let sdp = this._impl.localDescription;
     if (sdp.length == 0) {
       return null;
     }
+
+    sdp = this._localIdp.wrapSdp(sdp);
     return new this._win.mozRTCSessionDescription({ type: this._localType,
                                                     sdp: sdp });
   },
 
   get remoteDescription() {
     this._checkClosed();
-    let sdp = this._getPC().remoteDescription;
+    let sdp = this._impl.remoteDescription;
     if (sdp.length == 0) {
       return null;
     }
     return new this._win.mozRTCSessionDescription({ type: this._remoteType,
                                                     sdp: sdp });
   },
 
+  get peerIdentity() { return this._peerIdentity; },
   get iceGatheringState()  { return this._iceGatheringState; },
   get iceConnectionState() { return this._iceConnectionState; },
 
   get signalingState() {
     // checking for our local pc closed indication
     // before invoking the pc methods.
-    if(this._closed) {
+    if (this._closed) {
       return "closed";
     }
     return {
       "SignalingInvalid":            "",
       "SignalingStable":             "stable",
       "SignalingHaveLocalOffer":     "have-local-offer",
       "SignalingHaveRemoteOffer":    "have-remote-offer",
       "SignalingHaveLocalPranswer":  "have-local-pranswer",
       "SignalingHaveRemotePranswer": "have-remote-pranswer",
       "SignalingClosed":             "closed"
-    }[this._getPC().signalingState];
+    }[this._impl.signalingState];
   },
 
   changeIceGatheringState: function(state) {
     this._iceGatheringState = state;
   },
 
   changeIceConnectionState: function(state) {
     this._iceConnectionState = state;
@@ -823,45 +948,46 @@ RTCPeerConnection.prototype = {
       wait: true
     });
   },
 
   _getStats: function(selector, onSuccess, onError, internal) {
     this._onGetStatsSuccess = onSuccess;
     this._onGetStatsFailure = onError;
 
-    this._getPC().getStats(selector, internal);
+    this._impl.getStats(selector, internal);
   },
 
   getLogging: function(pattern, onSuccess, onError) {
     this._queueOrRun({
       func: this._getLogging,
       args: [pattern, onSuccess, onError],
       wait: true
     });
   },
 
   _getLogging: function(pattern, onSuccess, onError) {
     this._onGetLoggingSuccess = onSuccess;
     this._onGetLoggingFailure = onError;
 
-    this._getPC().getLogging(pattern);
+    this._impl.getLogging(pattern);
   },
 
   createDataChannel: function(label, dict) {
     this._checkClosed();
     if (dict == undefined) {
       dict = {};
     }
     if (dict.maxRetransmitNum != undefined) {
       dict.maxRetransmits = dict.maxRetransmitNum;
       this.reportWarning("Deprecated RTCDataChannelInit dictionary entry maxRetransmitNum used!", null, 0);
     }
     if (dict.outOfOrderAllowed != undefined) {
-      dict.ordered = !dict.outOfOrderAllowed; // the meaning is swapped with the name change
+      dict.ordered = !dict.outOfOrderAllowed; // the meaning is swapped with
+                                              // the name change
       this.reportWarning("Deprecated RTCDataChannelInit dictionary entry outOfOrderAllowed used!", null, 0);
     }
     if (dict.preset != undefined) {
       dict.negotiated = dict.preset;
       this.reportWarning("Deprecated RTCDataChannelInit dictionary entry preset used!", null, 0);
     }
     if (dict.stream != undefined) {
       dict.id = dict.stream;
@@ -886,17 +1012,17 @@ RTCPeerConnection.prototype = {
       type = Ci.IPeerConnection.kDataChannelPartialReliableTimed;
     } else if (dict.maxRetransmits != undefined) {
       type = Ci.IPeerConnection.kDataChannelPartialReliableRexmit;
     } else {
       type = Ci.IPeerConnection.kDataChannelReliable;
     }
 
     // Synchronous since it doesn't block.
-    let channel = this._getPC().createDataChannel(
+    let channel = this._impl.createDataChannel(
       label, protocol, type, !dict.ordered, dict.maxRetransmitTime,
       dict.maxRetransmits, dict.negotiated ? true : false,
       dict.id != undefined ? dict.id : 0xFFFF
     );
     return channel;
   },
 
   connectDataConnection: function(localport, remoteport, numstreams) {
@@ -906,17 +1032,17 @@ RTCPeerConnection.prototype = {
     this._queueOrRun({
       func: this._connectDataConnection,
       args: [localport, remoteport, numstreams],
       wait: false
     });
   },
 
   _connectDataConnection: function(localport, remoteport, numstreams) {
-    this._getPC().connectDataConnection(localport, remoteport, numstreams);
+    this._impl.connectDataConnection(localport, remoteport, numstreams);
   }
 };
 
 function RTCError(code, message) {
   this.name = this.reasonName[Math.min(code, this.reasonName.length - 1)];
   this.message = (typeof message === "string")? message : this.name;
   this.__exposedProps__ = { name: "rw", message: "rw" };
 }
@@ -965,32 +1091,40 @@ PeerConnectionObserver.prototype = {
         // want this to take down peerconnection, but we still want the user
         // to see it, so we catch it, report it, and move on.
         this._dompc.reportError(e.message, e.fileName, e.lineNumber);
       }
     }
   },
 
   onCreateOfferSuccess: function(sdp) {
-    this.callCB(this._dompc._onCreateOfferSuccess,
-                new this._dompc._win.mozRTCSessionDescription({ type: "offer",
-                                                                sdp: sdp }));
-    this._dompc._executeNext();
+    let pc = this._dompc;
+    let fp = pc._impl.fingerprint;
+    pc._localIdp.appendIdentityToSDP(sdp, fp, function(sdp, assertion) {
+      this.callCB(pc._onCreateOfferSuccess,
+                  new pc._win.mozRTCSessionDescription({ type: "offer",
+                                                         sdp: sdp }));
+      pc._executeNext();
+    }.bind(this));
   },
 
   onCreateOfferError: function(code, message) {
     this.callCB(this._dompc._onCreateOfferFailure, new RTCError(code, message));
     this._dompc._executeNext();
   },
 
   onCreateAnswerSuccess: function(sdp) {
-    this.callCB (this._dompc._onCreateAnswerSuccess,
-                 new this._dompc._win.mozRTCSessionDescription({ type: "answer",
-                                                                 sdp: sdp }));
-    this._dompc._executeNext();
+    let pc = this._dompc;
+    let fp = pc._impl.fingerprint;
+    pc._localIdp.appendIdentityToSDP(sdp, fp, function(sdp, assertion) {
+      this.callCB (pc._onCreateAnswerSuccess,
+                   new pc._win.mozRTCSessionDescription({ type: "answer",
+                                                          sdp: sdp }));
+      pc._executeNext();
+    }.bind(this));
   },
 
   onCreateAnswerError: function(code, message) {
     this.callCB(this._dompc._onCreateAnswerFailure, new RTCError(code, message));
     this._dompc._executeNext();
   },
 
   onSetLocalDescriptionSuccess: function() {
@@ -1224,11 +1358,12 @@ PeerConnectionObserver.prototype = {
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(
   [GlobalPCList,
    RTCIceCandidate,
    RTCSessionDescription,
    RTCPeerConnection,
    RTCStatsReport,
+   RTCIdentityAssertion,
    PeerConnectionObserver,
    WebrtcGlobalInformation]
 );
--- a/dom/media/PeerConnection.manifest
+++ b/dom/media/PeerConnection.manifest
@@ -1,15 +1,17 @@
 component {00e0e20d-1494-4776-8e0e-0f0acbea3c79} PeerConnection.js
 component {f6063d11-f467-49ad-9765-e7923050dc08} PeerConnection.js
 component {d1748d4c-7f6a-4dc5-add6-d55b7678537e} PeerConnection.js
 component {02b9970c-433d-4cc2-923d-f7028ac66073} PeerConnection.js
 component {1775081b-b62d-4954-8ffe-a067bbf508a7} PeerConnection.js
 component {7293e901-2be3-4c02-b4bd-cbef6fc24f78} PeerConnection.js
 component {7fe6e18b-0da3-4056-bf3b-440ef3809e06} PeerConnection.js
+component {1abc7499-3c54-43e0-bd60-686e2703f072} PeerConnection.js
 
 contract @mozilla.org/dom/peerconnection;1 {00e0e20d-1494-4776-8e0e-0f0acbea3c79}
 contract @mozilla.org/dom/webrtcglobalinformation;1 {f6063d11-f467-49ad-9765-e7923050dc08}
 contract @mozilla.org/dom/peerconnectionobserver;1 {d1748d4c-7f6a-4dc5-add6-d55b7678537e}
 contract @mozilla.org/dom/rtcicecandidate;1 {02b9970c-433d-4cc2-923d-f7028ac66073}
 contract @mozilla.org/dom/rtcsessiondescription;1 {1775081b-b62d-4954-8ffe-a067bbf508a7}
 contract @mozilla.org/dom/peerconnectionmanager;1 {7293e901-2be3-4c02-b4bd-cbef6fc24f78}
 contract @mozilla.org/dom/rtcstatsreport;1 {7fe6e18b-0da3-4056-bf3b-440ef3809e06}
+contract @mozilla.org/dom/rtcidentityassertion;1 {1abc7499-3c54-43e0-bd60-686e2703f072}
new file mode 100644
--- /dev/null
+++ b/dom/media/PeerConnectionIdp.jsm
@@ -0,0 +1,340 @@
+/* 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"];
+
+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");
+
+/**
+ * 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
+ */
+function PeerConnectionIdp(window, timeout, warningFunc) {
+  this._win = window;
+  this._timeout = timeout || 5000;
+  this._warning = warningFunc;
+
+  this.assertion = null;
+  this.provider = null;
+}
+
+(function() {
+  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");
+})();
+
+PeerConnectionIdp.prototype = {
+  setIdentityProvider: function(
+      provider, protocol, username) {
+    this.provider = provider;
+    this._idpchannel = new IdpProxy(provider, protocol, username);
+  },
+
+  close: function() {
+    this.assertion = null;
+    this.provider = null;
+    if (this._idpchannel) {
+      this._idpchannel.close();
+      this._idpchannel = null;
+    }
+  },
+
+  _getFingerprintFromSdp: function(sdp) {
+    let sections = sdp.split(PeerConnectionIdp._mLinePattern);
+    let attributes = sections.map(function(sect) {
+      let m = sect.match(PeerConnectionIdp._fingerprintPattern);
+      if (m) {
+        let remainder = sect.substring(m.index + m[0].length);
+        if (!remainder.match(PeerConnectionIdp._fingerprintPattern)) {
+          return { algorithm: m[1], digest: m[2] };
+        }
+        this._warning("RTC identity: two fingerprint values in same media " +
+            "section are not supported", null, 0);
+        // we have to return non-falsy here so that a media section doesn't
+        // accidentally fall back to the session-level stuff (which is bad)
+        return "error";
+      }
+      // return undefined unless there is exactly one match
+    }, this);
+
+    let sessionLevel = attributes.shift();
+    attributes = attributes.map(function(sectionLevel) {
+      return sectionLevel || sessionLevel;
+    });
+
+    let first = attributes.shift();
+    function sameAsFirst(attr) {
+      return typeof attr === "object" &&
+      first.algorithm === attr.algorithm &&
+      first.digest === attr.digest;
+    }
+
+    if (typeof first === "object" && attributes.every(sameAsFirst)) {
+      return first;
+    }
+    // undefined!
+  },
+
+  _getIdentityFromSdp: function(sdp) {
+    // we only pull from the session level right now
+    // TODO allow for per-m=-section identity
+    let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern);
+    let sessionLevel = sdp.substring(0, mLineMatch.index);
+    let idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
+    if (idMatch) {
+      let assertion = {};
+      try {
+        assertion = JSON.parse(atob(idMatch[1]));
+      } catch (e) {
+        this._warning("RTC identity: invalid identity assertion: " + e, null, 0);
+      } // for JSON.parse
+      if (typeof assertion.idp === "object" &&
+          typeof assertion.idp.domain === "string" &&
+          typeof assertion.assertion === "string") {
+        return assertion;
+      }
+      this._warning("RTC identity: assertion missing idp/idp.domain/assertion",
+                    null, 0);
+    }
+    // undefined!
+  },
+
+  /**
+   * Queues a task to verify 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.
+   */
+  verifyIdentityFromSDP: function(sdp, callback) {
+    let identity = this._getIdentityFromSdp(sdp);
+    let fingerprint = this._getFingerprintFromSdp(sdp);
+    // it's safe to use the fingerprint from the SDP here,
+    // only because we ensure that there is only one
+    if (!fingerprint || !identity) {
+      callback(null);
+      return;
+    }
+    if (!this._idpchannel) {
+      this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
+    }
+
+    this._verifyIdentity(identity.assertion, fingerprint, callback);
+  },
+
+  /**
+   * Checks that the name in the identity provided by the IdP is OK.
+   *
+   * @param name (string) the name to validate
+   * @returns (string) an error message, iff the name isn't good
+   */
+  _validateName: function(name) {
+    if (typeof name !== "string") {
+      return "name not a string";
+    }
+    let atIdx = name.indexOf("@");
+    if (atIdx > 0) {
+      // 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);
+      }
+      // this really isn't correct for IDN names
+      // Bug 958741 will fix that
+      if (tail.toLowerCase() !== provider.toLowerCase()) {
+        return "name '" + identity.name +
+            "' doesn't match IdP: '" + this.provider + "'";
+      }
+      return null;
+    }
+    return "missing authority in name from IdP";
+  },
+
+  // 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, fingerprint) {
+    let warn = function(message) {
+      this._warning("RTC identity: VERIFY error: " + message, null, 0);
+    }.bind(this);
+
+    try {
+      let contents = JSON.parse(message.contents);
+      if (typeof contents.fingerprint !== "object" ||
+          typeof message.identity !== "object") {
+        warn("fingerprint or identity not objects");
+      } else if (contents.fingerprint.digest !== fingerprint.digest ||
+          contents.fingerprint.algorithm !== fingerprint.algorithm) {
+        warn("fingerprint does not match");
+      } else {
+        let error = this._validateName(message.identity.name);
+        if (error) {
+          warn(error);
+        } else {
+          return true;
+        }
+      }
+    } catch(e) {
+      warn("invalid JSON in content");
+    }
+    return false;
+  },
+
+  /**
+   * Asks the IdP proxy to verify an identity.
+   */
+  _verifyIdentity: function(
+      assertion, fingerprint, callback) {
+    function onVerification(message) {
+      if (!message) {
+        this._warning("RTC identity: verification failure", null, 0);
+        callback(null);
+        return;
+      }
+      if (this._checkVerifyResponse(message, fingerprint)) {
+        callback(message);
+      } else {
+        callback(null);
+      }
+    }
+
+    this._sendToIdp("VERIFY", assertion, onVerification.bind(this));
+  },
+
+  /**
+   * 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.
+   */
+  appendIdentityToSDP: function(
+      sdp, fingerprint, callback) {
+    if (!this._idpchannel) {
+      callback(sdp);
+      return;
+    }
+
+    if (this.assertion) {
+      callback(this.wrapSdp(sdp));
+      return;
+    }
+
+    function onAssertion(assertion) {
+      if (!assertion) {
+        this._warning("RTC identity: assertion generation failure", null, 0);
+        callback(sdp);
+        return;
+      }
+
+      this.assertion = btoa(JSON.stringify(assertion));
+      callback(this.wrapSdp(sdp));
+    }
+
+    this._getIdentityAssertion(fingerprint, onAssertion.bind(this));
+  },
+
+  /**
+   * Inserts an identity assertion into the given SDP.
+   */
+  wrapSdp: 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" +
+      sdp.substring(match.index);
+  },
+
+  getIdentityAssertion: function(
+      fingerprint, callback) {
+    if (!this._idpchannel) {
+      throw new Error("IdP not set");
+    }
+
+    this._getIdentityAssertion(fingerprint, callback);
+  },
+
+  _getIdentityAssertion: function(
+      fingerprint, callback) {
+    let [algorithm, digest] = fingerprint.split(" ");
+    let message = {
+      fingerprint: {
+        algorithm: algorithm,
+        digest: digest
+      }
+    };
+    this._sendToIdp("SIGN", JSON.stringify(message), callback);
+  },
+
+  /**
+   * Packages a message and sends it to the IdP.
+   */
+  _sendToIdp: function(type, message, callback) {
+    let origin = this._win.QueryInterface(Ci.nsIInterfaceRequestor)
+        .getInterface(Ci.nsIWebNavigation)
+        .QueryInterface(Ci.nsIDocShell).chromeEventHandler
+        .ownerDocument
+        .defaultView
+        .gBrowser
+        .currentURI
+        .prePath;
+
+    this._idpchannel.send({
+      type: type,
+      message: message,
+      origin: origin
+    }, this._wrapCallback(callback));
+  },
+
+  /**
+   * 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(callback) {
+    let timeout = this._win.setTimeout(function() {
+      this._warning("RTC identity: IdP timeout for " + this._idpchannel + " " +
+           (this._idpchannel.ready ? "[ready]" : "[not ready]"), null, 0);
+      timeout = null;
+      callback(null);
+    }.bind(this), this._timeout);
+
+    return function(message) {
+      if (!timeout) {
+        return;
+      }
+      this._win.clearTimeout(timeout);
+      timeout = null;
+      var content = null;
+      if (message.type === "SUCCESS") {
+        content = message.message;
+      } else {
+        this._warning("RTC Identity: received response of type '" +
+            message.type + "' from IdP: " + message.message, null, 0);
+      }
+      callback(content);
+    }.bind(this);
+  }
+};
+
+this.PeerConnectionIdp = PeerConnectionIdp;
--- a/dom/media/moz.build
+++ b/dom/media/moz.build
@@ -39,16 +39,17 @@ EXTRA_COMPONENTS += [
     'PeerConnection.js',
     'PeerConnection.manifest',
 ]
 
 JS_MODULES_PATH = 'modules/media'
 
 EXTRA_JS_MODULES += [
     'IdpProxy.jsm',
+    'PeerConnectionIdp.jsm',
 ]
 
 if CONFIG['MOZ_B2G']:
     EXPORTS.mozilla += [
         'MediaPermissionGonk.h',
     ]
     SOURCES += [
         'MediaPermissionGonk.cpp',
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -47,9 +47,12 @@ skip-if = os == 'mac'
 [test_peerConnection_setLocalAnswerInHaveLocalOffer.html]
 [test_peerConnection_setLocalAnswerInStable.html]
 [test_peerConnection_setLocalOfferInHaveRemoteOffer.html]
 [test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html]
 [test_peerConnection_setRemoteAnswerInStable.html]
 [test_peerConnection_setRemoteOfferInHaveLocalOffer.html]
 [test_peerConnection_throwInCallbacks.html]
 [test_peerConnection_toJSON.html]
+[test_setIdentityProvider.html]
+[test_setIdentityProviderWithErrors.html]
+[test_getIdentityAssertion.html]
 [test_idpproxy.html]
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -430,16 +430,21 @@ function PCT_createAnswer(peer, onSucces
  */
 PeerConnectionTest.prototype.createOffer =
 function PCT_createOffer(peer, onSuccess) {
   peer.createOffer(function (offer) {
     onSuccess(offer);
   });
 };
 
+PeerConnectionTest.prototype.setIdentityProvider =
+function(peer, provider, protocol, identity) {
+  peer.setIdentityProvider(provider, protocol, identity);
+};
+
 /**
  * Sets the local description for the specified peer connection instance
  * and automatically handles the failure case.
  *
  * @param {PeerConnectionWrapper} peer
           The peer connection wrapper to run the command on
  * @param {mozRTCSessionDescription} desc
  *        Session description for the local description request
@@ -665,17 +670,17 @@ DataChannelTest.prototype = Object.creat
           // externally negotiated - we need to open from both ends
           options.id = options.id || channel.id;  // allow for no id to let the impl choose
           self.pcRemote.createDataChannel(options, function (channel) {
             remoteChannel = channel;
             check_next_test();
           });
         } else {
           check_next_test();
-	}
+        }
       });
     }
   },
 
   send : {
     /**
      * Send data (message or blob) to the other peer
      *
@@ -1104,16 +1109,20 @@ PeerConnectionWrapper.prototype = {
    * Returns the ICE connection state.
    *
    * @returns {object} The local description
    */
   get iceConnectionState() {
     return this._pc.iceConnectionState;
   },
 
+  setIdentityProvider: function(provider, protocol, identity) {
+      this._pc.setIdentityProvider(provider, protocol, identity);
+  },
+
   /**
    * Callback when we get media from either side. Also an appropriate
    * HTML media element will be created.
    *
    * @param {MediaStream} stream
    *        Media stream to handle
    * @param {string} type
    *        The type of media stream ('audio' or 'video')
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getIdentityAssertion.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="head.js"></script>
+  <script type="application/javascript" src="pc.js"></script>
+  <script type="application/javascript" src="templates.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    title: "getIdentityAssertion Tests"
+  });
+
+var test;
+function theTest() {
+  test = new PeerConnectionTest();
+  test.setMediaConstraints([{audio: true}], [{audio: true}]);
+  test.chain.append([
+  [
+    "GET_IDENTITY_ASSERTION_FAILS_WITHOUT_PROVIDER",
+    function(test) {
+      test.pcLocal._pc.getIdentityAssertion(function(err) {
+        ok(err, "getIdentityAssertion must fail without provider");
+        test.next();
+      });
+    },
+  ],
+  [
+    "GET_IDENTITY_ASSERTION_FIRES_EVENTUALLY_AND_SUBSEQUENTLY",
+    function(test) {
+      var fired = 0;
+      test.setIdentityProvider(test.pcLocal, 'example.com', 'idp.html', 'nobody');
+      test.pcLocal._pc.onidentityresult = function() {
+        fired++;
+        if (fired == 1) {
+          ok(true, "identityresult fired");
+        } else if (fired == 2) {
+          ok(true, "identityresult fired 2x");
+          test.next();
+        }
+      };
+      test.pcLocal._pc.getIdentityAssertion();
+      test.pcLocal._pc.getIdentityAssertion();
+    }
+  ],
+  [
+    "GET_IDENTITY_ASSERTION_FAILS",
+    function(test) {
+      test.setIdentityProvider(test.pcLocal, 'example.com', 'idp.html#error');
+      test.pcLocal._pc.onidentityresult = function(e) {
+       ok(false, "Should not get an identity result");
+        test.next();
+      };
+      test.pcLocal._pc.getIdentityAssertion(function(err) {
+        ok(err, "Got error callback from getIdentityAssertion");
+        test.next();
+      });
+    }
+  ],
+  [
+    "GET_IDENTITY_ASSERTION_IDP_NOT_READY",
+    function(test) {
+      test.setIdentityProvider(test.pcLocal, 'example.com', 'idp.html#error:ready');
+      test.pcLocal._pc.onidentityresult = function(e) {
+        ok(false, "Should not get an identity result");
+        test.next();
+      };
+      test.pcLocal._pc.getIdentityAssertion(function(err) {
+        ok(err, "Got error callback from getIdentityAssertion");
+        test.next();
+      });
+    }
+  ]
+  ]);
+  test.run();
+}
+runTest(theTest);
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_setIdentityProvider.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8"/>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="head.js"></script>
+  <script type="application/javascript" src="pc.js"></script>
+  <script type="application/javascript" src="templates.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    title: "setIdentityProvider leads to peerIdentity and assertions in SDP"
+  });
+
+var test;
+function theTest() {
+  test = new PeerConnectionTest();
+  test.setMediaConstraints([{audio: true}], [{audio: true}]);
+  test.setIdentityProvider(test.pcLocal, 'test1.example.com', 'idp.html', 'nobody');
+  test.setIdentityProvider(test.pcRemote, 'test2.example.com', 'idp.html', 'nobody');
+  test.chain.append([
+  [
+    "PEER_IDENTITY_IS_SET_CORRECTLY",
+    function(test) {
+      var outstanding = 0;
+      // we have to wait for the identity result in order to get the actual
+      // identity information, since the call will complete before the identity
+      // provider has a chance to finish verifying... that's OK, but it makes
+      // testing more difficult
+      function checkOrSetupCheck(pc, pfx, idp, name) {
+        if (pc.peerIdentity) {
+          is(pc.peerIdentity.idp, idp, pfx + "IdP is correct");
+          is(pc.peerIdentity.name, name + "@" + idp, pfx + "identity is correct");
+        } else {
+          ++outstanding;
+          pc.onidentityresult = function checkIdentity(e) {
+            console.log("Got result", e, e.assertion, e.peerIdentity);
+            if (e.assertion) {
+              return;
+            }
+            ok(e.peerIdentity, pfx + "identity result contains identity");
+            is(e.peerIdentity, e.target.peerIdentity, pfx + "identity result is on PC");
+            is(e.peerIdentity.idp, idp, pfx + "IdP is correct");
+            is(e.peerIdentity.name, name + "@" + idp, pfx + "identity is correct");
+            --outstanding;
+            if (outstanding <= 0) {
+              test.next();
+            }
+          };
+        }
+      }
+
+      checkOrSetupCheck(test.pcLocal._pc, "local: ", "test2.example.com", "someone");
+      checkOrSetupCheck(test.pcRemote._pc, "remote: ", "test1.example.com", "someone");
+      if (outstanding <= 0) {
+        test.next();
+      }
+    }
+  ],
+  [
+    "OFFERS_AND_ANSWERS_INCLUDE_IDENTITY",
+    function(test) {
+      ok(test.pcLocal._last_offer.sdp.contains("a=identity"), "a=identity is in the offer SDP");
+      ok(test.pcRemote._last_answer.sdp.contains("a=identity"), "a=identity is in the answer SDP");
+      test.next();
+    }
+  ],
+  [
+    "DESCRIPTIONS_CONTAIN_IDENTITY",
+    function(test) {
+      ok(test.pcLocal.localDescription.sdp.contains("a=identity"),
+                         "a=identity is in the local copy of the offer");
+      ok(test.pcRemote.localDescription.sdp.contains("a=identity"),
+                         "a=identity is in the remote copy of the offer");
+      ok(test.pcLocal.remoteDescription.sdp.contains("a=identity"),
+                         "a=identity is in the local copy of the answer");
+      ok(test.pcRemote.remoteDescription.sdp.contains("a=identity"),
+                         "a=identity is in the remote copy of the answer");
+      test.next();
+    }
+  ]
+  ]);
+  test.run();
+}
+runTest(theTest);
+
+
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_setIdentityProviderWithErrors.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="head.js"></script>
+  <script type="application/javascript" src="pc.js"></script>
+  <script type="application/javascript" src="templates.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    title: "Identity Provider returning errors is handled correctly"
+  });
+
+var test;
+runTest(function () {
+  test = new PeerConnectionTest();
+  test.setMediaConstraints([{audio: true}], [{audio: true}]);
+  // first example generates an error
+  test.setIdentityProvider(test.pcLocal, 'example.com', 'idp.html#error', 'nobody');
+  // first doesn't even get a ready message from the IdP - results in a timeout
+  test.setIdentityProvider(test.pcRemote, 'example.com', 'idp.html#error:ready', 'nobody');
+  test.chain.append([
+  [
+    "PEER_IDENTITY_IS_EMPTY",
+    function(test) {
+      ok(!test.pcLocal._pc.peerIdentity, "local peerIdentity is not set");
+      ok(!test.pcRemote._pc.peerIdentity, "remote peerIdentity is not set");
+      test.next();
+    }
+  ],
+  [
+    "OFFERS_AND_ANSWERS_DONT_INCLUDE_IDENTITY",
+    function(test) {
+      ok(!test.pcLocal._last_offer.sdp.contains("a=identity"), "a=identity not contained in the offer SDP");
+      ok(!test.pcRemote._last_answer.sdp.contains("a=identity"), "a=identity not contained in the answer SDP");
+      test.next();
+    }
+  ],
+  ]);
+  test.run();
+});
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -437,16 +437,17 @@ var interfaceNamesInGlobalScope =
     {name: "Promise", b2g: false, release: false},
     "PropertyNodeList",
     "Range",
     "RecordErrorEvent",
     "Rect",
     "RGBColor",
     {name: "RTCDataChannelEvent", pref: "media.peerconnection.enabled"},
     {name: "RTCPeerConnectionIceEvent", pref: "media.peerconnection.enabled"},
+    {name: "RTCPeerConnectionIdentityEvent", pref: "media.peerconnection.identity.enabled"},
     {name: "RTCStatsReport", pref: "media.peerconnection.enabled"},
     "Screen",
     "ScriptProcessorNode",
     "ScrollAreaEvent",
     "Selection",
     "SettingsLock",
     "SettingsManager",
     {name: "ShadowRoot", pref: "dom.webcomponents.enabled"},
new file mode 100644
--- /dev/null
+++ b/dom/webidl/RTCIdentityAssertion.webidl
@@ -0,0 +1,16 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ *
+ * The origin of this IDL file is
+ * http://www.w3.org/TR/2013/WD-webrtc-20130910/#idl-def-RTCIdentityAssertion
+ */
+
+[Pref="media.peerconnection.identity.enabled",
+ JSImplementation="@mozilla.org/dom/rtcidentityassertion;1",
+ Constructor(DOMString idp, DOMString name)]
+interface RTCIdentityAssertion {
+  attribute DOMString idp;
+  attribute DOMString name;
+};
--- a/dom/webidl/RTCPeerConnection.webidl
+++ b/dom/webidl/RTCPeerConnection.webidl
@@ -82,16 +82,22 @@ dictionary MediaConstraintsInternal {
 interface RTCDataChannel;
 
 [Pref="media.peerconnection.enabled",
  JSImplementation="@mozilla.org/dom/peerconnection;1",
  Constructor (optional RTCConfiguration configuration,
               optional object? constraints)]
 // moz-prefixed until sufficiently standardized.
 interface mozRTCPeerConnection : EventTarget  {
+  [Pref="media.peerconnection.identity.enabled"]
+  void setIdentityProvider (DOMString provider,
+                            optional DOMString protocol,
+                            optional DOMString username);
+  [Pref="media.peerconnection.identity.enabled"]
+  void getIdentityAssertion(optional RTCPeerConnectionErrorCallback failureCallback);
   void createOffer (RTCSessionDescriptionCallback successCallback,
                     RTCPeerConnectionErrorCallback failureCallback,
                     optional MediaConstraints constraints);
   void createAnswer (RTCSessionDescriptionCallback successCallback,
                      RTCPeerConnectionErrorCallback failureCallback,
                      optional MediaConstraints constraints);
   void setLocalDescription (mozRTCSessionDescription description,
                             optional VoidFunction successCallback,
@@ -104,16 +110,19 @@ interface mozRTCPeerConnection : EventTa
   readonly attribute RTCSignalingState signalingState;
   void updateIce (optional RTCConfiguration configuration,
                   optional MediaConstraints constraints);
   void addIceCandidate (mozRTCIceCandidate candidate,
                         optional VoidFunction successCallback,
                         optional RTCPeerConnectionErrorCallback failureCallback);
   readonly attribute RTCIceGatheringState iceGatheringState;
   readonly attribute RTCIceConnectionState iceConnectionState;
+  [Pref="media.peerconnection.identity.enabled"]
+  readonly attribute RTCIdentityAssertion? peerIdentity;
+
   sequence<MediaStream> getLocalStreams ();
   sequence<MediaStream> getRemoteStreams ();
   MediaStream? getStreamById (DOMString streamId);
   void addStream (MediaStream stream, optional MediaConstraints constraints);
   void removeStream (MediaStream stream);
   void close ();
   attribute EventHandler onnegotiationneeded;
   attribute EventHandler onicecandidate;
@@ -132,25 +141,24 @@ interface mozRTCPeerConnection : EventTa
                          RTCPeerConnectionErrorCallback failureCallback);
 
   // Data channel.
   RTCDataChannel createDataChannel (DOMString label,
                                     optional RTCDataChannelInit dataChannelDict);
   attribute EventHandler ondatachannel;
   attribute EventHandler onconnection;
   attribute EventHandler onclosedconnection;
+  [Pref="media.peerconnection.identity.enabled"]
+  attribute EventHandler onidentityresult;
 };
 
 callback RTCLogCallback = void (sequence<DOMString> logMessages);
 
 [JSImplementation="@mozilla.org/dom/webrtcglobalinformation;1",
  ChromeOnly,
  Constructor ()]
 interface WebrtcGlobalInformation {
     void getAllStats(RTCStatsCallback callback,
                      RTCPeerConnectionErrorCallback errorCallback);
     void getCandPairLogs(DOMString candPairId,
                          RTCLogCallback callback,
                          RTCPeerConnectionErrorCallback errorCallback);
 };
-
-
-
new file mode 100644
--- /dev/null
+++ b/dom/webidl/RTCPeerConnectionIdentityEvent.webidl
@@ -0,0 +1,21 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ *
+ * Proposed only, not in spec yet:
+ * http://lists.w3.org/Archives/Public/public-webrtc/2013Dec/0104.html
+ */
+
+dictionary RTCPeerConnectionIdentityEventInit : EventInit {
+  RTCIdentityAssertion? peerIdentity = null;
+  DOMString? assertion = null;
+};
+
+[Pref="media.peerconnection.identity.enabled",
+ Constructor(DOMString type,
+             optional RTCPeerConnectionIdentityEventInit eventInitDict)]
+interface RTCPeerConnectionIdentityEvent : Event {
+  readonly attribute RTCIdentityAssertion? peerIdentity;
+  readonly attribute DOMString? assertion;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -265,16 +265,17 @@ WEBIDL_FILES = [
     'ProcessingInstruction.webidl',
     'Promise.webidl',
     'PushManager.webidl',
     'Range.webidl',
     'Rect.webidl',
     'RGBColor.webidl',
     'RTCConfiguration.webidl',
     'RTCIceCandidate.webidl',
+    'RTCIdentityAssertion.webidl',
     'RTCPeerConnection.webidl',
     'RTCSessionDescription.webidl',
     'RTCStatsReport.webidl',
     'Screen.webidl',
     'ScriptProcessorNode.webidl',
     'ScrollAreaEvent.webidl',
     'Selection.webidl',
     'SettingsManager.webidl',
@@ -565,16 +566,17 @@ GENERATED_EVENTS_WEBIDL_FILES = [
     'MediaStreamEvent.webidl',
     'MozContactChangeEvent.webidl',
     'MozEmergencyCbModeEvent.webidl',
     'MozInterAppMessageEvent.webidl',
     'MozOtaStatusEvent.webidl',
     'MozStkCommandEvent.webidl',
     'RTCDataChannelEvent.webidl',
     'RTCPeerConnectionIceEvent.webidl',
+    'RTCPeerConnectionIdentityEvent.webidl',
     'TrackEvent.webidl',
     'UserProximityEvent.webidl',
     'USSDReceivedEvent.webidl',
 ]
 
 if CONFIG['MOZ_GAMEPAD']:
     GENERATED_EVENTS_WEBIDL_FILES += [
         'GamepadAxisMoveEvent.webidl',
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -238,16 +238,21 @@ pref("media.peerconnection.enabled", tru
 pref("media.peerconnection.video.enabled", true);
 pref("media.navigator.video.max_fs", 0); // unrestricted
 pref("media.navigator.video.max_fr", 0); // unrestricted
 #endif
 pref("media.navigator.permission.disabled", false);
 pref("media.peerconnection.default_iceservers", "[{\"url\": \"stun:stun.services.mozilla.com\"}]");
 pref("media.peerconnection.trickle_ice", true);
 pref("media.peerconnection.use_document_iceservers", true);
+// Do not enable identity before ensuring that the UX cannot be spoofed
+// see Bug 884573 for details
+// Do not enable identity before fixing domain comparison: see Bug 958741
+pref("media.peerconnection.identity.enabled", false);
+pref("media.peerconnection.identity.timeout", 5000);
 // These values (aec, agc, and noice) are from media/webrtc/trunk/webrtc/common_types.h
 // kXxxUnchanged = 0, kXxxDefault = 1, and higher values are specific to each 
 // setting (for Xxx = Ec, Agc, or Ns).  Defaults are all set to kXxxDefault here.
 pref("media.peerconnection.turn.disable", false);
 pref("media.peerconnection.aec_enabled", true);
 pref("media.peerconnection.aec", 1);
 pref("media.peerconnection.agc_enabled", false);
 pref("media.peerconnection.agc", 1);