return match.index; }, _getIdentityFromSdp(sdp) { // a=identity is session level let idMatch; const index = this._getSessionLevelEnd(sdp); const sessionLevel = sdp.substring(0, index); idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern); if (!idMatch) { return undefined; // undefined === no identity } let assertion; try { assertion = JSON.parse(atob(idMatch[1])); } catch (e) { throw new this._win.DOMException( "invalid identity assertion: " + e, "InvalidSessionDescriptionError" ); } if (!this._isValidAssertion(assertion)) { throw new this._win.DOMException( "assertion missing idp/idp.domain/assertion", "InvalidSessionDescriptionError" ); } return assertion; }, /** * 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. We still rely on * the fact that the RTCPeerConnection won't connect to a peer if the * fingerprint of the certificate they offer doesn't appear in the SDP. */ verifyIdentityFromSDP(sdp, origin) { let identity = this._getIdentityFromSdp(sdp); let fingerprints = this._getFingerprintsFromSdp(sdp); if (!identity || fingerprints.length <= 0) { return this._win.Promise.resolve(); // undefined result = no identity } this.setIdentityProvider(identity.idp.domain, identity.idp.protocol); return this._verifyIdentity(identity.assertion, fingerprints, origin); }, /** * Checks that the name in the identity provided by the IdP is OK. * * @param name (string) the name to validate * @throws if the name isn't valid */ _validateName(name) { let error = msg => { throw new this._win.DOMException( "assertion name error: " + msg, "IdpError" ); }; if (typeof name !== "string") { error("name not a string"); } let atIdx = name.indexOf("@"); if (atIdx <= 0) { error("missing authority in name from IdP"); } // 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 = Cc["@mozilla.org/network/idn-service;1"].getService( Ci.nsIIDNService ); if (idnService.domainToASCII(tail) !== idnService.domainToASCII(provider)) { error('name "' + name + '" doesn\'t match IdP: "' + this.provider + '"'); } }, /** * 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. */ _checkValidation(validation, sdpFingerprints) { let error = msg => { throw new this._win.DOMException( "IdP validation error: " + msg, "IdpError" ); }; if (!this.provider) { error("IdP closed"); } if ( typeof validation !== "object" || typeof validation.contents !== "string" || typeof validation.identity !== "string" ) { error("no payload in validation response"); } let fingerprints; try { fingerprints = JSON.parse(validation.contents).fingerprint; } catch (e) { error("invalid JSON"); } let isFingerprint = f => typeof f.digest === "string" && typeof f.algorithm === "string"; if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) { error( "fingerprints must be an array of objects" + " with digest and algorithm attributes" ); } // everything in `innerSet` is found in `outerSet` 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)) { error("the fingerprints must be covered by the assertion"); } this._validateName(validation.identity); return validation; }, /** * Asks the IdP proxy to verify an identity assertion. */ _verifyIdentity(assertion, fingerprints, origin) { let p = this.start() .then(idp => this._wrapCrossCompartmentPromise( idp.validateAssertion(assertion, origin) ) ) .then(validation => this._checkValidation(validation, fingerprints)); return this._applyTimeout(p); }, /** * Enriches the given SDP with an `a=identity` line. getIdentityAssertion() * must have already run successfully, otherwise this does nothing to the sdp. */ addIdentityAttribute(sdp) { if (!this.assertion) { return sdp; } const index = this._getSessionLevelEnd(sdp); return ( sdp.substring(0, index) + "a=identity:" + this.assertion + "\r\n" + sdp.substring(index) ); }, /** * Asks the IdP proxy for an identity assertion. Don't call this unless you * have checked .enabled, or you really like exceptions. Also, don't call * this when another call is still running, because it's not certain which * call will finish first and the final state will be similarly uncertain. */ getIdentityAssertion(fingerprint, origin) { if (!this.enabled) { throw new this._win.DOMException( "no IdP set, call setIdentityProvider() to set one", "InvalidStateError" ); } let [algorithm, digest] = fingerprint.split(" ", 2); let content = { fingerprint: [ { algorithm, digest, }, ], }; this._resetAssertion(); let p = this.start() .then(idp => { let options = { protocol: this.protocol, usernameHint: this.username, peerIdentity: this.peeridentity, }; return this._wrapCrossCompartmentPromise( idp.generateAssertion(JSON.stringify(content), origin, options) ); }) .then(assertion => { if (!this._isValidAssertion(assertion)) { throw new this._win.DOMException( "IdP generated invalid assertion", "IdpError" ); } // save the base64+JSON assertion, since that is all that is used this.assertion = btoa(JSON.stringify(assertion)); return this.assertion; }); return this._applyTimeout(p); }, /** * Promises generated by the sandbox need to be very carefully treated so that * they can chain into promises in the `this._win` compartment. Results need * to be cloned across; errors need to be converted. */ _wrapCrossCompartmentPromise(sandboxPromise) { return new this._win.Promise((resolve, reject) => { sandboxPromise.then( result => resolve(Cu.cloneInto(result, this._win)), e => { let message = "" + (e.message || JSON.stringify(e) || "IdP error"); if (e.name === "IdpLoginError") { if (typeof e.loginUrl === "string") { this.idpLoginUrl = e.loginUrl; } reject(new this._win.DOMException(message, "IdpLoginError")); } else { reject(new this._win.DOMException(message, "IdpError")); } } ); }); }, /** * Wraps a promise, adding a timeout guard on it so that it can't take longer * than the specified time. Returns a promise that rejects if the timeout * elapses before `p` resolves. */ _applyTimeout(p) { let timeout = new this._win.Promise(r => this._win.setTimeout(r, this._timeout) ).then(() => { throw new this._win.DOMException("IdP timed out", "IdpError"); }); return this._win.Promise.race([timeout, p]); }, }; PK