Bug 975144 - Rework RTC identity to use JS sandbox, r=jib

This commit is contained in:
Martin Thomson 2015-02-22 10:57:20 +13:00
parent 63540e94d0
commit c24d8aeadd
5 changed files with 505 additions and 543 deletions

View File

@ -1,272 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["IdpProxy"];
const {
classes: Cc,
interfaces: Ci,
utils: Cu,
results: Cr
} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Sandbox",
"resource://gre/modules/identity/Sandbox.jsm");
/**
* An invisible iframe for hosting the idp shim.
*
* There is no visible UX here, as we assume the user has already
* logged in elsewhere (on a different screen in the web site hosting
* the RTC functions).
*/
function IdpChannel(uri, messageCallback) {
this.sandbox = null;
this.messagechannel = null;
this.source = uri;
this.messageCallback = messageCallback;
}
IdpChannel.prototype = {
/**
* Create a hidden, sandboxed iframe for hosting the IdP's js shim.
*
* @param callback
* (function) invoked when this completes, with an error
* argument if there is a problem, no argument if everything is
* ok
*/
open: function(callback) {
if (this.sandbox) {
return callback(new Error("IdP channel already open"));
}
let ready = this._sandboxReady.bind(this, callback);
this.sandbox = new Sandbox(this.source, ready);
},
_sandboxReady: function(aCallback, aSandbox) {
// Inject a message channel into the subframe.
try {
this.messagechannel = new aSandbox._frame.contentWindow.MessageChannel();
Object.defineProperty(
aSandbox._frame.contentWindow.wrappedJSObject,
"rtcwebIdentityPort",
{
value: this.messagechannel.port2,
configurable: true
}
);
} catch (e) {
this.close();
aCallback(e); // oops, the IdP proxy overwrote this.. bad
return;
}
this.messagechannel.port1.onmessage = function(msg) {
this.messageCallback(msg.data);
}.bind(this);
this.messagechannel.port1.start();
aCallback();
},
send: function(msg) {
this.messagechannel.port1.postMessage(msg);
},
close: function IdpChannel_close() {
if (this.sandbox) {
if (this.messagechannel) {
this.messagechannel.port1.close();
}
this.sandbox.free();
}
this.messagechannel = null;
this.sandbox = null;
}
};
/**
* A message channel between the RTC PeerConnection and a designated IdP Proxy.
*
* @param domain (string) the domain to load up
* @param protocol (string) Optional string for the IdP protocol
*/
function IdpProxy(domain, protocol) {
IdpProxy.validateDomain(domain);
IdpProxy.validateProtocol(protocol);
this.domain = domain;
this.protocol = protocol || "default";
this._reset();
}
/**
* Checks that the domain is only a domain, and doesn't contain anything else.
* Adds it to a URI, then checks that it matches perfectly.
*/
IdpProxy.validateDomain = function(domain) {
let message = "Invalid domain for identity provider; ";
if (!domain || typeof domain !== "string") {
throw new Error(message + "must be a non-zero length string");
}
message += "must only have a domain name and optionally a port";
try {
let ioService = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
let uri = ioService.newURI('https://' + domain + '/', null, null);
// this should trap errors
// we could check uri.userPass, uri.path and uri.ref, but there is no need
if (uri.hostPort !== domain) {
throw new Error(message);
}
} catch (e if (e.result === Cr.NS_ERROR_MALFORMED_URI)) {
throw new Error(message);
}
};
/**
* Checks that the IdP protocol is sane. In particular, we don't want someone
* adding relative paths (e.g., "../../myuri"), which could be used to move
* outside of /.well-known/ and into space that they control.
*/
IdpProxy.validateProtocol = function(protocol) {
if (!protocol) {
return; // falsy values turn into "default", so they are OK
}
let message = "Invalid protocol for identity provider; ";
if (typeof protocol !== "string") {
throw new Error(message + "must be a string");
}
if (decodeURIComponent(protocol).match(/[\/\\]/)) {
throw new Error(message + "must not include '/' or '\\'");
}
};
IdpProxy.prototype = {
_reset: function() {
this.channel = null;
this.ready = false;
this.counter = 0;
this.tracking = {};
this.pending = [];
},
isSame: function(domain, protocol) {
return this.domain === domain && ((protocol || "default") === this.protocol);
},
/**
* Get a sandboxed iframe for hosting the idp-proxy's js. Create a message
* channel down to the frame.
*
* @param errorCallback (function) a callback that will be invoked if there
* is a fatal error starting the proxy
*/
start: function(errorCallback) {
if (this.channel) {
return;
}
let well_known = "https://" + this.domain;
well_known += "/.well-known/idp-proxy/" + this.protocol;
this.channel = new IdpChannel(well_known, this._messageReceived.bind(this));
this.channel.open(function(error) {
if (error) {
this.close();
if (typeof errorCallback === "function") {
errorCallback(error);
}
}
}.bind(this));
},
/**
* Send a message up to the idp proxy. This should be an RTC "SIGN" or
* "VERIFY" message. This method adds the tracking 'id' parameter
* automatically to the message so that the callback is only invoked for the
* response to the message.
*
* This enqueues the message to send if the IdP hasn't signaled that it is
* "READY", and sends the message when it is.
*
* The caller is responsible for ensuring that a response is received. If the
* IdP doesn't respond, the callback simply isn't invoked.
*/
send: function(message, callback) {
this.start();
if (this.ready) {
message.id = "" + (++this.counter);
this.tracking[message.id] = callback;
this.channel.send(message);
} else {
this.pending.push({ message: message, callback: callback });
}
},
/**
* Handle a message from the IdP. This automatically sends if the message is
* 'READY' so there is no need to track readiness state outside of this obj.
*/
_messageReceived: function(message) {
if (!message) {
return;
}
if (!this.ready && message.type === "READY") {
this.ready = true;
this.pending.forEach(function(p) {
this.send(p.message, p.callback);
}, this);
this.pending = [];
} else if (this.tracking[message.id]) {
var callback = this.tracking[message.id];
delete this.tracking[message.id];
callback(message);
} else {
let console = Cc["@mozilla.org/consoleservice;1"].
getService(Ci.nsIConsoleService);
console.logStringMessage("Received bad message from IdP: " +
message.id + ":" + message.type);
}
},
/**
* Performs cleanup. The object should be OK to use again.
*/
close: function() {
if (!this.channel) {
return;
}
// clear out before letting others know in case they do something bad
let trackingCopy = this.tracking;
let pendingCopy = this.pending;
this.channel.close();
this._reset();
// dump a message of type "ERROR" in response to all outstanding
// messages to the IdP
let error = { type: "ERROR", error: "IdP closed" };
Object.keys(trackingCopy).forEach(function(k) {
trackingCopy[k](error);
});
pendingCopy.forEach(function(p) {
p.callback(error);
});
},
toString: function() {
return this.domain + '/.../' + this.protocol;
}
};
this.IdpProxy = IdpProxy;

238
dom/media/IdpSandbox.jsm Normal file
View File

@ -0,0 +1,238 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const {
classes: Cc,
interfaces: Ci,
utils: Cu,
results: Cr
} = Components;
Cu.import('resource://gre/modules/Services.jsm');
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
/** This little class ensures that redirects maintain an https:// origin */
function RedirectHttpsOnly() {}
RedirectHttpsOnly.prototype = {
asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
if (newChannel.URI.scheme !== 'https') {
callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT);
} else {
callback.onRedirectVerifyCallback(Cr.NS_OK);
}
},
getInterface: function(iid) {
return this.QueryInterface(iid);
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink])
};
/** This class loads a resource into a single string. ResourceLoader.load() is
* the entry point. */
function ResourceLoader(res, rej) {
this.resolve = res;
this.reject = rej;
this.data = '';
}
/** Loads the identified https:// URL. */
ResourceLoader.load = function(uri) {
return new Promise((resolve, reject) => {
let listener = new ResourceLoader(resolve, reject);
let ioService = Cc['@mozilla.org/network/io-service;1']
.getService(Ci.nsIIOService);
let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
// the '2' identifies this as a script load
let ioChannel = ioService.newChannelFromURI2(uri, null, systemPrincipal,
systemPrincipal, 0, 2);
ioChannel.notificationCallbacks = new RedirectHttpsOnly();
ioChannel.asyncOpen(listener, null);
});
};
ResourceLoader.prototype = {
onDataAvailable: function(request, context, input, offset, count) {
let stream = Cc['@mozilla.org/scriptableinputstream;1']
.createInstance(Ci.nsIScriptableInputStream);
stream.init(input);
this.data += stream.read(count);
},
onStartRequest: function (request, context) {},
onStopRequest: function(request, context, status) {
if (Components.isSuccessCode(status)) {
var statusCode = request.QueryInterface(Ci.nsIHttpChannel).responseStatus;
if (statusCode === 200) {
this.resolve({ request: request, data: this.data });
} else {
this.reject(new Error('Non-200 response from server: ' + statusCode));
}
} else {
this.reject(new Error('Load failed: ' + status));
}
},
getInterface: function(iid) {
return this.QueryInterface(iid);
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener])
};
/**
* A simple implementation of the WorkerLocation interface.
*/
function createLocationFromURI(uri) {
return {
href: uri.spec,
protocol: uri.scheme + ':',
host: uri.host + ((uri.port >= 0) ?
(':' + uri.port) : ''),
port: uri.port,
hostname: uri.host,
pathname: uri.path.replace(/[#\?].*/, ''),
search: uri.path.replace(/^[^\?]*/, '').replace(/#.*/, ''),
hash: uri.hasRef ? ('#' + uri.ref) : '',
origin: uri.prePath,
toString: function() {
return uri.spec;
}
};
}
/**
* A javascript sandbox for running an IdP.
*
* @param domain (string) the domain of the IdP
* @param protocol (string?) the protocol of the IdP [default: 'default']
* @throws if the domain or protocol aren't valid
*/
function IdpSandbox(domain, protocol) {
this.source = IdpSandbox.createIdpUri(domain, protocol || "default");
this.active = null;
this.sandbox = null;
}
IdpSandbox.checkDomain = function(domain) {
if (!domain || typeof domain !== 'string') {
throw new Error('Invalid domain for identity provider: ' +
'must be a non-zero length string');
}
};
/**
* Checks that the IdP protocol is superficially sane. In particular, we don't
* want someone adding relative paths (e.g., '../../myuri'), which could be used
* to move outside of /.well-known/ and into space that they control.
*/
IdpSandbox.checkProtocol = function(protocol) {
let message = 'Invalid protocol for identity provider: ';
if (!protocol || typeof protocol !== 'string') {
throw new Error(message + 'must be a non-zero length string');
}
if (decodeURIComponent(protocol).match(/[\/\\]/)) {
throw new Error(message + "must not include '/' or '\\'");
}
};
/**
* Turns a domain and protocol into a URI. This does some aggressive checking
* to make sure that we aren't being fooled somehow. Throws on fooling.
*/
IdpSandbox.createIdpUri = function(domain, protocol) {
IdpSandbox.checkDomain(domain);
IdpSandbox.checkProtocol(protocol);
let message = 'Invalid IdP parameters: ';
try {
let wkIdp = 'https://' + domain + '/.well-known/idp-proxy/' + protocol;
let ioService = Components.classes['@mozilla.org/network/io-service;1']
.getService(Ci.nsIIOService);
let uri = ioService.newURI(wkIdp, null, null);
if (uri.hostPort !== domain) {
throw new Error(message + 'domain is invalid');
}
if (uri.path.indexOf('/.well-known/idp-proxy/') !== 0) {
throw new Error(message + 'must produce a /.well-known/idp-proxy/ URI');
}
return uri;
} catch (e if (typeof e.result !== 'undefined' &&
e.result === Cr.NS_ERROR_MALFORMED_URI)) {
throw new Error(message + 'must produce a valid URI');
}
};
IdpSandbox.prototype = {
isSame: function(domain, protocol) {
return this.source.spec === IdpSandbox.createIdpUri(domain, protocol).spec;
},
start: function() {
if (!this.active) {
this.active = ResourceLoader.load(this.source)
.then(result => this._createSandbox(result));
}
return this.active;
},
// Provides the sandbox with some useful facilities. Initially, this is only
// a minimal set; it is far easier to add more as the need arises, than to
// take them back if we discover a mistake.
_populateSandbox: function() {
this.sandbox.location = Cu.cloneInto(createLocationFromURI(this.source),
this.sandbox,
{ cloneFunctions: true });
},
_createSandbox: function(result) {
let principal = Services.scriptSecurityManager
.getChannelResultPrincipal(result.request);
this.sandbox = Cu.Sandbox(principal, {
sandboxName: 'IdP-' + this.source.host,
wantComponents: false,
wantExportHelpers: false,
wantGlobalProperties: [
'indexedDB', 'XMLHttpRequest', 'TextEncoder', 'TextDecoder',
'URL', 'URLSearchParams', 'atob', 'btoa', 'Blob', 'crypto',
'rtcIdentityProvider'
]
});
this._populateSandbox();
let registrar = this.sandbox.rtcIdentityProvider;
if (!Cu.isXrayWrapper(registrar)) {
throw new Error('IdP setup failed');
}
// putting a javascript version of 1.8 here seems fragile
Cu.evalInSandbox(result.data, this.sandbox,
'1.8', result.request.URI.spec, 1);
if (!registrar.idp) {
throw new Error('IdP failed to call rtcIdentityProvider.register()');
}
return registrar;
},
stop: function() {
if (this.sandbox) {
Cu.nukeSandbox(this.sandbox);
}
this.sandbox = null;
this.active = null;
},
toString: function() {
return this.source.spec;
}
};
this.EXPORTED_SYMBOLS = ['IdpSandbox'];
this.IdpSandbox = IdpSandbox;

View File

@ -389,11 +389,13 @@ RTCPeerConnection.prototype = {
_initIdp: function() {
let prefName = "media.peerconnection.identity.timeout";
let idpTimeout = Services.prefs.getIntPref(prefName);
let warningFunc = this.logWarning.bind(this);
this._localIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
this.dispatchEvent.bind(this));
this._remoteIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
this.dispatchEvent.bind(this));
let warn = this.logWarning.bind(this);
let idpErrorReport = (type, args) => {
this.dispatchEvent(
new this._win.RTCPeerConnectionIdentityErrorEvent(type, args));
};
this._localIdp = new PeerConnectionIdp(idpTimeout, warn, idpErrorReport);
this._remoteIdp = new PeerConnectionIdp(idpTimeout, warn, idpErrorReport);
},
// Add a function to the internal operations chain.
@ -706,23 +708,22 @@ RTCPeerConnection.prototype = {
this._impl.setRemoteDescription(type, desc.sdp);
});
let pp = new Promise(resolve =>
this._remoteIdp.verifyIdentityFromSDP(desc.sdp, origin, resolve))
.then(msg => {
// If this pc has an identity already, then identity in sdp must match
if (expectedIdentity && (!msg || msg.identity !== expectedIdentity)) {
throw new this._win.DOMException(
"Peer Identity mismatch, expected: " + expectedIdentity,
"IncompatibleSessionDescriptionError");
}
if (msg) {
// Set new identity and generate an event.
this._impl.peerIdentity = msg.identity;
this._peerIdentity = new this._win.RTCIdentityAssertion(
this._remoteIdp.provider, msg.identity);
this.dispatchEvent(new this._win.Event("peeridentity"));
}
});
let pp = this._remoteIdp.verifyIdentityFromSDP(desc.sdp, origin)
.then(msg => {
// If this pc has an identity already, then identity in sdp must match
if (expectedIdentity && (!msg || msg.identity !== expectedIdentity)) {
throw new this._win.DOMException(
"Peer Identity mismatch, expected: " + expectedIdentity,
"IncompatibleSessionDescriptionError");
}
if (msg) {
// Set new identity and generate an event.
this._impl.peerIdentity = msg.identity;
this._peerIdentity = new this._win.RTCIdentityAssertion(
this._remoteIdp.provider, msg.identity);
this.dispatchEvent(new this._win.Event("peeridentity"));
}
});
// Only wait for Idp validation if we need identity matching.
return expectedIdentity? this._win.Promise.all([p, pp]).then(() => {}) : p;
});
@ -734,7 +735,10 @@ RTCPeerConnection.prototype = {
this._localIdp.setIdentityProvider(provider, protocol, username);
},
_gotIdentityAssertion: function(assertion){
_gotIdentityAssertion: function(assertion) {
if (!assertion) {
return;
}
let args = { assertion: assertion };
let ev = new this._win.RTCPeerConnectionIdentityEvent("identityresult", args);
this.dispatchEvent(ev);
@ -743,14 +747,8 @@ RTCPeerConnection.prototype = {
getIdentityAssertion: function() {
this._checkClosed();
var gotAssertion = assertion => {
if (assertion) {
this._gotIdentityAssertion(assertion);
}
};
this._localIdp.getIdentityAssertion(this._impl.fingerprint,
gotAssertion);
this._localIdp.getIdentityAssertion(this._impl.fingerprint)
.then(assertion => this._gotIdentityAssertion(assertion));
},
updateIce: function(config) {
@ -888,7 +886,7 @@ RTCPeerConnection.prototype = {
return null;
}
sdp = this._localIdp.wrapSdp(sdp);
sdp = this._localIdp.addIdentityAttribute(sdp);
return new this._win.mozRTCSessionDescription({ type: this._localType,
sdp: sdp });
},
@ -1042,15 +1040,20 @@ PeerConnectionObserver.prototype = {
onCreateOfferSuccess: function(sdp) {
let pc = this._dompc;
let fp = pc._impl.fingerprint;
let origin = Cu.getWebIDLCallerPrincipal().origin;
pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
if (assertion) {
pc._gotIdentityAssertion(assertion);
}
let idp = pc._localIdp;
if (idp.enabled) {
idp.getIdentityAssertion(pc._impl.fingerprint)
.then(assertion => {
pc._gotIdentityAssertion(assertion);
sdp = idp.addIdentityAttribute(sdp);
pc._onCreateOfferSuccess(new pc._win.mozRTCSessionDescription({ type: "offer",
sdp: sdp }));
}, e => {}); // errors are handled in the IdP
} else {
pc._onCreateOfferSuccess(new pc._win.mozRTCSessionDescription({ type: "offer",
sdp: sdp }));
}.bind(this));
}
},
onCreateOfferError: function(code, message) {
@ -1059,15 +1062,20 @@ PeerConnectionObserver.prototype = {
onCreateAnswerSuccess: function(sdp) {
let pc = this._dompc;
let fp = pc._impl.fingerprint;
let origin = Cu.getWebIDLCallerPrincipal().origin;
pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
if (assertion) {
pc._gotIdentityAssertion(assertion);
}
let idp = pc._localIdp;
if (idp.enabled) {
idp.getIdentityAssertion(pc._impl.fingerprint)
.then(assertion => {
pc._gotIdentityAssertion(assertion);
sdp = idp.addIdentityAttribute(sdp);
pc._onCreateAnswerSuccess(new pc._win.mozRTCSessionDescription({ type: "answer",
sdp: sdp }));
}, e => {});
} else {
pc._onCreateAnswerSuccess(new pc._win.mozRTCSessionDescription({ type: "answer",
sdp: sdp }));
}.bind(this));
}
},
onCreateAnswerError: function(code, message) {

View File

@ -3,68 +3,93 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
this.EXPORTED_SYMBOLS = ["PeerConnectionIdp"];
this.EXPORTED_SYMBOLS = ['PeerConnectionIdp'];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "IdpProxy",
"resource://gre/modules/media/IdpProxy.jsm");
Cu.import('resource://gre/modules/Services.jsm');
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'IdpSandbox',
'resource://gre/modules/media/IdpSandbox.jsm');
function TimerResolver(resolve) {
this.notify = resolve;
}
TimerResolver.prototype = {
getInterface: function(iid) {
return this.QueryInterface(iid);
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
}
function delay(t) {
return new Promise(resolve => {
let timer = Cc['@mozilla.org/timer;1'].getService(Ci.nsITimer);
timer.initWithCallback(new TimerResolver(resolve), t, 0); // One shot
});
}
/**
* Creates an IdP helper.
*
* @param window (object) the window object to use for miscellaneous goodies
* @param timeout (int) the timeout in milliseconds
* @param warningFunc (function) somewhere to dump warning messages
* @param dispatchEventFunc (function) somewhere to dump error events
* @param dispatchErrorFunc (function(string, dict)) somewhere to dump errors
*/
function PeerConnectionIdp(window, timeout, warningFunc, dispatchEventFunc) {
this._win = window;
function PeerConnectionIdp(timeout, warningFunc, dispatchErrorFunc) {
this._timeout = timeout || 5000;
this._warning = warningFunc;
this._dispatchEvent = dispatchEventFunc;
this._dispatchError = dispatchErrorFunc;
this.assertion = null;
this.provider = null;
}
(function() {
PeerConnectionIdp._mLinePattern = new RegExp("^m=", "m");
PeerConnectionIdp._mLinePattern = new RegExp('^m=', 'm');
// attributes are funny, the 'a' is case sensitive, the name isn't
let pattern = "^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)";
PeerConnectionIdp._identityPattern = new RegExp(pattern, "m");
pattern = "^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)";
PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, "m");
let pattern = '^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)';
PeerConnectionIdp._identityPattern = new RegExp(pattern, 'm');
pattern = '^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)';
PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, 'm');
})();
PeerConnectionIdp.prototype = {
get enabled() {
return !!this._idp;
},
setIdentityProvider: function(provider, protocol, username) {
this.provider = provider;
this.protocol = protocol;
this.protocol = protocol || 'default';
this.username = username;
if (this._idpchannel) {
if (this._idpchannel.isSame(provider, protocol)) {
return;
if (this._idp) {
if (this._idp.isSame(provider, protocol)) {
return; // noop
}
this._idpchannel.close();
this._idp.stop();
}
this._idpchannel = new IdpProxy(provider, protocol);
this._idp = new IdpSandbox(provider, protocol);
},
close: function() {
this.assertion = null;
this.provider = null;
if (this._idpchannel) {
this._idpchannel.close();
this._idpchannel = null;
this.protocol = null;
if (this._idp) {
this._idp.stop();
this._idp = null;
}
},
/**
* Generate an error event of the identified type;
* and put a little more precise information in the console.
*
* A little note on error handling in this class: this class reports errors
* exclusively through the event handlers that are passed to it
* (this._dispatchError, specifically). That means that all the functions
* return resolved promises; promises are never rejected. This probably isn't
* the best design, but the refactor can wait.
*/
reportError: function(type, message, extra) {
let args = {
@ -76,9 +101,8 @@ PeerConnectionIdp.prototype = {
args[k] = extra[k];
});
}
this._warning("RTC identity: " + message, null, 0);
let ev = new this._win.RTCPeerConnectionIdentityErrorEvent('idp' + type + 'error', args);
this._dispatchEvent(ev);
this._warning('RTC identity: ' + message, null, 0);
this._dispatchError('idp' + type + 'error', args);
},
_getFingerprintsFromSdp: function(sdp) {
@ -93,169 +117,163 @@ PeerConnectionIdp.prototype = {
return Object.keys(fingerprints).map(k => fingerprints[k]);
},
_isValidAssertion: function(assertion) {
return assertion && assertion.idp &&
typeof assertion.idp.domain === 'string' &&
(!assertion.idp.protocol ||
typeof assertion.idp.protocol === 'string') &&
typeof assertion.assertion === 'string';
},
_getIdentityFromSdp: function(sdp) {
// a=identity is session level
let idMatch;
let mLineMatch = sdp.match(PeerConnectionIdp._mLinePattern);
if (mLineMatch) {
let sessionLevel = sdp.substring(0, mLineMatch.index);
idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
let idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
}
if (!idMatch) {
return; // undefined === no identity
}
if (idMatch) {
let assertion = {};
try {
assertion = JSON.parse(atob(idMatch[1]));
} catch (e) {
this.reportError("validation",
"invalid identity assertion: " + e);
} // for JSON.parse
if (typeof assertion.idp === "object" &&
typeof assertion.idp.domain === "string" &&
typeof assertion.assertion === "string") {
return assertion;
}
this.reportError("validation", "assertion missing" +
" idp/idp.domain/assertion");
let assertion;
try {
assertion = JSON.parse(atob(idMatch[1]));
} catch (e) {
this.reportError('validation',
'invalid identity assertion: ' + e);
}
// undefined!
if (!this._isValidAssertion(assertion)) {
this.reportError('validation', 'assertion missing' +
' idp/idp.domain/assertion');
}
return assertion;
},
/**
* Queues a task to verify the a=identity line the given SDP contains, if any.
* Verifies the a=identity line the given SDP contains, if any.
* If the verification succeeds callback is called with the message from the
* IdP proxy as parameter, else (verification failed OR no a=identity line in
* SDP at all) null is passed to callback.
*
* Note that this only verifies that the SDP is coherent. This relies on the
* invariant that the RTCPeerConnection won't connect to a peer if the
* fingerprint of the certificate they offer doesn't appear in the SDP.
*/
verifyIdentityFromSDP: function(sdp, origin, callback) {
verifyIdentityFromSDP: function(sdp, origin) {
let identity = this._getIdentityFromSdp(sdp);
let fingerprints = this._getFingerprintsFromSdp(sdp);
// it's safe to use the fingerprint we got from the SDP here,
// only because we ensure that there is only one
if (!identity || fingerprints.length <= 0) {
callback(null);
return;
return Promise.resolve();
}
this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
this._verifyIdentity(identity.assertion, fingerprints, origin, callback);
return this._verifyIdentity(identity.assertion, fingerprints, origin);
},
/**
* Checks that the name in the identity provided by the IdP is OK.
*
* @param error (function) an error function to call
* @param name (string) the name to validate
* @returns (string) an error message, iff the name isn't good
* @throws if the name isn't valid
*/
_validateName: function(name) {
if (typeof name !== "string") {
return "name not a string";
_validateName: function(error, name) {
if (typeof name !== 'string') {
return error('name not a string');
}
let atIdx = name.indexOf('@');
if (atIdx <= 0) {
return error('missing authority in name from IdP');
}
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);
}
let idnService = Components.classes["@mozilla.org/network/idn-service;1"].
getService(Components.interfaces.nsIIDNService);
if (idnService.convertUTF8toACE(tail) !==
idnService.convertUTF8toACE(provider)) {
return "name '" + identity.name +
"' doesn't match IdP: '" + this.provider + "'";
}
return null;
// no third party assertions... for now
let tail = name.substring(atIdx + 1);
// strip the port number, if present
let provider = this.provider;
let providerPortIdx = provider.indexOf(':');
if (providerPortIdx > 0) {
provider = provider.substring(0, providerPortIdx);
}
return "missing authority in name from IdP";
let idnService = Components.classes['@mozilla.org/network/idn-service;1'].
getService(Components.interfaces.nsIIDNService);
if (idnService.convertUTF8toACE(tail) !==
idnService.convertUTF8toACE(provider)) {
return error('name "' + identity.name +
'" doesn\'t match IdP: "' + this.provider + '"');
}
return true;
},
// we are very defensive here when handling the message from the IdP
// proxy so that broken IdPs can only do as little harm as possible.
_checkVerifyResponse: function(message, fingerprints) {
let warn = msg => {
this.reportError("validation",
"assertion validation failure: " + msg);
/**
* Check the validation response. We are very defensive here when handling
* the message from the IdP proxy. That way, broken IdPs aren't likely to
* cause catastrophic damage.
*/
_isValidVerificationResponse: function(validation, sdpFingerprints) {
let error = msg => {
this.reportError('validation', 'assertion validation failure: ' + msg);
return false;
};
let isSubsetOf = (outer, inner, cmp) => {
return inner.some(i => {
return !outer.some(o => cmp(i, o));
if (typeof validation !== 'object' ||
typeof validation.contents !== 'string' ||
typeof validation.identity !== 'string') {
return error('no payload in validation response');
}
let fingerprints;
try {
fingerprints = JSON.parse(validation.contents).fingerprint;
} catch (e) {
return error('idp returned invalid JSON');
}
let isFingerprint = f =>
(typeof f.digest === 'string') &&
(typeof f.algorithm === 'string');
if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) {
return error('fingerprints must be an array of objects' +
' with digest and algorithm attributes');
}
let isSubsetOf = (outerSet, innerSet, comparator) => {
return innerSet.every(i => {
return outerSet.some(o => comparator(i, o));
});
};
let compareFingerprints = (a, b) => {
return (a.digest === b.digest) && (a.algorithm === b.algorithm);
};
if (!isSubsetOf(fingerprints, sdpFingerprints, compareFingerprints)) {
return error('the fingerprints in SDP aren\'t covered by the assertion');
}
return this._validateName(error, validation.identity);
},
try {
let contents = JSON.parse(message.contents);
if (!Array.isArray(contents.fingerprint)) {
warn("fingerprint is not an array");
} else if (isSubsetOf(contents.fingerprint, fingerprints,
compareFingerprints)) {
warn("fingerprints in SDP aren't a subset of those in the assertion");
} else {
let error = this._validateName(message.identity);
if (error) {
warn(error);
} else {
return true;
/**
* Asks the IdP proxy to verify an identity assertion.
*/
_verifyIdentity: function(assertion, fingerprints, origin) {
let validationPromise = this._idp.start()
.then(idp => idp.validateAssertion(assertion, origin));
return this._safetyNet('validation', validationPromise)
.then(validation => {
if (validation &&
this._isValidVerificationResponse(validation, fingerprints)) {
return validation;
}
}
} catch(e) {
warn("invalid JSON in content");
}
return false;
});
},
/**
* Asks the IdP proxy to verify an identity.
* Enriches the given SDP with an `a=identity` line. getIdentityAssertion()
* must have already run successfully, otherwise this does nothing to the sdp.
*/
_verifyIdentity: function(assertion, fingerprints, origin, callback) {
function onVerification(message) {
if (message && this._checkVerifyResponse(message, fingerprints)) {
callback(message);
} else {
this._warning("RTC identity: assertion validation failure", null, 0);
callback(null);
}
}
let request = {
type: "VERIFY",
message: assertion,
origin: origin
};
this._sendToIdp(request, "validation", 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, origin, callback) {
let onAssertion = function() {
callback(this.wrapSdp(sdp), this.assertion);
}.bind(this);
if (!this._idpchannel || this.assertion) {
onAssertion();
return;
}
this._getIdentityAssertion(fingerprint, origin, onAssertion);
},
/**
* Inserts an identity assertion into the given SDP.
*/
wrapSdp: function(sdp) {
addIdentityAttribute: function(sdp) {
if (!this.assertion) {
return sdp;
}
@ -263,103 +281,73 @@ PeerConnectionIdp.prototype = {
// yes, we assume that this matches; if it doesn't something is *wrong*
let match = sdp.match(PeerConnectionIdp._mLinePattern);
return sdp.substring(0, match.index) +
"a=identity:" + this.assertion + "\r\n" +
'a=identity:' + this.assertion + '\r\n' +
sdp.substring(match.index);
},
getIdentityAssertion: function(fingerprint, callback) {
if (!this._idpchannel) {
this.reportError("assertion", "IdP not set");
callback(null);
return;
/**
* Asks the IdP proxy for an identity assertion. Don't call this unless you
* have checked .enabled, or you really like exceptions.
*/
getIdentityAssertion: function(fingerprint) {
if (!this.enabled) {
this.reportError('assertion', 'no IdP set,' +
' call setIdentityProvider() to set one');
return Promise.resolve();
}
let origin = Cu.getWebIDLCallerPrincipal().origin;
this._getIdentityAssertion(fingerprint, origin, callback);
},
_getIdentityAssertion: function(fingerprint, origin, callback) {
let [algorithm, digest] = fingerprint.split(" ", 2);
let message = {
let [algorithm, digest] = fingerprint.split(' ', 2);
let content = {
fingerprint: [{
algorithm: algorithm,
digest: digest
}]
};
let request = {
type: "SIGN",
message: JSON.stringify(message),
username: this.username,
origin: origin
};
let origin = Cu.getWebIDLCallerPrincipal().origin;
// catch the assertion, clean it up, warn if absent
function trapAssertion(assertion) {
if (!assertion) {
this._warning("RTC identity: assertion generation failure", null, 0);
this.assertion = null;
} else {
this.assertion = btoa(JSON.stringify(assertion));
}
callback(this.assertion);
}
let assertionPromise = this._idp.start()
.then(idp => idp.generateAssertion(JSON.stringify(content),
origin, this.username));
this._sendToIdp(request, "assertion", trapAssertion.bind(this));
return this._safetyNet('assertion', assertionPromise)
.then(assertion => {
if (this._isValidAssertion(assertion)) {
// save the base64+JSON assertion, since that is all that is used
this.assertion = btoa(JSON.stringify(assertion));
} else {
if (assertion) {
// only report an error for an invalid assertion
// other paths generate more specific error reports
this.reportError('assertion', 'invalid assertion generated');
}
this.assertion = null;
}
return this.assertion;
});
},
/**
* Packages a message and sends it to the IdP.
* @param request (dictionary) the message to send
* @param type (DOMString) the type of message (assertion/validation)
* @param callback (function) the function to call with the results
* Wraps a promise, adding a timeout guard on it so that it can't take longer
* than the specified time. Returns a promise that always resolves; if there
* is a problem the resolved value is undefined.
*/
_sendToIdp: function(request, type, callback) {
this._idpchannel.send(request, this._wrapCallback(type, callback));
},
_reportIdpError: function(type, message) {
let args = {};
let msg = "";
if (message.type === "ERROR") {
msg = message.error;
} else {
msg = JSON.stringify(message.message);
if (message.type === "LOGINNEEDED") {
args.loginUrl = message.loginUrl;
}
}
this.reportError(type, "received response of type '" +
message.type + "' from IdP: " + msg, args);
},
/**
* Wraps a callback, adding a timeout and ensuring that the callback doesn't
* receive any message other than one where the IdP generated a "SUCCESS"
* response.
*/
_wrapCallback: function(type, callback) {
let timeout = this._win.setTimeout(function() {
this.reportError(type, "IdP timeout for " + this._idpchannel + " " +
(this._idpchannel.ready ? "[ready]" : "[not ready]"));
timeout = null;
callback(null);
}.bind(this), this._timeout);
return function(message) {
if (!timeout) {
return;
}
this._win.clearTimeout(timeout);
timeout = null;
let content = null;
if (message.type === "SUCCESS") {
content = message.message;
} else {
this._reportIdpError(type, message);
}
callback(content);
}.bind(this);
_safetyNet: function(type, p) {
let done = false; // ... all because Promises don't expose state
let timeoutPromise = delay(this._timeout)
.then(() => {
if (!done) {
this.reportError(type, 'IdP timed out');
}
});
let realPromise = p
.catch(e => this.reportError(type, 'error reported by IdP: ' + e.message))
.then(result => {
done = true;
return result;
});
// If timeoutPromise completes first, the returned value will be undefined,
// just like when there is an error.
return Promise.race([realPromise, timeoutPromise]);
}
};

View File

@ -238,7 +238,7 @@ EXTRA_COMPONENTS += [
]
EXTRA_JS_MODULES.media += [
'IdpProxy.jsm',
'IdpSandbox.jsm',
'PeerConnectionIdp.jsm',
'RTCStatsReport.jsm',
]