Bug 884573 - Part 2: Identity assertion generation and verification for WebRTC. r=abr
authorMartin Thomson <martin.thomson@gmail.com>
Mon, 10 Feb 2014 14:41:46 -0800
changeset 170555 dad774c849bbd9a78b70b2f6b6db913fc9398431
parent 170554 e633c972f2264f01ec6a7768d92e7cee6c5c33e8
child 170556 9e0e649f4e060ab76d4046e33a891ee130fe8542
push id26291
push userkwierso@gmail.com
push dateWed, 26 Feb 2014 04:10:11 +0000
treeherdermozilla-central@626d99c084cb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersabr
bugs884573
milestone30.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 884573 - Part 2: Identity assertion generation and verification for WebRTC. r=abr
dom/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/identity/mochitest.ini
dom/media/tests/identity/test_getIdentityAssertion.html
dom/media/tests/identity/test_idpproxy.html
dom/media/tests/identity/test_setIdentityProvider.html
dom/media/tests/identity/test_setIdentityProviderWithErrors.html
dom/media/tests/mochitest/pc.js
dom/webidl/RTCIdentityAssertion.webidl
dom/webidl/RTCPeerConnection.webidl
dom/webidl/RTCPeerConnectionIdentityEvent.webidl
dom/webidl/moz.build
media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
modules/libpref/src/init/all.js
--- a/dom/events/test/test_all_synthetic_events.html
+++ b/dom/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,32 @@ IdpProxy.prototype = {
   /**
    * Performs cleanup.  The object should be OK to use again.
    */
   close: function() {
     if (!this.channel) {
       return;
     }
 
-    // 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);
-    }, this);
-    this.pending.forEach(function(p) {
-      p.callback(error);
-    }, this);
+    // 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", message: "IdP closed" };
+    Object.keys(trackingCopy).forEach(function(k) {
+      this.trackingCopy[k](error);
+    }, this);
+    pendingCopy.forEach(function(p) {
+      p.callback(error);
+    }, this);
   },
 
   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) {
@@ -286,16 +291,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;
@@ -306,24 +327,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",
@@ -351,53 +373,64 @@ 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.makeGetterSetterEH("onpeeridentity");
 
     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);
@@ -476,18 +509,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) {
@@ -595,17 +628,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) {
 
@@ -618,17 +651,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");
@@ -666,17 +699,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;
@@ -685,28 +718,88 @@ 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 processIdentity = this._processIdentity.bind(this);
+      this._remoteIdp.verifyIdentityFromSDP(desc.sdp, processIdentity);
+    } catch (e) {
+      this.reportWarning(e.message, e.fileName, e.lineNumber);
+      // only happens if processing the SDP for identity doesn't work
+      // let _setRemoteDescription do the error reporting
+    }
+
     this._queueOrRun({
       func: this._setRemoteDescription,
       args: [type, desc.sdp, onSuccess, onError],
       wait: true,
       type: desc.type
     });
   },
 
+  _processIdentity: function(message) {
+    if (message) {
+      this._peerIdentity = new this._win.RTCIdentityAssertion(
+          this._remoteIdp.provider, message.identity.name);
+
+      let args = { peerIdentity: this._peerIdentity };
+      this.dispatchEvent(new this._win.Event("peeridentity"));
+    }
+  },
+
   _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);
+  },
+
+  _gotIdentityAssertion: function(assertion){
+    let args = { assertion: assertion };
+    let ev = new this._win.RTCPeerConnectionIdentityEvent("identityresult", args);
+    this.dispatchEvent(ev);
+  },
+
+  // 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) {
+        this._gotIdentityAssertion(assertion);
+      } 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) {
@@ -715,19 +808,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");
@@ -735,86 +828,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;
@@ -836,45 +934,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;
@@ -899,17 +998,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) {
@@ -919,17 +1018,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" };
 }
@@ -978,32 +1077,46 @@ 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) {
+      if (assertion) {
+        pc._gotIdentityAssertion(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) {
+      if (assertion) {
+        pc._gotIdentityAssertion(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() {
@@ -1237,11 +1350,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,336 @@
+/* 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.assertion);
+    }
+
+    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) {
+    // this is not secure
+    // but there are no good alternatives until bug 968335 lands
+    // when that happens, change this to use the new mechanism
+    let origin = this._win.document.nodePrincipal.origin;
+
+    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/identity/mochitest.ini
+++ b/dom/media/tests/identity/mochitest.ini
@@ -4,10 +4,16 @@ support-files =
   /.well-known/idp-proxy/idp-proxy.js
 
 # All tests are disabled on android due to lack of https support in mochitest
 # (Bug 975149)
 # All tests are disabled on b2g due to lack of e10s support in WebRTC identity
 # (Bug 975144)
 [test_idpproxy.html]
 skip-if = os == "android" || appname == "b2g"
+[test_getIdentityAssertion.html]
+skip-if = os == "android" || appname == "b2g"
+[test_setIdentityProvider.html]
+skip-if = os == "android" || appname == "b2g"
+[test_setIdentityProviderWithErrors.html]
+skip-if = os == "android" || appname == "b2g"
 [../mochitest/test_zmedia_cleanup.html]
 skip-if = os == "android" || appname == "b2g"
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/identity/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="../mochitest/head.js"></script>
+  <script type="application/javascript" src="../mochitest/pc.js"></script>
+  <script type="application/javascript" src="../mochitest/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>
--- a/dom/media/tests/identity/test_idpproxy.html
+++ b/dom/media/tests/identity/test_idpproxy.html
@@ -46,20 +46,22 @@ function test_protocol_sandbox(done) {
       var str = (typeof proto === "string") ? proto : typeof proto;
       ok(true, "Evil protocol '" + proto + "' raises exception");
     }
   });
   done();
 }
 
 function handleFailure(done) {
-  return function failure(error) {
+  function failure(error) {
     ok(false, "IdP error" + error);
     done();
-  };
+  }
+  setTimeout(failure, 5000);
+  return failure;
 }
 
 function test_success_response(done) {
   var idp;
   var failure = handleFailure(done);
   var timeout = setTimeout(failure, 5000);;
 
   function handleResponse(response) {
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/identity/test_setIdentityProvider.html
@@ -0,0 +1,100 @@
+<!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="../mochitest/head.js"></script>
+  <script type="application/javascript" src="../mochitest/pc.js"></script>
+  <script type="application/javascript" src="../mochitest/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", "someone");
+  test.setIdentityProvider(test.pcRemote, "test2.example.com", "idp.html", "someone");
+  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) {
+        function checkIdentity() {
+          is(pc.peerIdentity.idp, idp, pfx + "IdP is correct");
+          is(pc.peerIdentity.name, name + "@" + idp, pfx + "identity is correct");
+        }
+        if (pc.peerIdentity) {
+          info(pfx + "peerIdentity already set");
+          checkIdentity();
+        } else {
+          ++outstanding;
+          info(pfx + "setting onpeeridentity handler");
+          pc.onpeeridentity = function checkIdentityEvent(e) {
+            info(pfx + "checking peerIdentity");
+            checkIdentity();
+            --outstanding;
+            if (outstanding <= 0) {
+              test.next();
+            }
+          };
+        }
+      }
+
+      setTimeout(function() {
+        ok(false, "Timed out waiting for peerIdentity.");
+        test.next();
+      }, 5000); // probably should be something in the base harness for this
+
+      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/identity/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="../mochitest/head.js"></script>
+  <script type="application/javascript" src="../mochitest/pc.js"></script>
+  <script type="application/javascript" src="../mochitest/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/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -525,16 +525,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
@@ -760,17 +765,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
      *
@@ -1205,16 +1210,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/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,16 +141,20 @@ 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;
+  [Pref="media.peerconnection.identity.enabled"]
+  attribute EventHandler onpeeridentity;
 };
 
 callback RTCLogCallback = void (sequence<DOMString> logMessages);
 
 [JSImplementation="@mozilla.org/dom/webrtcglobalinformation;1",
  ChromeOnly,
  Constructor ()]
 interface WebrtcGlobalInformation {
@@ -149,11 +162,8 @@ interface WebrtcGlobalInformation {
                      RTCPeerConnectionErrorCallback errorCallback);
     void getCandPairLogs(DOMString candPairId,
                          RTCLogCallback callback,
                          RTCPeerConnectionErrorCallback errorCallback);
     void getLogs(DOMString pattern,
                  RTCLogCallback callback,
                  RTCPeerConnectionErrorCallback errorCallback);
 };
-
-
-
new file mode 100644
--- /dev/null
+++ b/dom/webidl/RTCPeerConnectionIdentityEvent.webidl
@@ -0,0 +1,19 @@
+/* -*- 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 {
+  DOMString? assertion = null;
+};
+
+[ChromeOnly,
+ Constructor(DOMString type,
+             optional RTCPeerConnectionIdentityEventInit eventInitDict)]
+interface RTCPeerConnectionIdentityEvent : Event {
+  readonly attribute DOMString? assertion;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -269,16 +269,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',
@@ -581,16 +582,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/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
@@ -200,30 +200,30 @@ public:
   PeerConnectionObserverDispatch(CSF::CC_CallInfoPtr aInfo,
                                  nsRefPtr<PeerConnectionImpl> aPC,
                                  PeerConnectionObserver* aObserver)
       : mPC(aPC),
         mObserver(aObserver),
         mCode(static_cast<PeerConnectionImpl::Error>(aInfo->getStatusCode())),
         mReason(aInfo->getStatus()),
         mSdpStr(),
-	mCandidateStr(),
+        mCandidateStr(),
         mCallState(aInfo->getCallState()),
         mFsmState(aInfo->getFsmState()),
         mStateStr(aInfo->callStateToString(mCallState)),
         mFsmStateStr(aInfo->fsmStateToString(mFsmState)) {
     if (mCallState == REMOTESTREAMADD) {
       MediaStreamTable *streams = nullptr;
       streams = aInfo->getMediaStreams();
       mRemoteStream = mPC->media()->GetRemoteStream(streams->media_stream_id);
       MOZ_ASSERT(mRemoteStream);
     } else if (mCallState == FOUNDICECANDIDATE) {
-	mCandidateStr = aInfo->getCandidate();
+        mCandidateStr = aInfo->getCandidate();
     } else if ((mCallState == CREATEOFFERSUCCESS) ||
-	       (mCallState == CREATEANSWERSUCCESS)) {
+               (mCallState == CREATEANSWERSUCCESS)) {
         mSdpStr = aInfo->getSDP();
     }
   }
 
   ~PeerConnectionObserverDispatch(){}
 
 #ifdef MOZILLA_INTERNAL_API
   class TracksAvailableCallback : public DOMMediaStream::OnTracksAvailableCallback
@@ -1602,20 +1602,24 @@ PeerConnectionImpl::IceGatheringState(PC
 
 nsresult
 PeerConnectionImpl::CheckApiState(bool assert_ice_ready) const
 {
   PC_AUTO_ENTER_API_CALL_NO_CHECK();
   MOZ_ASSERT(mTrickle || !assert_ice_ready ||
              (mIceGatheringState == PCImplIceGatheringState::Complete));
 
-  if (mReadyState == PCImplReadyState::Closed)
+  if (mReadyState == PCImplReadyState::Closed) {
+    CSFLogError(logTag, "%s: called API while closed", __FUNCTION__);
     return NS_ERROR_FAILURE;
-  if (!mMedia)
+  }
+  if (!mMedia) {
+    CSFLogError(logTag, "%s: called API with disposed mMedia", __FUNCTION__);
     return NS_ERROR_FAILURE;
+  }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PeerConnectionImpl::Close()
 {
   CSFLogDebug(logTag, "%s: for %s", __FUNCTION__, mHandle.c_str());
   PC_AUTO_ENTER_API_CALL_NO_CHECK();
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -242,16 +242,22 @@ 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
+// Do not enable identity before fixing origin spoofing: see Bug 968335
+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);