--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -214,16 +214,17 @@
#ifdef MOZ_GTK2
@BINPATH@/components/filepicker.xpt
#endif
@BINPATH@/components/find.xpt
@BINPATH@/components/fuel.xpt
@BINPATH@/components/gfx.xpt
@BINPATH@/components/html5.xpt
@BINPATH@/components/htmlparser.xpt
+@BINPATH@/components/identity.xpt
@BINPATH@/components/imglib2.xpt
@BINPATH@/components/imgicon.xpt
@BINPATH@/components/inspector.xpt
@BINPATH@/components/intl.xpt
@BINPATH@/components/jar.xpt
#ifdef MOZ_JSDEBUGGER
@BINPATH@/components/jsdservice.xpt
#endif
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/Identity.jsm
@@ -0,0 +1,305 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 EXPORTED_SYMBOLS = ["IdentityService"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+Cu.import("resource://gre/modules/identity/IdentityStore.jsm");
+Cu.import("resource://gre/modules/identity/RelyingParty.jsm");
+Cu.import("resource://gre/modules/identity/IdentityProvider.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "jwcrypto",
+ "resource://gre/modules/identity/jwcrypto.jsm");
+
+function log(...aMessageArgs) {
+ Logger.log([null].concat(aMessageArgs));
+}
+function reportError(...aMessageArgs) {
+ Logger.reportError([null].concat(aMessageArgs));
+}
+
+function IDService() {
+ Services.obs.addObserver(this, "quit-application-granted", false);
+ Services.obs.addObserver(this, "identity-auth-complete", false);
+
+ this._store = IdentityStore;
+ this.RP = RelyingParty;
+ this.IDP = IdentityProvider;
+}
+
+IDService.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "quit-application-granted":
+ Services.obs.removeObserver(this, "quit-application-granted");
+ this.shutdown();
+ break;
+ case "identity-auth-complete":
+ if (!aSubject || !aSubject.wrappedJSObject)
+ break;
+ let subject = aSubject.wrappedJSObject;
+ log("NOW SELECT", aSubject.wrappedJSObject);
+ // We have authenticated in order to provision an identity.
+ // So try again.
+ this.selectIdentity(subject.rpId, subject.identity);
+ break;
+ }
+ },
+
+ reset: function reset() {
+ // Explicitly call reset() on our RP and IDP classes.
+ // This is here to make testing easier. When the
+ // quit-application-granted signal is emitted, reset() will be
+ // called here, on RP, on IDP, and on the store. So you don't
+ // need to use this :)
+ this._store.reset();
+ this.RP.reset();
+ this.IDP.reset();
+ },
+
+ shutdown: function shutdown() {
+ log("shutdown");
+ Services.obs.removeObserver(this, "identity-auth-complete");
+ Services.obs.removeObserver(this, "quit-application-granted");
+ },
+
+ /**
+ * Parse an email into username and domain if it is valid, else return null
+ */
+ parseEmail: function parseEmail(email) {
+ var match = email.match(/^([^@]+)@([^@^/]+.[a-z]+)$/);
+ if (match) {
+ return {
+ username: match[1],
+ domain: match[2]
+ };
+ }
+ return null;
+ },
+
+ /**
+ * The UX wants to add a new identity
+ * often followed by selectIdentity()
+ *
+ * @param aIdentity
+ * (string) the email chosen for login
+ */
+ addIdentity: function addIdentity(aIdentity) {
+ if (this._store.fetchIdentity(aIdentity) === null) {
+ this._store.addIdentity(aIdentity, null, null);
+ }
+ },
+
+ /**
+ * The UX comes back and calls selectIdentity once the user has picked
+ * an identity.
+ *
+ * @param aRPId
+ * (integer) the id of the doc object obtained in .watch() and
+ * passed to the UX component.
+ *
+ * @param aIdentity
+ * (string) the email chosen for login
+ */
+ selectIdentity: function selectIdentity(aRPId, aIdentity) {
+ log("selectIdentity: RP id:", aRPId, "identity:", aIdentity);
+
+ // Get the RP that was stored when watch() was invoked.
+ let rp = this.RP._rpFlows[aRPId];
+ if (!rp) {
+ reportError("selectIdentity", "Invalid RP id: ", aRPId);
+ return;
+ }
+
+ // It's possible that we are in the process of provisioning an
+ // identity.
+ let provId = rp.provId;
+
+ let rpLoginOptions = {
+ loggedInEmail: aIdentity,
+ origin: rp.origin
+ };
+ log("selectIdentity: provId:", provId, "origin:", rp.origin);
+
+ // Once we have a cert, and once the user is authenticated with the
+ // IdP, we can generate an assertion and deliver it to the doc.
+ let self = this;
+ this.RP._generateAssertion(rp.origin, aIdentity, function hadReadyAssertion(err, assertion) {
+ if (!err && assertion) {
+ self.RP._doLogin(rp, rpLoginOptions, assertion);
+ return;
+
+ }
+ // Need to provision an identity first. Begin by discovering
+ // the user's IdP.
+ self._discoverIdentityProvider(aIdentity, function gotIDP(err, idpParams) {
+ if (err) {
+ rp.doError(err);
+ return;
+ }
+
+ // The idpParams tell us where to go to provision and authenticate
+ // the identity.
+ self.IDP._provisionIdentity(aIdentity, idpParams, provId, function gotID(err, aProvId) {
+
+ // Provision identity may have created a new provision flow
+ // for us. To make it easier to relate provision flows with
+ // RP callers, we cross index the two here.
+ rp.provId = aProvId;
+ self.IDP._provisionFlows[aProvId].rpId = aRPId;
+
+ // At this point, we already have a cert. If the user is also
+ // already authenticated with the IdP, then we can try again
+ // to generate an assertion and login.
+ if (err) {
+ // We are not authenticated. If we have already tried to
+ // authenticate and failed, then this is a "hard fail" and
+ // we give up. Otherwise we try to authenticate with the
+ // IdP.
+
+ if (self.IDP._provisionFlows[aProvId].didAuthentication) {
+ self.IDP._cleanUpProvisionFlow(aProvId);
+ self.RP._cleanUpProvisionFlow(aRPId, aProvId);
+ log("ERROR: selectIdentity: authentication hard fail");
+ rp.doError("Authentication fail.");
+ return;
+ }
+ // Try to authenticate with the IdP. Note that we do
+ // not clean up the provision flow here. We will continue
+ // to use it.
+ self.IDP._doAuthentication(aProvId, idpParams);
+ return;
+ }
+
+ // Provisioning flows end when a certificate has been registered.
+ // Thus IdentityProvider's registerCertificate() cleans up the
+ // current provisioning flow. We only do this here on error.
+ self.RP._generateAssertion(rp.origin, aIdentity, function gotAssertion(err, assertion) {
+ if (err) {
+ rp.doError(err);
+ return;
+ }
+ self.RP._doLogin(rp, rpLoginOptions, assertion);
+ self.RP._cleanUpProvisionFlow(aRPId, aProvId);
+ return;
+ });
+ });
+ });
+ });
+ },
+
+ // methods for chrome and add-ons
+
+ /**
+ * Discover the IdP for an identity
+ *
+ * @param aIdentity
+ * (string) the email we're logging in with
+ *
+ * @param aCallback
+ * (function) callback to invoke on completion
+ * with first-positional parameter the error.
+ */
+ _discoverIdentityProvider: function _discoverIdentityProvider(aIdentity, aCallback) {
+ // XXX bug 767610 - validate email address call
+ // When that is available, we can remove this custom parser
+ var parsedEmail = this.parseEmail(aIdentity);
+ if (parsedEmail === null) {
+ return aCallback("Could not parse email: " + aIdentity);
+ }
+ log("_discoverIdentityProvider: identity:", aIdentity, "domain:", parsedEmail.domain);
+
+ this._fetchWellKnownFile(parsedEmail.domain, function fetchedWellKnown(err, idpParams) {
+ // idpParams includes the pk, authorization url, and
+ // provisioning url.
+
+ // XXX bug 769861 follow any authority delegations
+ // if no well-known at any point in the delegation
+ // fall back to browserid.org as IdP
+ return aCallback(err, idpParams);
+ });
+ },
+
+ /**
+ * Fetch the well-known file from the domain.
+ *
+ * @param aDomain
+ *
+ * @param aScheme
+ * (string) (optional) Protocol to use. Default is https.
+ * This is necessary because we are unable to test
+ * https.
+ *
+ * @param aCallback
+ *
+ */
+ _fetchWellKnownFile: function _fetchWellKnownFile(aDomain, aCallback, aScheme='https') {
+ // XXX bug 769854 make tests https and remove aScheme option
+ let url = aScheme + '://' + aDomain + "/.well-known/browserid";
+ log("_fetchWellKnownFile:", url);
+
+ // this appears to be a more successful way to get at xmlhttprequest (which supposedly will close with a window
+ let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .getService(Ci.nsIXMLHttpRequest);
+
+ // XXX bug 769865 gracefully handle being off-line
+ // XXX bug 769866 decide on how to handle redirects
+ req.open("GET", url, true);
+ req.responseType = "json";
+ req.mozBackgroundRequest = true;
+ req.onload = function _fetchWellKnownFile_onload() {
+ if (req.status < 200 || req.status >= 400) {
+ log("_fetchWellKnownFile", url, ": server returned status:", req.status);
+ return aCallback("Error");
+ }
+ try {
+ let idpParams = req.response;
+
+ // Verify that the IdP returned a valid configuration
+ if (! (idpParams.provisioning &&
+ idpParams.authentication &&
+ idpParams['public-key'])) {
+ let errStr= "Invalid well-known file from: " + aDomain;
+ log("_fetchWellKnownFile:", errStr);
+ return aCallback(errStr);
+ }
+
+ let callbackObj = {
+ domain: aDomain,
+ idpParams: idpParams,
+ };
+ log("_fetchWellKnownFile result: ", callbackObj);
+ // Yay. Valid IdP configuration for the domain.
+ return aCallback(null, callbackObj);
+
+ } catch (err) {
+ reportError("_fetchWellKnownFile", "Bad configuration from", aDomain, err);
+ return aCallback(err.toString());
+ }
+ };
+ req.onerror = function _fetchWellKnownFile_onerror() {
+ log("_fetchWellKnownFile", "ERROR:", req.status, req.statusText);
+ log("ERROR: _fetchWellKnownFile:", err);
+ return aCallback("Error");
+ };
+ req.send(null);
+ },
+
+};
+
+let IdentityService = new IDService();
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/IdentityCryptoService.cpp
@@ -0,0 +1,601 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "nsIIdentityCryptoService.h"
+#include "mozilla/ModuleUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsNSSShutDown.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+#include "nsCOMPtr.h"
+#include "nsStringGlue.h"
+#include "mozilla/Base64.h"
+
+#include "nss.h"
+#include "pk11pub.h"
+#include "secmod.h"
+#include "secerr.h"
+#include "keyhi.h"
+#include "cryptohi.h"
+
+using namespace mozilla;
+
+namespace {
+
+void
+HexEncode(const SECItem * it, nsACString & result)
+{
+ const char * digits = "0123456789ABCDEF";
+ result.SetCapacity((it->len * 2) + 1);
+ result.SetLength(it->len * 2);
+ char * p = result.BeginWriting();
+ for (unsigned int i = 0; i < it->len; ++i) {
+ *p++ = digits[it->data[i] >> 4];
+ *p++ = digits[it->data[i] & 0x0f];
+ }
+}
+
+nsresult
+Base64UrlEncodeImpl(const nsACString & utf8Input, nsACString & result)
+{
+ nsresult rv = Base64Encode(utf8Input, result);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsACString::char_type * out = result.BeginWriting();
+ nsACString::size_type length = result.Length();
+ // base64url encoding is defined in RFC 4648. It replaces the last two
+ // alphabet characters of base64 encoding with '-' and '_' respectively.
+ for (unsigned int i = 0; i < length; ++i) {
+ if (out[i] == '+') {
+ out[i] = '-';
+ } else if (out[i] == '/') {
+ out[i] = '_';
+ }
+ }
+
+ return NS_OK;
+}
+
+
+nsresult
+PRErrorCode_to_nsresult(PRErrorCode error)
+{
+ if (!error) {
+ MOZ_NOT_REACHED("Function failed without calling PR_GetError");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // From NSSErrorsService::GetXPCOMFromNSSError
+ return NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_SECURITY, -1 * error);
+}
+
+// IMPORTANT: This must be called immediately after the function returning the
+// SECStatus result. The recommended usage is:
+// nsresult rv = MapSECStatus(f(x, y, z));
+nsresult
+MapSECStatus(SECStatus rv)
+{
+ if (rv == SECSuccess)
+ return NS_OK;
+
+ PRErrorCode error = PR_GetError();
+ return PRErrorCode_to_nsresult(error);
+}
+
+#define DSA_KEY_TYPE_STRING (NS_LITERAL_CSTRING("DS160"))
+#define RSA_KEY_TYPE_STRING (NS_LITERAL_CSTRING("RS256"))
+
+class KeyPair : public nsIIdentityKeyPair, public nsNSSShutDownObject
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIIDENTITYKEYPAIR
+
+ KeyPair(SECKEYPrivateKey* aPrivateKey, SECKEYPublicKey* aPublicKey);
+
+private:
+ ~KeyPair()
+ {
+ destructorSafeDestroyNSSReference();
+ shutdown(calledFromObject);
+ }
+
+ void virtualDestroyNSSReference() MOZ_OVERRIDE
+ {
+ destructorSafeDestroyNSSReference();
+ }
+
+ void destructorSafeDestroyNSSReference()
+ {
+ nsNSSShutDownPreventionLock locker;
+ if (isAlreadyShutDown())
+ return;
+
+ SECKEY_DestroyPrivateKey(mPrivateKey);
+ mPrivateKey = NULL;
+ SECKEY_DestroyPublicKey(mPublicKey);
+ mPublicKey = NULL;
+ }
+
+ SECKEYPrivateKey * mPrivateKey;
+ SECKEYPublicKey * mPublicKey;
+
+ KeyPair(const KeyPair &) MOZ_DELETE;
+ void operator=(const KeyPair &) MOZ_DELETE;
+};
+
+NS_IMPL_THREADSAFE_ISUPPORTS1(KeyPair, nsIIdentityKeyPair)
+
+class KeyGenRunnable : public nsRunnable, public nsNSSShutDownObject
+{
+public:
+ NS_DECL_NSIRUNNABLE
+
+ KeyGenRunnable(KeyType keyType, nsIIdentityKeyGenCallback * aCallback);
+
+private:
+ ~KeyGenRunnable()
+ {
+ destructorSafeDestroyNSSReference();
+ shutdown(calledFromObject);
+ }
+
+ virtual void virtualDestroyNSSReference() MOZ_OVERRIDE
+ {
+ destructorSafeDestroyNSSReference();
+ }
+
+ void destructorSafeDestroyNSSReference()
+ {
+ nsNSSShutDownPreventionLock locker;
+ if (isAlreadyShutDown())
+ return;
+
+ mKeyPair = NULL;
+ }
+
+ const KeyType mKeyType; // in
+ nsCOMPtr<nsIIdentityKeyGenCallback> mCallback; // in
+ nsresult mRv; // out
+ nsCOMPtr<KeyPair> mKeyPair; // out
+
+ KeyGenRunnable(const KeyGenRunnable &) MOZ_DELETE;
+ void operator=(const KeyGenRunnable &) MOZ_DELETE;
+};
+
+class SignRunnable : public nsRunnable, public nsNSSShutDownObject
+{
+public:
+ NS_DECL_NSIRUNNABLE
+
+ SignRunnable(const nsACString & textToSign, SECKEYPrivateKey * privateKey,
+ nsIIdentitySignCallback * aCallback);
+
+private:
+ ~SignRunnable()
+ {
+ destructorSafeDestroyNSSReference();
+ shutdown(calledFromObject);
+ }
+
+ void virtualDestroyNSSReference() MOZ_OVERRIDE
+ {
+ destructorSafeDestroyNSSReference();
+ }
+
+ void destructorSafeDestroyNSSReference()
+ {
+ nsNSSShutDownPreventionLock locker;
+ if (isAlreadyShutDown())
+ return;
+
+ SECKEY_DestroyPrivateKey(mPrivateKey);
+ mPrivateKey = NULL;
+ }
+
+ const nsCString mTextToSign; // in
+ SECKEYPrivateKey* mPrivateKey; // in
+ const nsCOMPtr<nsIIdentitySignCallback> mCallback; // in
+ nsresult mRv; // out
+ nsCString mSignature; // out
+
+private:
+ SignRunnable(const SignRunnable &) MOZ_DELETE;
+ void operator=(const SignRunnable &) MOZ_DELETE;
+};
+
+class IdentityCryptoService : public nsIIdentityCryptoService
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIIDENTITYCRYPTOSERVICE
+
+ IdentityCryptoService() { }
+ nsresult Init()
+ {
+ nsresult rv;
+ nsCOMPtr<nsISupports> dummyUsedToEnsureNSSIsInitialized
+ = do_GetService("@mozilla.org/psm;1");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+private:
+ IdentityCryptoService(const KeyPair &) MOZ_DELETE;
+ void operator=(const IdentityCryptoService &) MOZ_DELETE;
+};
+
+NS_IMPL_THREADSAFE_ISUPPORTS1(IdentityCryptoService, nsIIdentityCryptoService)
+
+NS_IMETHODIMP
+IdentityCryptoService::GenerateKeyPair(
+ const nsACString & keyTypeString, nsIIdentityKeyGenCallback * callback)
+{
+ KeyType keyType;
+ if (keyTypeString.Equals(RSA_KEY_TYPE_STRING)) {
+ keyType = rsaKey;
+ } else if (keyTypeString.Equals(DSA_KEY_TYPE_STRING)) {
+ keyType = dsaKey;
+ } else {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsCOMPtr<nsIRunnable> r = new KeyGenRunnable(keyType, callback);
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewThread(getter_AddRefs(thread), r);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+IdentityCryptoService::Base64UrlEncode(const nsACString & utf8Input,
+ nsACString & result)
+{
+ return Base64UrlEncodeImpl(utf8Input, result);
+}
+
+KeyPair::KeyPair(SECKEYPrivateKey * privateKey, SECKEYPublicKey * publicKey)
+ : mPrivateKey(privateKey)
+ , mPublicKey(publicKey)
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexRSAPublicKeyExponent(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == rsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.rsa.publicExponent, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexRSAPublicKeyModulus(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == rsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.rsa.modulus, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexDSAPrime(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.dsa.params.prime, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexDSASubPrime(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.dsa.params.subPrime, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexDSAGenerator(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.dsa.params.base, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetHexDSAPublicValue(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mPublicKey->keyType == dsaKey, NS_ERROR_NOT_AVAILABLE);
+ HexEncode(&mPublicKey->u.dsa.publicValue, result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+KeyPair::GetKeyType(nsACString & result)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(mPublicKey, NS_ERROR_NOT_AVAILABLE);
+
+ switch (mPublicKey->keyType) {
+ case rsaKey: result = RSA_KEY_TYPE_STRING; return NS_OK;
+ case dsaKey: result = DSA_KEY_TYPE_STRING; return NS_OK;
+ default: return NS_ERROR_UNEXPECTED;
+ }
+}
+
+NS_IMETHODIMP
+KeyPair::Sign(const nsACString & textToSign,
+ nsIIdentitySignCallback* callback)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIRunnable> r = new SignRunnable(textToSign, mPrivateKey,
+ callback);
+
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewThread(getter_AddRefs(thread), r);
+ return rv;
+}
+
+KeyGenRunnable::KeyGenRunnable(KeyType keyType,
+ nsIIdentityKeyGenCallback * callback)
+ : mKeyType(keyType)
+ , mCallback(callback)
+ , mRv(NS_ERROR_NOT_INITIALIZED)
+{
+}
+
+MOZ_WARN_UNUSED_RESULT nsresult
+GenerateKeyPair(PK11SlotInfo * slot,
+ NS_OUTPARAM SECKEYPrivateKey ** privateKey,
+ NS_OUTPARAM SECKEYPublicKey ** publicKey,
+ CK_MECHANISM_TYPE mechanism,
+ void * params)
+{
+ *publicKey = NULL;
+ *privateKey = PK11_GenerateKeyPair(slot, mechanism, params, publicKey,
+ PR_FALSE /*isPerm*/,
+ PR_TRUE /*isSensitive*/,
+ NULL /*&pwdata*/);
+ if (!*privateKey) {
+ MOZ_ASSERT(!*publicKey);
+ return PRErrorCode_to_nsresult(PR_GetError());
+ }
+ if (!*publicKey) {
+ SECKEY_DestroyPrivateKey(*privateKey);
+ *privateKey = NULL;
+ MOZ_NOT_REACHED("PK11_GnerateKeyPair returned private key without public "
+ "key");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ return NS_OK;
+}
+
+
+MOZ_WARN_UNUSED_RESULT nsresult
+GenerateRSAKeyPair(PK11SlotInfo * slot,
+ NS_OUTPARAM SECKEYPrivateKey ** privateKey,
+ NS_OUTPARAM SECKEYPublicKey ** publicKey)
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ PK11RSAGenParams rsaParams;
+ rsaParams.keySizeInBits = 2048;
+ rsaParams.pe = 0x10001;
+ return GenerateKeyPair(slot, privateKey, publicKey, CKM_RSA_PKCS_KEY_PAIR_GEN,
+ &rsaParams);
+}
+
+MOZ_WARN_UNUSED_RESULT nsresult
+GenerateDSAKeyPair(PK11SlotInfo * slot,
+ NS_OUTPARAM SECKEYPrivateKey ** privateKey,
+ NS_OUTPARAM SECKEYPublicKey ** publicKey)
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // XXX: These could probably be static const arrays, but this way we avoid
+ // compiler warnings and also we avoid having to worry much about whether the
+ // functions that take these inputs will (unexpectedly) modify them.
+
+ // Using NIST parameters. Some other BrowserID components require that these
+ // exact parameters are used.
+ PRUint8 P[] = {
+ 0xFF,0x60,0x04,0x83,0xDB,0x6A,0xBF,0xC5,0xB4,0x5E,0xAB,0x78,
+ 0x59,0x4B,0x35,0x33,0xD5,0x50,0xD9,0xF1,0xBF,0x2A,0x99,0x2A,
+ 0x7A,0x8D,0xAA,0x6D,0xC3,0x4F,0x80,0x45,0xAD,0x4E,0x6E,0x0C,
+ 0x42,0x9D,0x33,0x4E,0xEE,0xAA,0xEF,0xD7,0xE2,0x3D,0x48,0x10,
+ 0xBE,0x00,0xE4,0xCC,0x14,0x92,0xCB,0xA3,0x25,0xBA,0x81,0xFF,
+ 0x2D,0x5A,0x5B,0x30,0x5A,0x8D,0x17,0xEB,0x3B,0xF4,0xA0,0x6A,
+ 0x34,0x9D,0x39,0x2E,0x00,0xD3,0x29,0x74,0x4A,0x51,0x79,0x38,
+ 0x03,0x44,0xE8,0x2A,0x18,0xC4,0x79,0x33,0x43,0x8F,0x89,0x1E,
+ 0x22,0xAE,0xEF,0x81,0x2D,0x69,0xC8,0xF7,0x5E,0x32,0x6C,0xB7,
+ 0x0E,0xA0,0x00,0xC3,0xF7,0x76,0xDF,0xDB,0xD6,0x04,0x63,0x8C,
+ 0x2E,0xF7,0x17,0xFC,0x26,0xD0,0x2E,0x17
+ };
+
+ PRUint8 Q[] = {
+ 0xE2,0x1E,0x04,0xF9,0x11,0xD1,0xED,0x79,0x91,0x00,0x8E,0xCA,
+ 0xAB,0x3B,0xF7,0x75,0x98,0x43,0x09,0xC3
+ };
+
+ PRUint8 G[] = {
+ 0xC5,0x2A,0x4A,0x0F,0xF3,0xB7,0xE6,0x1F,0xDF,0x18,0x67,0xCE,
+ 0x84,0x13,0x83,0x69,0xA6,0x15,0x4F,0x4A,0xFA,0x92,0x96,0x6E,
+ 0x3C,0x82,0x7E,0x25,0xCF,0xA6,0xCF,0x50,0x8B,0x90,0xE5,0xDE,
+ 0x41,0x9E,0x13,0x37,0xE0,0x7A,0x2E,0x9E,0x2A,0x3C,0xD5,0xDE,
+ 0xA7,0x04,0xD1,0x75,0xF8,0xEB,0xF6,0xAF,0x39,0x7D,0x69,0xE1,
+ 0x10,0xB9,0x6A,0xFB,0x17,0xC7,0xA0,0x32,0x59,0x32,0x9E,0x48,
+ 0x29,0xB0,0xD0,0x3B,0xBC,0x78,0x96,0xB1,0x5B,0x4A,0xDE,0x53,
+ 0xE1,0x30,0x85,0x8C,0xC3,0x4D,0x96,0x26,0x9A,0xA8,0x90,0x41,
+ 0xF4,0x09,0x13,0x6C,0x72,0x42,0xA3,0x88,0x95,0xC9,0xD5,0xBC,
+ 0xCA,0xD4,0xF3,0x89,0xAF,0x1D,0x7A,0x4B,0xD1,0x39,0x8B,0xD0,
+ 0x72,0xDF,0xFA,0x89,0x62,0x33,0x39,0x7A
+ };
+
+ MOZ_STATIC_ASSERT(PR_ARRAY_SIZE(P) == 1024 / PR_BITS_PER_BYTE, "bad DSA P");
+ MOZ_STATIC_ASSERT(PR_ARRAY_SIZE(Q) == 160 / PR_BITS_PER_BYTE, "bad DSA Q");
+ MOZ_STATIC_ASSERT(PR_ARRAY_SIZE(G) == 1024 / PR_BITS_PER_BYTE, "bad DSA G");
+
+ PQGParams pqgParams = {
+ NULL /*arena*/,
+ { siBuffer, P, PR_ARRAY_SIZE(P) },
+ { siBuffer, Q, PR_ARRAY_SIZE(Q) },
+ { siBuffer, G, PR_ARRAY_SIZE(G) }
+ };
+
+ return GenerateKeyPair(slot, privateKey, publicKey, CKM_DSA_KEY_PAIR_GEN,
+ &pqgParams);
+}
+
+NS_IMETHODIMP
+KeyGenRunnable::Run()
+{
+ if (!NS_IsMainThread()) {
+ nsNSSShutDownPreventionLock locker;
+ if (isAlreadyShutDown()) {
+ mRv = NS_ERROR_NOT_AVAILABLE;
+ } else {
+ // We always want to use the internal slot for BrowserID; in particular,
+ // we want to avoid smartcard slots.
+ PK11SlotInfo *slot = PK11_GetInternalSlot();
+ if (!slot) {
+ mRv = NS_ERROR_UNEXPECTED;
+ } else {
+ SECKEYPrivateKey *privk = NULL;
+ SECKEYPublicKey *pubk = NULL;
+
+ switch (mKeyType) {
+ case rsaKey:
+ mRv = GenerateRSAKeyPair(slot, &privk, &pubk);
+ break;
+ case dsaKey:
+ mRv = GenerateDSAKeyPair(slot, &privk, &pubk);
+ break;
+ default:
+ MOZ_NOT_REACHED("unknown key type");
+ mRv = NS_ERROR_UNEXPECTED;
+ }
+
+ PK11_FreeSlot(slot);
+
+ if (NS_SUCCEEDED(mRv)) {
+ MOZ_ASSERT(privk);
+ MOZ_ASSERT(pubk);
+ // mKeyPair will take over ownership of privk and pubk
+ mKeyPair = new KeyPair(privk, pubk);
+ }
+ }
+ }
+
+ NS_DispatchToMainThread(this);
+ } else {
+ // Back on Main Thread
+ (void) mCallback->GenerateKeyPairFinished(mRv, mKeyPair);
+ }
+ return NS_OK;
+}
+
+SignRunnable::SignRunnable(const nsACString & aText,
+ SECKEYPrivateKey * privateKey,
+ nsIIdentitySignCallback * aCallback)
+ : mTextToSign(aText)
+ , mPrivateKey(SECKEY_CopyPrivateKey(privateKey))
+ , mCallback(aCallback)
+ , mRv(NS_ERROR_NOT_INITIALIZED)
+{
+}
+
+NS_IMETHODIMP
+SignRunnable::Run()
+{
+ if (!NS_IsMainThread()) {
+ nsNSSShutDownPreventionLock locker;
+ if (isAlreadyShutDown()) {
+ mRv = NS_ERROR_NOT_AVAILABLE;
+ } else {
+ // We need the output in PKCS#11 format, not DER encoding, so we must use
+ // PK11_HashBuf and PK11_Sign instead of SEC_SignData.
+
+ SECItem sig = { siBuffer, NULL, 0 };
+ int sigLength = PK11_SignatureLen(mPrivateKey);
+ if (sigLength <= 0) {
+ mRv = PRErrorCode_to_nsresult(PR_GetError());
+ } else if (!SECITEM_AllocItem(NULL, &sig, sigLength)) {
+ mRv = PRErrorCode_to_nsresult(PR_GetError());
+ } else {
+ PRUint8 hash[32]; // big enough for SHA-1 or SHA-256
+ SECOidTag hashAlg = mPrivateKey->keyType == dsaKey ? SEC_OID_SHA1
+ : SEC_OID_SHA256;
+ SECItem hashItem = { siBuffer, hash,
+ hashAlg == SEC_OID_SHA1 ? 20 : 32 };
+
+ mRv = MapSECStatus(PK11_HashBuf(hashAlg, hash,
+ const_cast<PRUint8*>(reinterpret_cast<const PRUint8 *>(
+ mTextToSign.get())),
+ mTextToSign.Length()));
+ if (NS_SUCCEEDED(mRv)) {
+ mRv = MapSECStatus(PK11_Sign(mPrivateKey, &sig, &hashItem));
+ }
+ if (NS_SUCCEEDED(mRv)) {
+ nsDependentCSubstring sigString(
+ reinterpret_cast<const char*>(sig.data), sig.len);
+ mRv = Base64UrlEncodeImpl(sigString, mSignature);
+ }
+ SECITEM_FreeItem(&sig, false);
+ }
+ }
+
+ NS_DispatchToMainThread(this);
+ } else {
+ // Back on Main Thread
+ (void) mCallback->SignFinished(mRv, mSignature);
+ }
+
+ return NS_OK;
+}
+
+// XPCOM module registration
+
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(IdentityCryptoService, Init)
+
+#define NS_IDENTITYCRYPTOSERVICE_CID \
+ {0xbea13a3a, 0x44e8, 0x4d7f, {0xa0, 0xa2, 0x2c, 0x67, 0xf8, 0x4e, 0x3a, 0x97}}
+
+NS_DEFINE_NAMED_CID(NS_IDENTITYCRYPTOSERVICE_CID);
+
+const mozilla::Module::CIDEntry kCIDs[] = {
+ { &kNS_IDENTITYCRYPTOSERVICE_CID, false, NULL, IdentityCryptoServiceConstructor },
+ { NULL }
+};
+
+const mozilla::Module::ContractIDEntry kContracts[] = {
+ { "@mozilla.org/identity/crypto-service;1", &kNS_IDENTITYCRYPTOSERVICE_CID },
+ { NULL }
+};
+
+const mozilla::Module kModule = {
+ mozilla::Module::kVersion,
+ kCIDs,
+ kContracts
+};
+
+} // unnamed namespace
+
+NSMODULE_DEFN(identity) = &kModule;
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/IdentityProvider.jsm
@@ -0,0 +1,477 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+Cu.import("resource://gre/modules/identity/Sandbox.jsm");
+
+const EXPORTED_SYMBOLS = ["IdentityProvider"];
+const FALLBACK_PROVIDER = "browserid.org";
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "jwcrypto",
+ "resource://gre/modules/identity/jwcrypto.jsm");
+
+function log(...aMessageArgs) {
+ Logger.log(["IDP"].concat(aMessageArgs));
+}
+function reportError(...aMessageArgs) {
+ Logger.reportError(["IDP"].concat(aMessageArgs));
+}
+
+
+function IdentityProviderService() {
+ XPCOMUtils.defineLazyModuleGetter(this,
+ "_store",
+ "resource://gre/modules/identity/IdentityStore.jsm",
+ "IdentityStore");
+
+ this.reset();
+}
+
+IdentityProviderService.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "quit-application-granted":
+ Services.obs.removeObserver(this, "quit-application-granted");
+ this.shutdown();
+ break;
+ }
+ },
+
+ reset: function IDP_reset() {
+ // Clear the provisioning flows. Provision flows contain an
+ // identity, idpParams (how to reach the IdP to provision and
+ // authenticate), a callback (a completion callback for when things
+ // are done), and a provisioningFrame (which is the provisioning
+ // sandbox). Additionally, two callbacks will be attached:
+ // beginProvisioningCallback and genKeyPairCallback.
+ this._provisionFlows = {};
+
+ // Clear the authentication flows. Authentication flows attach
+ // to provision flows. In the process of provisioning an id, it
+ // may be necessary to authenticate with an IdP. The authentication
+ // flow maintains the state of that authentication process.
+ this._authenticationFlows = {};
+ },
+
+ getProvisionFlow: function getProvisionFlow(aProvId, aErrBack) {
+ let provFlow = this._provisionFlows[aProvId];
+ if (provFlow) {
+ return provFlow;
+ }
+
+ let err = "No provisioning flow found with id " + aProvId;
+ log("ERROR:", err);
+ if (typeof aErrBack === 'function') {
+ aErrBack(err);
+ }
+ },
+
+ shutdown: function RP_shutdown() {
+ this.reset();
+ Services.obs.removeObserver(this, "quit-application-granted");
+ },
+
+ get securityLevel() {
+ return 1;
+ },
+
+ get certDuration() {
+ switch(this.securityLevel) {
+ default:
+ return 3600;
+ }
+ },
+
+ /**
+ * Provision an Identity
+ *
+ * @param aIdentity
+ * (string) the email we're logging in with
+ *
+ * @param aIDPParams
+ * (object) parameters of the IdP
+ *
+ * @param aCallback
+ * (function) callback to invoke on completion
+ * with first-positional parameter the error.
+ */
+ _provisionIdentity: function _provisionIdentity(aIdentity, aIDPParams, aProvId, aCallback) {
+ let provPath = aIDPParams.idpParams.provisioning;
+ let url = Services.io.newURI("https://" + aIDPParams.domain, null, null).resolve(provPath);
+ log("_provisionIdentity: identity:", aIdentity, "url:", url);
+
+ // If aProvId is not null, then we already have a flow
+ // with a sandbox. Otherwise, get a sandbox and create a
+ // new provision flow.
+
+ if (aProvId) {
+ // Re-use an existing sandbox
+ log("_provisionIdentity: re-using sandbox in provisioning flow with id:", aProvId);
+ this._provisionFlows[aProvId].provisioningSandbox.reload();
+
+ } else {
+ this._createProvisioningSandbox(url, function createdSandbox(aSandbox) {
+ // create a provisioning flow, using the sandbox id, and
+ // stash callback associated with this provisioning workflow.
+
+ let provId = aSandbox.id;
+ this._provisionFlows[provId] = {
+ identity: aIdentity,
+ idpParams: aIDPParams,
+ securityLevel: this.securityLevel,
+ provisioningSandbox: aSandbox,
+ callback: function doCallback(aErr) {
+ aCallback(aErr, provId);
+ },
+ };
+
+ log("_provisionIdentity: Created sandbox and provisioning flow with id:", provId);
+ // XXX bug 769862 - provisioning flow should timeout after N seconds
+
+ }.bind(this));
+ }
+ },
+
+ // DOM Methods
+ /**
+ * the provisioning iframe sandbox has called navigator.id.beginProvisioning()
+ *
+ * @param aCaller
+ * (object) the iframe sandbox caller with all callbacks and
+ * other information. Callbacks include:
+ * - doBeginProvisioningCallback(id, duration_s)
+ * - doGenKeyPairCallback(pk)
+ */
+ beginProvisioning: function beginProvisioning(aCaller) {
+ log("beginProvisioning:", aCaller.id);
+
+ // Expect a flow for this caller already to be underway.
+ let provFlow = this.getProvisionFlow(aCaller.id, aCaller.doError);
+
+ // keep the caller object around
+ provFlow.caller = aCaller;
+
+ let identity = provFlow.identity;
+ let frame = provFlow.provisioningFrame;
+
+ // Determine recommended length of cert.
+ let duration = this.certDuration;
+
+ // Make a record that we have begun provisioning. This is required
+ // for genKeyPair.
+ provFlow.didBeginProvisioning = true;
+
+ // Let the sandbox know to invoke the callback to beginProvisioning with
+ // the identity and cert length.
+ return aCaller.doBeginProvisioningCallback(identity, duration);
+ },
+
+ /**
+ * the provisioning iframe sandbox has called
+ * navigator.id.raiseProvisioningFailure()
+ *
+ * @param aProvId
+ * (int) the identifier of the provisioning flow tied to that sandbox
+ * @param aReason
+ */
+ raiseProvisioningFailure: function raiseProvisioningFailure(aProvId, aReason) {
+ reportError("Provisioning failure", aReason);
+
+ // look up the provisioning caller and its callback
+ let provFlow = this.getProvisionFlow(aProvId);
+
+ // Sandbox is deleted in _cleanUpProvisionFlow in case we re-use it.
+
+ // This may be either a "soft" or "hard" fail. If it's a
+ // soft fail, we'll flow through setAuthenticationFlow, where
+ // the provision flow data will be copied into a new auth
+ // flow. If it's a hard fail, then the callback will be
+ // responsible for cleaning up the now defunct provision flow.
+
+ // invoke the callback with an error.
+ provFlow.callback(aReason);
+ },
+
+ /**
+ * When navigator.id.genKeyPair is called from provisioning iframe sandbox.
+ * Generates a keypair for the current user being provisioned.
+ *
+ * @param aProvId
+ * (int) the identifier of the provisioning caller tied to that sandbox
+ *
+ * It is an error to call genKeypair without receiving the callback for
+ * the beginProvisioning() call first.
+ */
+ genKeyPair: function genKeyPair(aProvId) {
+ // Look up the provisioning caller and make sure it's valid.
+ let provFlow = this.getProvisionFlow(aProvId);
+
+ if (!provFlow.didBeginProvisioning) {
+ let errStr = "ERROR: genKeyPair called before beginProvisioning";
+ log(errStr);
+ provFlow.callback(errStr);
+ return;
+ }
+
+ // Ok generate a keypair
+ jwcrypto.generateKeyPair(jwcrypto.ALGORITHMS.DS160, function gkpCb(err, kp) {
+ log("in gkp callback");
+ if (err) {
+ log("ERROR: genKeyPair:" + err);
+ provFlow.callback(err);
+ return;
+ }
+
+ provFlow.kp = kp;
+
+ // Serialize the publicKey of the keypair and send it back to the
+ // sandbox.
+ log("genKeyPair: generated keypair for provisioning flow with id:", aProvId);
+ provFlow.caller.doGenKeyPairCallback(provFlow.kp.serializedPublicKey);
+ }.bind(this));
+ },
+
+ /**
+ * When navigator.id.registerCertificate is called from provisioning iframe
+ * sandbox.
+ *
+ * Sets the certificate for the user for which a certificate was requested
+ * via a preceding call to beginProvisioning (and genKeypair).
+ *
+ * @param aProvId
+ * (integer) the identifier of the provisioning caller tied to that
+ * sandbox
+ *
+ * @param aCert
+ * (String) A JWT representing the signed certificate for the user
+ * being provisioned, provided by the IdP.
+ */
+ registerCertificate: function registerCertificate(aProvId, aCert) {
+ log("registerCertificate:", aProvId, aCert);
+
+ // look up provisioning caller, make sure it's valid.
+ let provFlow = this.getProvisionFlow(aProvId);
+
+ if (!provFlow.caller) {
+ reportError("registerCertificate", "No provision flow or caller");
+ return;
+ }
+ if (!provFlow.kp) {
+ let errStr = "Cannot register a certificate without a keypair";
+ reportError("registerCertificate", errStr);
+ provFlow.callback(errStr);
+ return;
+ }
+
+ // store the keypair and certificate just provided in IDStore.
+ this._store.addIdentity(provFlow.identity, provFlow.kp, aCert);
+
+ // Great success!
+ provFlow.callback(null);
+
+ // Clean up the flow.
+ this._cleanUpProvisionFlow(aProvId);
+ },
+
+ /**
+ * Begin the authentication process with an IdP
+ *
+ * @param aProvId
+ * (int) the identifier of the provisioning flow which failed
+ *
+ * @param aCallback
+ * (function) to invoke upon completion, with
+ * first-positional-param error.
+ */
+ _doAuthentication: function _doAuthentication(aProvId, aIDPParams) {
+ log("_doAuthentication: provId:", aProvId, "idpParams:", aIDPParams);
+ // create an authentication caller and its identifier AuthId
+ // stash aIdentity, idpparams, and callback in it.
+
+ // extract authentication URL from idpParams
+ let authPath = aIDPParams.idpParams.authentication;
+ let authURI = Services.io.newURI("https://" + aIDPParams.domain, null, null).resolve(authPath);
+
+ // beginAuthenticationFlow causes the "identity-auth" topic to be
+ // observed. Since it's sending a notification to the DOM, there's
+ // no callback. We wait for the DOM to trigger the next phase of
+ // provisioning.
+ this._beginAuthenticationFlow(aProvId, authURI);
+
+ // either we bind the AuthID to the sandbox ourselves, or UX does that,
+ // in which case we need to tell UX the AuthId.
+ // Currently, the UX creates the UI and gets the AuthId from the window
+ // and sets is with setAuthenticationFlow
+ },
+
+ /**
+ * The authentication frame has called navigator.id.beginAuthentication
+ *
+ * IMPORTANT: the aCaller is *always* non-null, even if this is called from
+ * a regular content page. We have to make sure, on every DOM call, that
+ * aCaller is an expected authentication-flow identifier. If not, we throw
+ * an error or something.
+ *
+ * @param aCaller
+ * (object) the authentication caller
+ *
+ */
+ beginAuthentication: function beginAuthentication(aCaller) {
+ log("beginAuthentication: caller id:", aCaller.id);
+
+ // Begin the authentication flow after having concluded a provisioning
+ // flow. The aCaller that the DOM gives us will have the same ID as
+ // the provisioning flow we just concluded. (see setAuthenticationFlow)
+ let authFlow = this._authenticationFlows[aCaller.id];
+ if (!authFlow) {
+ return aCaller.doError("beginAuthentication: no flow for caller id", aCaller.id);
+ }
+
+ authFlow.caller = aCaller;
+
+ let identity = this._provisionFlows[authFlow.provId].identity;
+
+ // tell the UI to start the authentication process
+ log("beginAuthentication: authFlow:", aCaller.id, "identity:", identity);
+ return authFlow.caller.doBeginAuthenticationCallback(identity);
+ },
+
+ /**
+ * The auth frame has called navigator.id.completeAuthentication
+ *
+ * @param aAuthId
+ * (int) the identifier of the authentication caller tied to that sandbox
+ *
+ */
+ completeAuthentication: function completeAuthentication(aAuthId) {
+ log("completeAuthentication:", aAuthId);
+
+ // look up the AuthId caller, and get its callback.
+ let authFlow = this._authenticationFlows[aAuthId];
+ if (!authFlow) {
+ reportError("completeAuthentication", "No auth flow with id", aAuthId);
+ return;
+ }
+ let provId = authFlow.provId;
+
+ // delete caller
+ delete authFlow['caller'];
+ delete this._authenticationFlows[aAuthId];
+
+ let provFlow = this.getProvisionFlow(provId);
+ provFlow.didAuthentication = true;
+ let subject = {
+ rpId: provFlow.rpId,
+ identity: provFlow.identity,
+ };
+ Services.obs.notifyObservers({ wrappedJSObject: subject }, "identity-auth-complete", aAuthId);
+ },
+
+ /**
+ * The auth frame has called navigator.id.cancelAuthentication
+ *
+ * @param aAuthId
+ * (int) the identifier of the authentication caller
+ *
+ */
+ cancelAuthentication: function cancelAuthentication(aAuthId) {
+ log("cancelAuthentication:", aAuthId);
+
+ // look up the AuthId caller, and get its callback.
+ let authFlow = this._authenticationFlows[aAuthId];
+ if (!authFlow) {
+ reportError("cancelAuthentication", "No auth flow with id:", aAuthId);
+ return;
+ }
+ let provId = authFlow.provId;
+
+ // delete caller
+ delete authFlow['caller'];
+ delete this._authenticationFlows[aAuthId];
+
+ let provFlow = this.getProvisionFlow(provId);
+ provFlow.didAuthentication = true;
+ Services.obs.notifyObservers(null, "identity-auth-complete", aAuthId);
+
+ // invoke callback with ERROR.
+ let errStr = "Authentication canceled by IDP";
+ log("ERROR: cancelAuthentication:", errStr);
+ provFlow.callback(errStr);
+ },
+
+ /**
+ * Called by the UI to set the ID and caller for the authentication flow after it gets its ID
+ */
+ setAuthenticationFlow: function(aAuthId, aProvId) {
+ // this is the transition point between the two flows,
+ // provision and authenticate. We tell the auth flow which
+ // provisioning flow it is started from.
+ log("setAuthenticationFlow: authId:", aAuthId, "provId:", aProvId);
+ this._authenticationFlows[aAuthId] = { provId: aProvId };
+ this._provisionFlows[aProvId].authId = aAuthId;
+ },
+
+ /**
+ * Load the provisioning URL in a hidden frame to start the provisioning
+ * process.
+ */
+ _createProvisioningSandbox: function _createProvisioningSandbox(aURL, aCallback) {
+ log("_createProvisioningSandbox:", aURL);
+ new Sandbox(aURL, aCallback);
+ },
+
+ /**
+ * Load the authentication UI to start the authentication process.
+ */
+ _beginAuthenticationFlow: function _beginAuthenticationFlow(aProvId, aURL) {
+ log("_beginAuthenticationFlow:", aProvId, aURL);
+ let propBag = {provId: aProvId};
+
+ Services.obs.notifyObservers({wrappedJSObject:propBag}, "identity-auth", aURL);
+ },
+
+ /**
+ * Clean up a provision flow and the authentication flow and sandbox
+ * that may be attached to it.
+ */
+ _cleanUpProvisionFlow: function _cleanUpProvisionFlow(aProvId) {
+ log('_cleanUpProvisionFlow:', aProvId);
+ let prov = this._provisionFlows[aProvId];
+
+ // Clean up the sandbox, if there is one.
+ if (prov.provisioningSandbox) {
+ let sandbox = this._provisionFlows[aProvId]['provisioningSandbox'];
+ if (sandbox.free) {
+ log('_cleanUpProvisionFlow: freeing sandbox');
+ sandbox.free();
+ }
+ delete this._provisionFlows[aProvId]['provisioningSandbox'];
+ }
+
+ // Clean up a related authentication flow, if there is one.
+ if (this._authenticationFlows[prov.authId]) {
+ delete this._authenticationFlows[prov.authId];
+ }
+
+ // Finally delete the provision flow
+ delete this._provisionFlows[aProvId];
+ }
+
+};
+
+let IdentityProvider = new IdentityProviderService();
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/IdentityStore.jsm
@@ -0,0 +1,97 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const EXPORTED_SYMBOLS = ["IdentityStore"];
+
+// the data store for IDService
+// written as a separate thing so it can easily be mocked
+function IDServiceStore() {
+ this.reset();
+}
+
+// Note: eventually these methods may be async, but we haven no need for this
+// for now, since we're not storing to disk.
+IDServiceStore.prototype = {
+ addIdentity: function addIdentity(aEmail, aKeyPair, aCert) {
+ this._identities[aEmail] = {keyPair: aKeyPair, cert: aCert};
+ },
+ fetchIdentity: function fetchIdentity(aEmail) {
+ return aEmail in this._identities ? this._identities[aEmail] : null;
+ },
+ removeIdentity: function removeIdentity(aEmail) {
+ let data = this._identities[aEmail];
+ delete this._identities[aEmail];
+ return data;
+ },
+ getIdentities: function getIdentities() {
+ // XXX - should clone?
+ return this._identities;
+ },
+ clearCert: function clearCert(aEmail) {
+ // XXX - should remove key from store?
+ this._identities[aEmail].cert = null;
+ this._identities[aEmail].keyPair = null;
+ },
+
+ /**
+ * set the login state for a given origin
+ *
+ * @param aOrigin
+ * (string) a web origin
+ *
+ * @param aState
+ * (boolean) whether or not the user is logged in
+ *
+ * @param aEmail
+ * (email) the email address the user is logged in with,
+ * or, if not logged in, the default email for that origin.
+ */
+ setLoginState: function setLoginState(aOrigin, aState, aEmail) {
+ if (aState && !aEmail) {
+ throw "isLoggedIn cannot be set to true without an email";
+ }
+ return this._loginStates[aOrigin] = {isLoggedIn: aState, email: aEmail};
+ },
+ getLoginState: function getLoginState(aOrigin) {
+ return aOrigin in this._loginStates ? this._loginStates[aOrigin] : null;
+ },
+ clearLoginState: function clearLoginState(aOrigin) {
+ delete this._loginStates[aOrigin];
+ },
+
+ reset: function Store_reset() {
+ // _identities associates emails with keypairs and certificates
+ this._identities = {};
+
+ // _loginStates associates. remote origins with a login status and
+ // the email the user has chosen as his or her identity when logging
+ // into that origin.
+ this._loginStates = {};
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "quit-application-granted":
+ Services.obs.removeObserver(this, "quit-application-granted");
+ this.reset();
+ break;
+ }
+ },
+};
+
+let IdentityStore = new IDServiceStore();
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/LogUtils.jsm
@@ -0,0 +1,106 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 EXPORTED_SYMBOLS = ["Logger"];
+const PREF_DEBUG = "toolkit.identity.debug";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function IdentityLogger() {
+ Services.prefs.addObserver(PREF_DEBUG, this, false);
+ this._debug = Services.prefs.getBoolPref(PREF_DEBUG);
+ return this;
+}
+
+IdentityLogger.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch(aTopic) {
+ case "nsPref:changed":
+ this._debug = Services.prefs.getBoolPref(PREF_DEBUG);
+ break;
+
+ case "quit-application-granted":
+ Services.prefs.removeObserver(PREF_DEBUG, this);
+ break;
+
+ default:
+ this.log("Logger observer", "Unknown topic:", aTopic);
+ break;
+ }
+ },
+
+ _generateLogMessage: function _generateLogMessage(aPrefix, ...args) {
+ // create a string representation of a list of arbitrary things
+ let strings = [];
+
+ // XXX bug 770418 - args look like flattened array, not list of strings
+
+ args.forEach(function(arg) {
+ if (typeof arg === 'string') {
+ strings.push(arg);
+ } else if (typeof arg === 'undefined') {
+ strings.push('undefined');
+ } else if (arg === null) {
+ strings.push('null');
+ } else {
+ try {
+ strings.push(JSON.stringify(arg, null, 2));
+ } catch(err) {
+ strings.push("<<something>>");
+ }
+ }
+ });
+ return 'Identity ' + aPrefix + ': ' + strings.join(' : ');
+ },
+
+ /**
+ * log() - utility function to print a list of arbitrary things
+ * Depends on IdentityStore (bottom of this file) for _debug.
+ *
+ * Enable with about:config pref toolkit.identity.debug
+ */
+ log: function log(aPrefix, ...args) {
+ if (!this._debug) {
+ return;
+ }
+ if (typeof this === 'undefined') {
+ for (var frame=Components.stack; frame; frame = frame.caller) {
+ dump (frame + "\n");
+ }
+ }
+ let output = this._generateLogMessage(aPrefix, args);
+ dump(output + "\n");
+
+ // Additionally, make the output visible in the Error Console
+ Services.console.logStringMessage(output);
+ },
+
+ /**
+ * reportError() - report an error through component utils as well as
+ * our log function
+ */
+ reportError: function reportError(aPrefix, ...aArgs) {
+ let prefix = aPrefix + ' ERROR';
+
+ // Report the error in the browser
+ let output = this._generateLogMessage(aPrefix, aArgs);
+ Cu.reportError("Identity: " + output);
+ dump(output + "\n");
+ }
+
+};
+
+let Logger = new IdentityLogger();
--- a/toolkit/identity/Makefile.in
+++ b/toolkit/identity/Makefile.in
@@ -4,17 +4,43 @@
DEPTH = ../..
topsrcdir = @top_srcdir@
srcdir = @srcdir@
VPATH = @srcdir@
include $(DEPTH)/config/autoconf.mk
+FAIL_ON_WARNINGS := 1
+
+MODULE = identity
+MODULE_NAME = identity
+XPIDL_MODULE = identity
+LIBRARY_NAME = identity
+FORCE_STATIC_LIB = 1
+LIBXUL_LIBRARY = 1
+IS_COMPONENT = 1
+GRE_MODULE = 1
+EXPORT_LIBRARY = 1
+
+XPIDLSRCS = \
+ nsIIdentityCryptoService.idl \
+ $(NULL)
+
+CPPSRCS = \
+ IdentityCryptoService.cpp \
+ $(NULL)
+
EXTRA_JS_MODULES = \
+ Identity.jsm \
+ IdentityProvider.jsm \
+ IdentityStore.jsm \
+ jwcrypto.jsm \
+ LogUtils.jsm \
+ RelyingParty.jsm \
Sandbox.jsm \
$(NULL)
ifdef ENABLE_TESTS
DIRS += tests
endif
include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/RelyingParty.jsm
@@ -0,0 +1,368 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+Cu.import("resource://gre/modules/identity/IdentityStore.jsm");
+
+const EXPORTED_SYMBOLS = ["RelyingParty"];
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "jwcrypto",
+ "resource://gre/modules/identity/jwcrypto.jsm");
+
+function log(...aMessageArgs) {
+ Logger.log(["RP"].concat(aMessageArgs));
+}
+function reportError(...aMessageArgs) {
+ Logger.reportError(["RP"].concat(aMessageArgs));
+}
+
+function IdentityRelyingParty() {
+ // The store is a singleton shared among Identity, RelyingParty, and
+ // IdentityProvider. The Identity module takes care of resetting
+ // state in the _store on shutdown.
+ this._store = IdentityStore;
+
+ this.reset();
+}
+
+IdentityRelyingParty.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "quit-application-granted":
+ Services.obs.removeObserver(this, "quit-application-granted");
+ this.shutdown();
+ break;
+
+ }
+ },
+
+ reset: function RP_reset() {
+ // Forget all documents that call in. (These are sometimes
+ // referred to as callers.)
+ this._rpFlows = {};
+ },
+
+ shutdown: function RP_shutdown() {
+ this.reset();
+ Services.obs.removeObserver(this, "quit-application-granted");
+ },
+
+ /**
+ * Register a listener for a given windowID as a result of a call to
+ * navigator.id.watch().
+ *
+ * @param aCaller
+ * (Object) an object that represents the caller document, and
+ * is expected to have properties:
+ * - id (unique, e.g. uuid)
+ * - loggedInEmail (string or null)
+ * - origin (string)
+ *
+ * and a bunch of callbacks
+ * - doReady()
+ * - doLogin()
+ * - doLogout()
+ * - doError()
+ * - doCancel()
+ *
+ */
+ watch: function watch(aRpCaller) {
+ this._rpFlows[aRpCaller.id] = aRpCaller;
+ let origin = aRpCaller.origin;
+ let state = this._store.getLoginState(origin) || { isLoggedIn: false, email: null };
+
+ log("watch: rpId:", aRpCaller.id,
+ "origin:", origin,
+ "loggedInEmail:", aRpCaller.loggedInEmail,
+ "loggedIn:", state.isLoggedIn,
+ "email:", state.email);
+
+ // If the user is already logged in, then there are three cases
+ // to deal with:
+ //
+ // 1. the email is valid and unchanged: 'ready'
+ // 2. the email is null: 'login'; 'ready'
+ // 3. the email has changed: 'login'; 'ready'
+ if (state.isLoggedIn) {
+ if (state.email && aRpCaller.loggedInEmail === state.email) {
+ this._notifyLoginStateChanged(aRpCaller.id, state.email);
+ return aRpCaller.doReady();
+
+ } else if (aRpCaller.loggedInEmail === null) {
+ // Generate assertion for existing login
+ let options = {loggedInEmail: state.email, origin: origin};
+ return this._doLogin(aRpCaller, options);
+
+ } else {
+ // A loggedInEmail different from state.email has been specified.
+ // Change login identity.
+
+ let options = {loggedInEmail: state.email, origin: origin};
+ return this._doLogin(aRpCaller, options);
+ }
+
+ // If the user is not logged in, there are two cases:
+ //
+ // 1. a logged in email was provided: 'ready'; 'logout'
+ // 2. not logged in, no email given: 'ready';
+
+ } else {
+ if (aRpCaller.loggedInEmail) {
+ return this._doLogout(aRpCaller, {origin: origin});
+
+ } else {
+ return aRpCaller.doReady();
+ }
+ }
+ },
+
+ /**
+ * A utility for watch() to set state and notify the dom
+ * on login
+ *
+ * Note that this calls _getAssertion
+ */
+ _doLogin: function _doLogin(aRpCaller, aOptions, aAssertion) {
+ log("_doLogin: rpId:", aRpCaller.id, "origin:", aOptions.origin);
+
+ let loginWithAssertion = function loginWithAssertion(assertion) {
+ this._store.setLoginState(aOptions.origin, true, aOptions.loggedInEmail);
+ this._notifyLoginStateChanged(aRpCaller.id, aOptions.loggedInEmail);
+ aRpCaller.doLogin(assertion);
+ aRpCaller.doReady();
+ }.bind(this);
+
+ if (aAssertion) {
+ loginWithAssertion(aAssertion);
+ } else {
+ this._getAssertion(aOptions, function gotAssertion(err, assertion) {
+ if (err) {
+ reportError("_doLogin:", "Failed to get assertion on login attempt:", err);
+ this._doLogout(aRpCaller);
+ } else {
+ loginWithAssertion(assertion);
+ }
+ }.bind(this));
+ }
+ },
+
+ /**
+ * A utility for watch() to set state and notify the dom
+ * on logout.
+ */
+ _doLogout: function _doLogout(aRpCaller, aOptions) {
+ log("_doLogout: rpId:", aRpCaller.id, "origin:", aOptions.origin);
+
+ let state = this._store.getLoginState(aOptions.origin) || {};
+
+ state.isLoggedIn = false;
+ this._notifyLoginStateChanged(aRpCaller.id, null);
+
+ aRpCaller.doLogout();
+ aRpCaller.doReady();
+ },
+
+ /**
+ * For use with login or logout, emit 'identity-login-state-changed'
+ *
+ * The notification will send the rp caller id in the properties,
+ * and the email of the user in the message.
+ *
+ * @param aRpCallerId
+ * (integer) The id of the RP caller
+ *
+ * @param aIdentity
+ * (string) The email of the user whose login state has changed
+ */
+ _notifyLoginStateChanged: function _notifyLoginStateChanged(aRpCallerId, aIdentity) {
+ log("_notifyLoginStateChanged: rpId:", aRpCallerId, "identity:", aIdentity);
+
+ let options = {rpId: aRpCallerId};
+ Services.obs.notifyObservers({wrappedJSObject: options},
+ "identity-login-state-changed",
+ aIdentity);
+ },
+
+ /**
+ * Initiate a login with user interaction as a result of a call to
+ * navigator.id.request().
+ *
+ * @param aRPId
+ * (integer) the id of the doc object obtained in .watch()
+ *
+ * @param aOptions
+ * (Object) options including privacyPolicy, termsOfService
+ */
+ request: function request(aRPId, aOptions) {
+ log("request: rpId:", aRPId);
+
+ // Notify UX to display identity picker.
+ // Pass the doc id to UX so it can pass it back to us later.
+ let options = {rpId: aRPId};
+
+ // Append URLs after resolving
+ let rp = this._rpFlows[aRPId];
+ let baseURI = Services.io.newURI(rp.origin, null, null);
+ for (let optionName of ["privacyPolicy", "termsOfService"]) {
+ if (aOptions[optionName]) {
+ options[optionName] = baseURI.resolve(aOptions[optionName]);
+ }
+ }
+
+ Services.obs.notifyObservers({wrappedJSObject: options}, "identity-request", null);
+ },
+
+ /**
+ * Invoked when a user wishes to logout of a site (for instance, when clicking
+ * on an in-content logout button).
+ *
+ * @param aRpCallerId
+ * (integer) the id of the doc object obtained in .watch()
+ *
+ */
+ logout: function logout(aRpCallerId) {
+ log("logout: RP caller id:", aRpCallerId);
+ let rp = this._rpFlows[aRpCallerId];
+ if (rp && rp.origin) {
+ let origin = rp.origin;
+ log("logout: origin:", origin);
+ this._doLogout(rp, {origin: origin});
+ } else {
+ log("logout: no RP found with id:", aRpCallerId);
+ }
+ // We don't delete this._rpFlows[aRpCallerId], because
+ // the user might log back in again.
+ },
+
+ getDefaultEmailForOrigin: function getDefaultEmailForOrigin(aOrigin) {
+ let identities = this.getIdentitiesForSite(aOrigin);
+ let result = identities.lastUsed || null;
+ log("getDefaultEmailForOrigin:", aOrigin, "->", result);
+ return result;
+ },
+
+ /**
+ * Return the list of identities a user may want to use to login to aOrigin.
+ */
+ getIdentitiesForSite: function getIdentitiesForSite(aOrigin) {
+ let rv = { result: [] };
+ for (let id in this._store.getIdentities()) {
+ rv.result.push(id);
+ }
+ let loginState = this._store.getLoginState(aOrigin);
+ if (loginState && loginState.email)
+ rv.lastUsed = loginState.email;
+ return rv;
+ },
+
+ /**
+ * Obtain a BrowserID assertion with the specified characteristics.
+ *
+ * @param aCallback
+ * (Function) Callback to be called with (err, assertion) where 'err'
+ * can be an Error or NULL, and 'assertion' can be NULL or a valid
+ * BrowserID assertion. If no callback is provided, an exception is
+ * thrown.
+ *
+ * @param aOptions
+ * (Object) An object that may contain the following properties:
+ *
+ * "audience" : The audience for which the assertion is to be
+ * issued. If this property is not set an exception
+ * will be thrown.
+ *
+ * Any properties not listed above will be ignored.
+ */
+ _getAssertion: function _getAssertion(aOptions, aCallback) {
+ let audience = aOptions.origin;
+ let email = aOptions.loggedInEmail || this.getDefaultEmailForOrigin(audience);
+ log("_getAssertion: audience:", audience, "email:", email);
+ if (!audience) {
+ throw "audience required for _getAssertion";
+ }
+
+ // We might not have any identity info for this email
+ if (!this._store.fetchIdentity(email)) {
+ this._store.addIdentity(email, null, null);
+ }
+
+ let cert = this._store.fetchIdentity(email)['cert'];
+ if (cert) {
+ this._generateAssertion(audience, email, function generatedAssertion(err, assertion) {
+ if (err) {
+ log("ERROR: _getAssertion:", err);
+ }
+ log("_getAssertion: generated assertion:", assertion);
+ return aCallback(err, assertion);
+ });
+ }
+ },
+
+ /**
+ * Generate an assertion, including provisioning via IdP if necessary,
+ * but no user interaction, so if provisioning fails, aCallback is invoked
+ * with an error.
+ *
+ * @param aAudience
+ * (string) web origin
+ *
+ * @param aIdentity
+ * (string) the email we're logging in with
+ *
+ * @param aCallback
+ * (function) callback to invoke on completion
+ * with first-positional parameter the error.
+ */
+ _generateAssertion: function _generateAssertion(aAudience, aIdentity, aCallback) {
+ log("_generateAssertion: audience:", aAudience, "identity:", aIdentity);
+
+ let id = this._store.fetchIdentity(aIdentity);
+ if (! (id && id.cert)) {
+ let errStr = "Cannot generate an assertion without a certificate";
+ log("ERROR: _generateAssertion:", errStr);
+ aCallback(errStr);
+ return;
+ }
+
+ let kp = id.keyPair;
+
+ if (!kp) {
+ let errStr = "Cannot generate an assertion without a keypair";
+ log("ERROR: _generateAssertion:", errStr);
+ aCallback(errStr);
+ return;
+ }
+
+ jwcrypto.generateAssertion(id.cert, kp, aAudience, aCallback);
+ },
+
+ /**
+ * Clean up references to the provisioning flow for the specified RP.
+ */
+ _cleanUpProvisionFlow: function RP_cleanUpProvisionFlow(aRPId, aProvId) {
+ let rp = this._rpFlows[aRPId];
+ if (rp) {
+ delete rp['provId'];
+ } else {
+ log("Error: Couldn't delete provision flow ", aProvId, " for RP ", aRPId);
+ }
+ },
+
+};
+
+let RelyingParty = new IdentityRelyingParty();
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/jwcrypto.jsm
@@ -0,0 +1,133 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this,
+ "IDLog",
+ "resource://gre/modules/identity/IdentityStore.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this,
+ "IdentityCryptoService",
+ "@mozilla.org/identity/crypto-service;1",
+ "nsIIdentityCryptoService");
+
+const EXPORTED_SYMBOLS = ["jwcrypto"];
+
+const ALGORITHMS = { RS256: "RS256", DS160: "DS160" };
+
+function log(...aMessageArgs) {
+ Logger.log(["jwcrypto"].concat(aMessageArgs));
+}
+
+function generateKeyPair(aAlgorithmName, aCallback) {
+ log("Generate key pair; alg =", aAlgorithmName);
+
+ IdentityCryptoService.generateKeyPair(aAlgorithmName, function(rv, aKeyPair) {
+ if (!Components.isSuccessCode(rv)) {
+ return aCallback("key generation failed");
+ }
+
+ var publicKey;
+
+ switch (aKeyPair.keyType) {
+ case ALGORITHMS.RS256:
+ publicKey = {
+ algorithm: "RS",
+ exponent: aKeyPair.hexRSAPublicKeyExponent,
+ modulus: aKeyPair.hexRSAPublicKeyModulus
+ };
+ break;
+
+ case ALGORITHMS.DS160:
+ publicKey = {
+ algorithm: "DS",
+ y: aKeyPair.hexDSAPublicValue,
+ p: aKeyPair.hexDSAPrime,
+ q: aKeyPair.hexDSASubPrime,
+ g: aKeyPair.hexDSAGenerator
+ };
+ break;
+
+ default:
+ return aCallback("unknown key type");
+ }
+
+ let keyWrapper = {
+ serializedPublicKey: JSON.stringify(publicKey),
+ _kp: aKeyPair
+ };
+
+ return aCallback(null, keyWrapper);
+ });
+}
+
+function sign(aPayload, aKeypair, aCallback) {
+ aKeypair._kp.sign(aPayload, function(rv, signature) {
+ if (!Components.isSuccessCode(rv)) {
+ log("ERROR: signer.sign failed");
+ return aCallback("Sign failed");
+ }
+ log("signer.sign: success");
+ return aCallback(null, signature);
+ });
+}
+
+function jwcryptoClass()
+{
+}
+
+jwcryptoClass.prototype = {
+ isCertValid: function(aCert, aCallback) {
+ // XXX check expiration, bug 769850
+ aCallback(true);
+ },
+
+ generateKeyPair: function(aAlgorithmName, aCallback) {
+ log("generating");
+ generateKeyPair(aAlgorithmName, aCallback);
+ },
+
+ generateAssertion: function(aCert, aKeyPair, aAudience, aCallback) {
+ // for now, we hack the algorithm name
+ // XXX bug 769851
+ var header = {"alg": "DS128"};
+ var headerBytes = IdentityCryptoService.base64UrlEncode(
+ JSON.stringify(header));
+
+ var payload = {
+ // expires in 2 minutes
+ // XXX clock skew needs exploration bug 769852
+ exp: Date.now() + (2 * 60 * 1000),
+ aud: aAudience
+ };
+ var payloadBytes = IdentityCryptoService.base64UrlEncode(
+ JSON.stringify(payload));
+
+ log("payload bytes", payload, payloadBytes);
+ sign(headerBytes + "." + payloadBytes, aKeyPair, function(err, signature) {
+ if (err)
+ return aCallback(err);
+
+ var signedAssertion = headerBytes + "." + payloadBytes + "." + signature;
+ return aCallback(null, aCert + "~" + signedAssertion);
+ });
+ }
+
+};
+
+var jwcrypto = new jwcryptoClass();
+jwcrypto.ALGORITHMS = ALGORITHMS;
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/nsIIdentityCryptoService.idl
@@ -0,0 +1,106 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsIIdentityKeyGenCallback;
+interface nsIIdentitySignCallback;
+
+/* Naming and calling conventions:
+ *
+ * A"hex" prefix means "hex-encoded string representation of a byte sequence"
+ * e.g. "ae34bcdf123"
+ *
+ * A "base64url" prefix means "base-64-URL-encoded string repressentation of a
+ * byte sequence.
+ * e.g. "eyJhbGciOiJSUzI1NiJ9"
+ * http://en.wikipedia.org/wiki/Base64#Variants_summary_table
+ * we use the no-padding approach to base64-url-encoding
+ *
+ * Callbacks take an "in nsresult rv" argument that indicates whether the async
+ * operation succeeded. On success, rv will be a success code
+ * (NS_SUCCEEDED(rv) / Components.isSuccessCode(rv)) and the remaining
+ * arguments are as defined in the documentation for the callback. When the
+ * operation fails, rv will be a failure code (NS_FAILED(rv) /
+ * !Components.isSuccessCode(rv)) and the values of the remaining arguments will
+ * be unspecified.
+ *
+ * Key Types:
+ *
+ * "RS256": RSA + SHA-256.
+ *
+ * "DS160": DSA with SHA-1. A 1024-bit prime and a 160-bit subprime with SHA-1.
+ *
+ * we use these abbreviated algorithm names as per the JWA spec
+ * http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-02
+ */
+
+// "@mozilla.org/identity/crypto-service;1"
+[scriptable, builtinclass, uuid(f087e6bc-dd33-4f6c-a106-dd786e052ee9)]
+interface nsIIdentityCryptoService : nsISupports
+{
+ void generateKeyPair(in AUTF8String algorithm,
+ in nsIIdentityKeyGenCallback callback);
+
+ ACString base64UrlEncode(in AUTF8String toEncode);
+};
+
+/**
+ * This interface provides a keypair and signing interface for Identity functionality
+ */
+[scriptable, uuid(73962dc7-8ee7-4346-a12b-b039e1d9b54d)]
+interface nsIIdentityKeyPair : nsISupports
+{
+ readonly attribute AUTF8String keyType;
+
+ // RSA properties, only accessible when keyType == "RS256"
+
+ readonly attribute AUTF8String hexRSAPublicKeyExponent;
+ readonly attribute AUTF8String hexRSAPublicKeyModulus;
+
+ // DSA properties, only accessible when keyType == "DS128"
+ readonly attribute AUTF8String hexDSAPrime; // p
+ readonly attribute AUTF8String hexDSASubPrime; // q
+ readonly attribute AUTF8String hexDSAGenerator; // g
+ readonly attribute AUTF8String hexDSAPublicValue; // y
+
+ void sign(in AUTF8String aText,
+ in nsIIdentitySignCallback callback);
+
+ // XXX implement verification bug 769856
+ // AUTF8String verify(in AUTF8String aSignature, in AUTF8String encodedPublicKey);
+
+};
+
+/**
+ * This interface provides a JavaScript callback object used to collect the
+ * nsIIdentityServeKeyPair when the keygen operation is complete
+ *
+ * though there is discussion as to whether we need the nsresult,
+ * we keep it so we can track deeper crypto errors.
+ */
+[scriptable, function, uuid(90f24ca2-2b05-4ca9-8aec-89d38e2f905a)]
+interface nsIIdentityKeyGenCallback : nsISupports
+{
+ void generateKeyPairFinished(in nsresult rv,
+ in nsIIdentityKeyPair keyPair);
+};
+
+/**
+ * This interface provides a JavaScript callback object used to collect the
+ * AUTF8String signature
+ */
+[scriptable, function, uuid(2d3e5036-374b-4b47-a430-1196b67b890f)]
+interface nsIIdentitySignCallback : nsISupports
+{
+ /** On success, base64urlSignature is the base-64-URL-encoded signature
+ *
+ * For RS256 signatures, XXX bug 769858
+ *
+ * For DSA128 signatures, the signature is the r value concatenated with the
+ * s value, each component padded with leading zeroes as necessary.
+ */
+ void signFinished(in nsresult rv, in ACString base64urlSignature);
+};
--- a/toolkit/library/Makefile.in
+++ b/toolkit/library/Makefile.in
@@ -143,16 +143,17 @@ COMPONENT_LIBS += \
necko \
uconv \
i18n \
chardet \
jar$(VERSION_NUMBER) \
startupcache \
pref \
htmlpars \
+ identity \
imglib2 \
gkgfx \
gklayout \
docshell \
embedcomponents \
webbrwsr \
nsappshell \
txmgr \
--- a/toolkit/library/nsStaticXULComponents.cpp
+++ b/toolkit/library/nsStaticXULComponents.cpp
@@ -214,16 +214,17 @@
SPELLCHECK_MODULE \
LAYOUT_DEBUG_MODULE \
UNIXPROXY_MODULE \
OSXPROXY_MODULE \
WINDOWSPROXY_MODULE \
JSCTYPES_MODULE \
MODULE(jsreflect) \
MODULE(jsperf) \
+ MODULE(identity) \
MODULE(nsServicesCryptoModule) \
MOZ_APP_COMPONENT_MODULES \
MODULE(nsTelemetryModule) \
MODULE(jsinspector) \
MODULE(jsdebugger) \
/* end of list */
#define MODULE(_name) \