Bug 988469 - MSISDN verification API for privileged apps. Part 4: Mobile ID service. r=markh, jedp
authorFernando Jiménez <ferjmoreno@gmail.com>
Sat, 07 Jun 2014 19:30:19 +0200
changeset 187426 e3dc24f425734635f3f6d663d2339c4234027d86
parent 187425 8a38d3372bf2c525802e46ccd368abb60f04dc2f
child 187427 3c02a0eaeb59d28e86d4dbfd18a8fb089367a217
push idunknown
push userunknown
push dateunknown
reviewersmarkh, jedp
bugs988469
milestone32.0a1
Bug 988469 - MSISDN verification API for privileged apps. Part 4: Mobile ID service. r=markh, jedp
b2g/app/b2g.js
b2g/chrome/content/shell.js
b2g/installer/package-manifest.in
services/mobileid/MobileIdentityClient.jsm
services/mobileid/MobileIdentityCommon.jsm
services/mobileid/MobileIdentityCredentialsStore.jsm
services/mobileid/MobileIdentityManager.jsm
services/mobileid/MobileIdentitySmsMoMtVerificationFlow.jsm
services/mobileid/MobileIdentitySmsMtVerificationFlow.jsm
services/mobileid/MobileIdentitySmsVerificationFlow.jsm
services/mobileid/MobileIdentityUIGlueCommon.jsm
services/mobileid/MobileIdentityVerificationFlow.jsm
services/mobileid/interfaces/moz.build
services/mobileid/interfaces/nsIMobileIdentityUIGlue.idl
services/mobileid/moz.build
services/moz.build
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -983,10 +983,12 @@ pref("touchcaret.enabled", false);
 pref("selectioncaret.enabled", false);
 
 // Enable sync and mozId with Firefox Accounts.
 #ifdef MOZ_SERVICES_FXACCOUNTS
 pref("services.sync.fxaccounts.enabled", true);
 pref("identity.fxaccounts.enabled", true);
 #endif
 
+pref("services.mobileid.server.uri", "http://msisdn.dev.mozaws.net");
+
 // Enable mapped array buffer
 pref("dom.mapped_arraybuffer.enabled", true);
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -25,16 +25,17 @@ Cu.import('resource://gre/modules/Networ
 Cu.import('resource://gre/modules/SignInToWebsite.jsm');
 SignInToWebsiteController.init();
 
 #ifdef MOZ_SERVICES_FXACCOUNTS
 Cu.import('resource://gre/modules/FxAccountsMgmtService.jsm');
 #endif
 
 Cu.import('resource://gre/modules/DownloadsAPI.jsm');
+Cu.import('resource://gre/modules/MobileIdentityManager.jsm');
 
 XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
                                   "resource://gre/modules/SystemAppProxy.jsm");
 
 Cu.import('resource://gre/modules/Webapps.jsm');
 DOMApplicationRegistry.allAppsLaunchable = true;
 
 XPCOMUtils.defineLazyServiceGetter(Services, 'env',
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -826,18 +826,19 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DL
 #endif
 
 @BINPATH@/components/DataStore.manifest
 @BINPATH@/components/DataStoreImpl.js
 @BINPATH@/components/dom_datastore.xpt
 
 @BINPATH@/components/MobileIdentity.manifest
 @BINPATH@/components/MobileIdentity.js
+@BINPATH@/components/dom_mobileidentity.xpt
 @BINPATH@/components/MobileIdentityUIGlue.js
-@BINPATH@/components/dom_mobileidentity.xpt
+@BINPATH@/components/services_mobileidentity.xpt
 
 #ifdef MOZ_WEBSPEECH
 @BINPATH@/components/dom_webspeechsynth.xpt
 #endif
 
 #ifdef XP_MACOSX
 @BINPATH@/@DLL_PREFIX@plugin_child_interpose@DLL_SUFFIX@
 #endif
new file mode 100644
--- /dev/null
+++ b/services/mobileid/MobileIdentityClient.jsm
@@ -0,0 +1,158 @@
+/* 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/. */
+
+// REST client for
+// https://github.com/mozilla-services/msisdn-gateway/blob/master/API.md
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["MobileIdentityClient"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://services-common/hawkclient.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-crypto/utils.js");
+Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+this.MobileIdentityClient = function(aServerUrl) {
+  let serverUrl = aServerUrl || SERVER_URL;
+  let forceHttps = false;
+  try {
+    // TODO: Force https in production. Bug 1021595.
+    forceHttps = Services.prefs.getBoolPref(PREF_FORCE_HTTPS);
+  } catch(e) {
+    log.warn("Getting force HTTPS pref failed. If this was not intentional " +
+             "check that " + PREF_FORCE_HTTPS + " is defined");
+  }
+
+  log.debug("Force HTTPS " + forceHttps);
+
+  if (forceHttps && !/^https/.exec(serverUrl.toLowerCase())) {
+    throw new Error(ERROR_INTERNAL_HTTP_NOT_ALLOWED);
+  }
+
+  this.hawk = new HawkClient(SERVER_URL);
+  this.hawk.observerPrefix = "MobileId:hawk";
+};
+
+this.MobileIdentityClient.prototype = {
+
+  discover: function(aMsisdn, aMcc, aMnc, aRoaming) {
+    return this._request(DISCOVER, "POST", null, {
+      msisdn: aMsisdn || undefined,
+      mcc: aMcc,
+      mnc: aMnc,
+      roaming: aRoaming
+    });
+  },
+
+  register: function() {
+    return this._request(REGISTER, "POST", null, {});
+  },
+
+  smsMtVerify: function(aSessionToken, aMsisdn, aWantShortCode = false) {
+    let credentials = this._deriveHawkCredentials(aSessionToken);
+    return this._request(SMS_MT_VERIFY, "POST", credentials, {
+      msisdn: aMsisdn,
+      shortVerificationCode: aWantShortCode
+    });
+  },
+
+  verifyCode: function(aSessionToken, aVerificationCode) {
+    log.debug("verificationCode " + aVerificationCode);
+    let credentials = this._deriveHawkCredentials(aSessionToken);
+    return this._request(SMS_VERIFY_CODE, "POST", credentials, {
+      code: aVerificationCode
+    });
+  },
+
+  sign: function(aSessionToken, aDuration, aPublicKey) {
+    let credentials = this._deriveHawkCredentials(aSessionToken);
+    return this._request(SIGN, "POST", credentials, {
+      duration: aDuration,
+      publicKey: aPublicKey
+    });
+  },
+
+  unregister: function(aSessionToken) {
+    let credentials = this._deriveHawkCredentials(aSessionToken);
+    return this._request(UNREGISTER, "POST", credentials, {});
+  },
+
+  /**
+   * The MobileID server expects requests to certain endpoints to be
+   * authorized using Hawk.
+   *
+   * Hawk credentials are derived using shared secrets.
+   *
+   * @param tokenHex
+   *        The current session token encoded in hex
+   * @param context
+   *        A context for the credentials
+   * @param size
+   *        The size in bytes of the expected derived buffer
+   * @return credentials
+   *        Returns an object:
+   *        {
+   *          algorithm: sha256
+   *          id: the Hawk id (from the first 32 bytes derived)
+   *          key: the Hawk key (from bytes 32 to 64)
+   *        }
+   */
+  _deriveHawkCredentials: function(aSessionToken) {
+    let token = CommonUtils.hexToBytes(aSessionToken);
+    let out = CryptoUtils.hkdf(token, undefined,
+                               CREDENTIALS_DERIVATION_INFO,
+                               CREDENTIALS_DERIVATION_SIZE);
+    return {
+      algorithm: "sha256",
+      key: CommonUtils.bytesAsHex(out.slice(32, 64)),
+      id: CommonUtils.bytesAsHex(out.slice(0, 32))
+    };
+  },
+
+  /**
+   * A general method for sending raw API calls to the mobile id verification
+   * server.
+   * All request bodies and responses are JSON.
+   *
+   * @param path
+   *        API endpoint path
+   * @param method
+   *        The HTTP request method
+   * @param credentials
+   *        Hawk credentials
+   * @param jsonPayload
+   *        A JSON payload
+   * @return Promise
+   *        Returns a promise that resolves to the JSON response of the API
+   *        call, or is rejected with an error.
+   */
+  _request: function(path, method, credentials, jsonPayload) {
+    let deferred = Promise.defer();
+
+    this.hawk.request(path, method, credentials, jsonPayload).then(
+      (responseText) => {
+        log.debug("MobileIdentityClient -> responseText " + responseText);
+        try {
+          let response = JSON.parse(responseText);
+          deferred.resolve(response);
+        } catch (err) {
+          deferred.reject({error: err});
+        }
+      },
+
+      (error) => {
+        log.error("MobileIdentityClient -> Error ${}", error);
+        deferred.reject(SERVER_ERRNO_TO_ERROR[error.errno] || ERROR_UNKNOWN);
+      }
+    );
+
+    return deferred.promise;
+  },
+
+};
new file mode 100644
--- /dev/null
+++ b/services/mobileid/MobileIdentityCommon.jsm
@@ -0,0 +1,143 @@
+/* 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/. */
+
+const { interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+
+// loglevel should be one of "Fatal", "Error", "Warn", "Info", "Config",
+// "Debug", "Trace" or "All". If none is specified, "Error" will be used by
+// default.
+const PREF_LOG_LEVEL = "services.mobileid.loglevel";
+
+XPCOMUtils.defineLazyGetter(this, "log", function() {
+  let log = Log.repository.getLogger("MobileId");
+  log.addAppender(new Log.DumpAppender());
+  log.level = Log.Level.Error;
+  try {
+    let level =
+      Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
+      && Services.prefs.getCharPref(PREF_LOG_LEVEL);
+    log.level = Log.Level[level] || Log.Level.Error;
+  } catch (e) {
+    log.error(e);
+  }
+
+  return log;
+});
+
+this.PREF_FORCE_HTTPS = "services.mobileid.forcehttps";
+
+// Permission.
+this.MOBILEID_PERM = "mobileid";
+
+// IPC messages.
+this.GET_ASSERTION_IPC_MSG = "MobileId:GetAssertion";
+
+// Verification methods.
+this.SMS_MT    = "sms/mt";
+this.SMS_MO_MT = "sms/momt";
+
+// Server endpoints.
+this.DISCOVER         = "/discover";
+this.REGISTER         = "/register";
+this.SMS_MT_VERIFY    = "/" + this.SMS_MT + "/verify";
+this.SMS_MO_MT_VERIFY = "/" + this.SMS_MO_MT + "/verify";
+this.SMS_VERIFY_CODE  = "/sms/verify_code";
+this.SIGN             = "/certificate/sign";
+this.UNREGISTER       = "/unregister";
+
+// Server consts.
+this.SERVER_URL = Services.prefs.getCharPref("services.mobileid.server.uri");
+this.CREDENTIALS_DERIVATION_INFO = "identity.mozilla.com/picl/v1/sessionToken";
+this.CREDENTIALS_DERIVATION_SIZE = 2 * 32;
+
+this.SILENT_SMS_RECEIVED_TOPIC = "silent-sms-received";
+
+this.ASSERTION_LIFETIME   = 1000 * 60 * 5;   // 5 minutes.
+this.CERTIFICATE_LIFETIME = 1000 * 3600 * 6; // 6 hours.
+this.KEY_LIFETIME         = 1000 * 3600 * 12; // 12 hours.
+
+this.VERIFICATIONCODE_TIMEOUT = 60000;
+this.VERIFICATIONCODE_RETRIES = 3;
+
+// Internal Errors.
+this.ERROR_INTERNAL_CANNOT_CREATE_VERIFICATION_FLOW = "INTERNAL_CANNOT_CREATE_VERIFICATION_FLOW";
+this.ERROR_INTERNAL_CANNOT_GENERATE_ASSERTION       = "INTERNAL_CANNOT_GENERATE_ASSERTION";
+this.ERROR_INTERNAL_CANNOT_VERIFY_SELECTION         = "INTERNAL_CANNOT_VERIFY_SELECTION";
+this.ERROR_INTERNAL_DB_ERROR                        = "INTERNAL_DB_ERROR";
+this.ERROR_INTERNAL_HTTP_NOT_ALLOWED                = "INTERNAL_HTTP_NOT_ALLOWED";
+this.ERROR_INTERNAL_INVALID_CERTIFICATE             = "INTERNAL_INVALID_CERTIFICATE";
+this.ERROR_INTERNAL_INVALID_PROMPT_RESULT           = "INTERNAL_INVALID_PROMPT_RESULT";
+this.ERROR_INTERNAL_INVALID_USER_SELECTION          = "INTERNAL_INVALID_USER_SELECTION";
+this.ERROR_INTERNAL_INVALID_VERIFICATION_FLOW       = "INTERNAL_INVALID_VERIFICATION_FLOW";
+this.ERROR_INTERNAL_INVALID_VERIFICATION_RESULT     = "INTERNAL_INVALID_VERIFICATION_RESULT";
+this.ERROR_INTERNAL_UNEXPECTED                      = "INTERNAL_UNEXPECTED";
+
+// Errors.
+this.ERROR_ENDPOINT_NOT_SUPPORTED                 = "ENDPOINT_NOT_SUPPORTED";
+this.ERROR_INVALID_ASSERTION                      = "INVALID_ASSERTION";
+this.ERROR_INVALID_AUTH_TOKEN                     = "INVALID_AUTH_TOKEN";
+this.ERROR_INVALID_BODY_JSON                      = "INVALID_BODY_JSON";
+this.ERROR_INVALID_BODY_MISSING_PARAMS            = "INVALID_BODY_MISSING_PARAMS";
+this.ERROR_INVALID_BODY_PARAMS                    = "INVALID_BODY_PARAMS";
+this.ERROR_INVALID_PHONE_NUMBER                   = "INVALID_PHONE_NUMBER";
+this.ERROR_INVALID_PROMPT_RESULT                  = "INVALID_PROMPT_RESULT";
+this.ERROR_INVALID_REQUEST_SIGNATURE              = "INVALID_REQUEST_SIGNATURE";
+this.ERROR_INVALID_VERIFICATION_CODE              = "INVALID_VERIFICATION_CODE";
+this.ERROR_MISSING_CONTENT_LENGTH_HEADER          = "MISSING_CONTENT_LENGTH_HEADER";
+this.ERROR_NO_RETRIES_LEFT                        = "NO_RETRIES_LEFT";
+this.ERROR_OFFLINE                                = "OFFLINE";
+this.ERROR_REQUEST_BODY_TOO_LARGE                 = "REQUEST_BODY_TOO_LARGE";
+this.ERROR_SERVICE_TEMPORARILY_UNAVAILABLE        = "SERVICE_TEMPORARILY_UNAVAILABLE";
+this.ERROR_TOO_MANY_REQUESTS_MSISDN               = "TOO_MANY_REQUESTS_MSISDN";
+this.ERROR_TOO_MANY_REQUESTS_UNSPECIFIED          = "TOO_MANY_REQUESTS_UNSPECIFIED";
+this.ERROR_TOO_MANY_REQUESTS_VERIFICAITON_CODE    = "TOO_MANY_REQUESTS_VERIFICATION_CODE";
+this.ERROR_TOO_MANY_REQUESTS_VERIFICATION_METHOD  = "TOO_MANY_REQUESTS_VERIFICATION_METHOD";
+this.ERROR_UNKNOWN                                = "UNKNOWN";
+this.ERROR_UNVERIFIED_ACCOUNT                     = "UNVERIFIED_ACCOUNT";
+this.ERROR_VERIFICATION_CODE_TIMEOUT              = "VERIFICATION_CODE_TIMEOUT";
+
+// Server errno.
+// From https://github.com/mozilla-services/msisdn-gateway/blob/master/API.md#response-format
+this.ERRNO_UNVERIFIED_ACCOUNT                     = 104;
+this.ERRNO_INVALID_VERIFICATION_CODE              = 105;
+this.ERRNO_INVALID_BODY_JSON                      = 106;
+this.ERRNO_INVALID_BODY_INVALID_PARAMS            = 107;
+this.ERRNO_INVALID_BODY_MISSING_PARAMS            = 108;
+this.ERRNO_INVALID_REQUEST_SIGNATURE              = 109;
+this.ERRNO_INVALID_AUTH_TOKEN                     = 110;
+this.ERRNO_ENDPOINT_NOT_SUPPORTED                 = 111;
+this.ERRNO_MISSING_CONTENT_LENGTH_HEADER          = 112;
+this.ERRNO_REQUEST_BODY_TOO_LARGE                 = 113;
+this.ERRNO_TOO_MANY_REQUESTS_VERIFICATION_CODE    = 114;
+this.ERRNO_TOO_MANY_REQUESTS_MSISDN               = 115;
+this.ERRNO_TOO_MANY_REQUESTS_VERIFICATION_METHOD  = 116;
+this.ERRNO_TOO_MANY_REQUESTS_UNSPECIFIED          = 117;
+this.ERRNO_SERVICE_TEMPORARILY_UNAVAILABLE        = 201;
+this.ERRNO_UNKNOWN_ERROR                          = 999;
+
+// Error matching.
+this.SERVER_ERRNO_TO_ERROR = {};
+SERVER_ERRNO_TO_ERROR[ERRNO_UNVERIFIED_ACCOUNT] = ERROR_UNVERIFIED_ACCOUNT;
+SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_VERIFICATION_CODE] = ERROR_INVALID_VERIFICATION_CODE;
+SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_BODY_JSON] = ERROR_INVALID_BODY_JSON;
+SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_BODY_INVALID_PARAMS] = ERROR_INVALID_BODY_PARAMS;
+SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_BODY_MISSING_PARAMS] = ERROR_INVALID_BODY_MISSING_PARAMS;
+SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_REQUEST_SIGNATURE] = ERROR_INVALID_REQUEST_SIGNATURE;
+SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH_TOKEN] = ERROR_INVALID_AUTH_TOKEN;
+SERVER_ERRNO_TO_ERROR[ERRNO_ENDPOINT_NOT_SUPPORTED] = ERROR_ENDPOINT_NOT_SUPPORTED;
+SERVER_ERRNO_TO_ERROR[ERRNO_MISSING_CONTENT_LENGTH_HEADER] = ERROR_MISSING_CONTENT_LENGTH_HEADER;
+SERVER_ERRNO_TO_ERROR[ERRNO_REQUEST_BODY_TOO_LARGE] = ERROR_REQUEST_BODY_TOO_LARGE;
+SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_REQUESTS_VERIFICATION_CODE] = ERROR_TOO_MANY_REQUESTS_VERIFICAITON_CODE;
+SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_REQUESTS_MSISDN] = ERROR_TOO_MANY_REQUESTS_MSISDN;;
+SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_REQUESTS_VERIFICATION_METHOD] = ERROR_TOO_MANY_REQUESTS_VERIFICATION_METHOD;;
+SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_REQUESTS_UNSPECIFIED] = ERROR_TOO_MANY_REQUESTS_UNSPECIFIED;;
+SERVER_ERRNO_TO_ERROR[ERRNO_SERVICE_TEMPORARILY_UNAVAILABLE] = ERROR_SERVICE_TEMPORARILY_UNAVAILABLE;
+SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_ERROR] = ERROR_UNKNOWN;
+
+// Allow this file to be imported via Components.utils.import().
+this.EXPORTED_SYMBOLS = Object.keys(this);
new file mode 100644
--- /dev/null
+++ b/services/mobileid/MobileIdentityCredentialsStore.jsm
@@ -0,0 +1,173 @@
+/* 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 = ["MobileIdentityCredentialsStore"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
+Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+
+const CREDENTIALS_DB_NAME     = "mobile-id-credentials";
+const CREDENTIALS_DB_VERSION  = 1;
+const CREDENTIALS_STORE_NAME  = "credentials-store";
+
+this.MobileIdentityCredentialsStore = function() {
+};
+
+this.MobileIdentityCredentialsStore.prototype = {
+
+  __proto__: IndexedDBHelper.prototype,
+
+  init: function() {
+    log.debug("MobileIdentityCredentialsStore init");
+    this.initDBHelper(CREDENTIALS_DB_NAME,
+                      CREDENTIALS_DB_VERSION,
+                      [CREDENTIALS_STORE_NAME]);
+  },
+
+  upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
+    log.debug("upgradeSchema");
+    /**
+     * We will be storing objects like:
+     *  {
+     *    msisdn: <string> (key),
+     *    iccId: <string> (index),
+     *    origin: <array> (index),
+     *    msisdnSessionToken: <string>
+     *  }
+     */
+    let objectStore = aDb.createObjectStore(CREDENTIALS_STORE_NAME, {
+      keyPath: "msisdn"
+    });
+
+    objectStore.createIndex("iccId", "iccId", { unique: true });
+    objectStore.createIndex("origin", "origin", { unique: true, multiEntry: true });
+  },
+
+  add: function(aIccId, aMsisdn, aOrigin, aSessionToken) {
+    log.debug("put " + aIccId + ", " + aMsisdn + ", " + aOrigin + ", " +
+              aSessionToken);
+    if (!aOrigin || !aSessionToken) {
+      return Promise.reject(ERROR_INTERNAL_DB_ERROR);
+    }
+
+    let deferred = Promise.defer();
+
+    // We first try get an existing record for the given MSISDN.
+    this.newTxn(
+      "readwrite",
+      CREDENTIALS_STORE_NAME,
+      (aTxn, aStore) => {
+        let range = IDBKeyRange.only(aMsisdn);
+        let cursorReq = aStore.openCursor(range);
+        cursorReq.onsuccess = function(aEvent) {
+          let cursor = aEvent.target.result;
+          let record;
+          // If we already have a record of this MSISDN, we add the origin to
+          // the list of allowed origins.
+          if (cursor && cursor.value) {
+            record = cursor.value;
+            if (record.origin.indexOf(aOrigin) == -1) {
+              record.origin.push(aOrigin);
+            }
+            cursor.update(record);
+          } else {
+            // Otherwise, we store a new record.
+            record = {
+              iccId: aIccId,
+              msisdn: aMsisdn,
+              origin: [aOrigin],
+              sessionToken: aSessionToken
+            };
+            aStore.add(record);
+          }
+          deferred.resolve();
+        };
+        cursorReq.onerror = function(aEvent) {
+          log.error(aEvent.target.error);
+          deferred.reject(ERROR_INTERNAL_DB_ERROR);
+        };
+      }, null, deferred.reject);
+
+    return deferred.promise;
+  },
+
+  getByMsisdn: function(aMsisdn) {
+    log.debug("getByMsisdn " + aMsisdn);
+    if (!aMsisdn) {
+      return Promise.resolve(null);
+    }
+
+    let deferred = Promise.defer();
+    this.newTxn(
+      "readonly",
+      CREDENTIALS_STORE_NAME,
+      (aTxn, aStore) => {
+        aStore.get(aMsisdn).onsuccess = function(aEvent) {
+          aTxn.result = aEvent.target.result;
+        };
+      },
+      function(result) {
+        deferred.resolve(result);
+      },
+      deferred.reject
+    );
+    return deferred.promise;
+  },
+
+  getByIndex: function(aIndex, aValue) {
+    log.debug("getByIndex " + aIndex + ", " + aValue);
+    if (!aValue || !aIndex) {
+      return Promise.resolve(null);
+    }
+
+    let deferred = Promise.defer();
+    this.newTxn(
+      "readonly",
+      CREDENTIALS_STORE_NAME,
+      (aTxn, aStore) => {
+        let index = aStore.index(aIndex);
+        index.get(aValue).onsuccess = function(aEvent) {
+          aTxn.result = aEvent.target.result;
+        };
+      },
+      function(result) {
+        deferred.resolve(result);
+      },
+      deferred.reject
+    );
+    return deferred.promise;
+  },
+
+  getByOrigin: function(aOrigin) {
+    return this.getByIndex("origin", aOrigin);
+  },
+
+  getByIccId: function(aIccId) {
+    return this.getByIndex("iccId", aIccId);
+  },
+
+  delete: function(aMsisdn) {
+    log.debug("delete " + aMsisdn);
+    if (!aMsisdn) {
+      return Promise.resolve();
+    }
+
+    let deferred = Promise.defer();
+    this.newTxn(
+      "readwrite",
+      CREDENTIALS_STORE_NAME,
+      (aTxn, aStore) => {
+        aStore.delete(aMsisdn);
+      },
+      deferred.resolve,
+      deferred.reject
+    );
+    return deferred.promise;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/services/mobileid/MobileIdentityManager.jsm
@@ -0,0 +1,824 @@
+/* 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 = [];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
+Cu.import("resource://gre/modules/MobileIdentityUIGlueCommon.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "MobileIdentityCredentialsStore",
+  "resource://gre/modules/MobileIdentityCredentialsStore.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "MobileIdentityClient",
+  "resource://gre/modules/MobileIdentityClient.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "MobileIdentitySmsMtVerificationFlow",
+  "resource://gre/modules/MobileIdentitySmsMtVerificationFlow.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "MobileIdentitySmsMoMtVerificationFlow",
+  "resource://gre/modules/MobileIdentitySmsMoMtVerificationFlow.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PhoneNumberUtils",
+  "resource://gre/modules/PhoneNumberUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
+  "resource://gre/modules/identity/jwcrypto.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+                                   "@mozilla.org/uuid-generator;1",
+                                   "nsIUUIDGenerator");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+                                   "@mozilla.org/parentprocessmessagemanager;1",
+                                   "nsIMessageListenerManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "permissionManager",
+                                   "@mozilla.org/permissionmanager;1",
+                                   "nsIPermissionManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "securityManager",
+                                   "@mozilla.org/scriptsecuritymanager;1",
+                                   "nsIScriptSecurityManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "appsService",
+                                   "@mozilla.org/AppsService;1",
+                                   "nsIAppsService");
+
+#ifdef MOZ_B2G_RIL
+XPCOMUtils.defineLazyServiceGetter(this, "gRil",
+                                   "@mozilla.org/ril;1",
+                                   "nsIRadioInterfaceLayer");
+
+XPCOMUtils.defineLazyServiceGetter(this, "iccProvider",
+                                   "@mozilla.org/ril/content-helper;1",
+                                   "nsIIccProvider");
+#endif
+
+
+let MobileIdentityManager = {
+
+  init: function() {
+    log.debug("MobileIdentityManager init");
+    Services.obs.addObserver(this, "xpcom-shutdown", false);
+    ppmm.addMessageListener(GET_ASSERTION_IPC_MSG, this);
+    this.messageManagers = {};
+    // TODO: Store keyPairs and certificates in disk. Bug 1021605.
+    this.keyPairs = {};
+    this.certificates = {};
+  },
+
+  receiveMessage: function(aMessage) {
+    log.debug("Received " + aMessage.name);
+
+    if (aMessage.name !== GET_ASSERTION_IPC_MSG) {
+      return;
+    }
+
+    let msg = aMessage.json;
+
+    // We save the message target message manager so we can later dispatch
+    // back messages without broadcasting to all child processes.
+    let promiseId = msg.promiseId;
+    this.messageManagers[promiseId] = aMessage.target;
+
+    this.getMobileIdAssertion(aMessage.principal, promiseId,
+                              msg.msisdn, msg.prompt);
+  },
+
+  observe: function(subject, topic, data) {
+    if (topic != "xpcom-shutdown") {
+      return;
+    }
+
+    ppmm.removeMessageListener(GET_ASSERTION_IPC_MSG, this);
+    Services.obs.removeObserver(this, "xpcom-shutdown");
+    this.messageManagers = null;
+  },
+
+
+  /*********************************************************
+   * Getters
+   ********************************************************/
+
+  get iccInfo() {
+#ifdef MOZ_B2G_RIL
+    if (this._iccInfo) {
+      return this._iccInfo;
+    }
+
+    this._iccInfo = [];
+    for (let i = 0; i < gRil.numRadioInterfaces; i++) {
+      let rilContext = gRil.getRadioInterface(i).rilContext;
+      if (!rilContext) {
+        log.warn("Tried to get the RIL context for an invalid service ID " + i);
+        continue;
+      }
+      let info = rilContext.iccInfo;
+      if (!info) {
+        log.warn("No ICC info");
+        continue;
+      }
+
+      let operator = null;
+      if (rilContext.voice.network &&
+          rilContext.voice.network.shortName &&
+          rilContext.voice.network.shortName.length) {
+        operator = rilContext.voice.network.shortName;
+      } else if (rilContext.data.network &&
+                 rilContext.data.network.shortName &&
+                 rilContext.data.network.shortName.length) {
+        operator = rilContext.data.network.shortName;
+      }
+
+      this._iccInfo.push({
+        iccId: info.iccid,
+        mcc: info.mcc,
+        mnc: info.mnc,
+        // GSM SIMs may have MSISDN while CDMA SIMs may have MDN
+        msisdn: info.msisdn || info.mdn || null,
+        operator: operator,
+        serviceId: i,
+        roaming: rilContext.voice.roaming
+      });
+    }
+
+    return this._iccInfo;
+#endif
+    return null;
+  },
+
+  get credStore() {
+    if (!this._credStore) {
+      this._credStore = new MobileIdentityCredentialsStore();
+      this._credStore.init();
+    }
+    return this._credStore;
+  },
+
+  get ui() {
+    if (!this._ui) {
+      this._ui = Cc["@mozilla.org/services/mobileid-ui-glue;1"]
+                   .createInstance(Ci.nsIMobileIdentityUIGlue);
+      this._ui.oncancel = this.onUICancel.bind(this);
+      this._ui.onresendcode = this.onUIResendCode.bind(this);
+    }
+    return this._ui;
+  },
+
+  get client() {
+    if (!this._client) {
+      this._client = new MobileIdentityClient();
+    }
+    return this._client;
+  },
+
+  get isMultiSim() {
+    return this.iccInfo && this.iccInfo.length > 1;
+  },
+
+  getVerificationOptionsForIcc: function(aServiceId) {
+    log.debug("getVerificationOptionsForIcc " + aServiceId);
+    log.debug("iccInfo ${}", this.iccInfo[aServiceId]);
+    // First of all we need to check if we already have existing credentials
+    // for the given SIM information (ICC id or MSISDN). If we have no valid
+    // credentials, we have to check with the server which options to do we
+    // have to verify the associated phone number.
+    return this.credStore.getByIccId(this.iccInfo[aServiceId].iccId)
+    .then(
+      (creds) => {
+        if (creds) {
+          this.iccInfo[aServiceId].credentials = creds;
+          return;
+        }
+        return this.credStore.getByMsisdn(this.iccInfo[aServiceId].msisdn);
+      }
+    )
+    .then(
+      (creds) => {
+        if (creds) {
+          this.iccInfo[aServiceId].credentials = creds;
+          return;
+        }
+        // We have no credentials for this SIM, so we need to ask the server
+        // which options do we have to verify the phone number.
+        // But we need to be online...
+        if (Services.io.offline) {
+          return Promise.reject(ERROR_OFFLINE);
+        }
+        return this.client.discover(this.iccInfo[aServiceId].msisdn,
+                                    this.iccInfo[aServiceId].mcc,
+                                    this.iccInfo[aServiceId].mnc,
+                                    this.iccInfo[aServiceId].roaming);
+      }
+    )
+    .then(
+      (result) => {
+        log.debug("Discover result ${}", result);
+        if (!result || !result.verificationMethods) {
+          return;
+        }
+        this.iccInfo[aServiceId].verificationMethods = result.verificationMethods;
+        this.iccInfo[aServiceId].verificationDetails = result.verificationDetails;
+        this.iccInfo[aServiceId].canDoSilentVerification =
+          (result.verificationMethods.indexOf(SMS_MO_MT) != -1);
+        return;
+      }
+    );
+  },
+
+  getVerificationOptions: function() {
+    log.debug("getVerificationOptions");
+    // We try to get if we already have credentials for any of the inserted
+    // SIM cards if any is available and we try to get the possible
+    // verification mechanisms for these SIM cards.
+    // All this information will be stored in iccInfo.
+    if (!this.iccInfo || !this.iccInfo.length) {
+      return Promise.resolve();
+    }
+
+    let promises = [];
+    for (let i = 0; i < this.iccInfo.length; i++) {
+      promises.push(this.getVerificationOptionsForIcc(i));
+    }
+    return Promise.all(promises);
+  },
+
+  getKeyPair: function(aSessionToken) {
+    if (this.keyPairs[aSessionToken] &&
+        this.keyPairs[aSessionToken].validUntil > this.client.hawk.now()) {
+      return Promise.resolve(this.keyPairs[aSessionToken].keyPair);
+    }
+
+    let validUntil = this.client.hawk.now() + KEY_LIFETIME;
+    let deferred = Promise.defer();
+    jwcrypto.generateKeyPair("DS160", (error, kp) => {
+      if (error) {
+        return deferred.reject(error);
+      }
+      this.keyPairs[aSessionToken] = {
+        keyPair: kp,
+        validUntil: validUntil
+      };
+      delete this.certificates[aSessionToken];
+      deferred.resolve(kp);
+    });
+
+    return deferred.promise;
+  },
+
+  getCertificate: function(aSessionToken, aPublicKey) {
+    if (this.certificates[aSessionToken] &&
+        this.certificates[aSessionToken].validUntil > this.client.hawk.now()) {
+      return Promise.resolve(this.certificates[aSessionToken].cert);
+    }
+
+    if (Services.io.offline) {
+      return Promise.reject(ERROR_OFFLINE);
+    }
+
+    let validUntil = this.client.hawk.now() + KEY_LIFETIME;
+    let deferred = Promise.defer();
+    this.client.sign(aSessionToken, CERTIFICATE_LIFETIME,
+                     aPublicKey)
+    .then(
+      (signedCert) => {
+        this.certificates[aSessionToken] = {
+          cert: signedCert.cert,
+          validUntil: validUntil
+        };
+        deferred.resolve(signedCert.cert);
+      },
+      deferred.reject
+    );
+    return deferred.promise;
+  },
+
+  /*********************************************************
+   * UI callbacks
+   ********************************************************/
+
+  onUICancel: function() {
+    log.debug("UI cancel");
+    if (this.activeVerificationFlow) {
+      this.activeVerificationFlow.cleanup(true);
+    }
+  },
+
+  onUIResendCode: function() {
+    log.debug("UI resend code");
+    if (!this.activeVerificationFlow) {
+      return;
+    }
+    this.doVerification();
+  },
+
+  /*********************************************************
+   * Permissions helpers
+   ********************************************************/
+
+  hasPermission: function(aPrincipal) {
+    let permission = permissionManager.testPermissionFromPrincipal(aPrincipal,
+                                                                   MOBILEID_PERM);
+    return permission == Ci.nsIPermissionManager.ALLOW_ACTION;
+  },
+
+  addPermission: function(aPrincipal) {
+    permissionManager.addFromPrincipal(aPrincipal, MOBILEID_PERM,
+                                       Ci.nsIPermissionManager.ALLOW_ACTION);
+  },
+
+  /*********************************************************
+   * Phone number verification
+   ********************************************************/
+
+  rejectVerification: function(aReason) {
+    if (!this.activeVerificationDeferred) {
+      return;
+    }
+    this.activeVerificationDeferred.reject(aReason);
+    this.activeVerificationDeferred = null;
+    this.cleanupVerification(true);
+  },
+
+  resolveVerification: function(aResult) {
+    if (!this.activeVerificationDeferred) {
+      return;
+    }
+    this.activeVerificationDeferred.resolve(aResult);
+    this.activeVerificationDeferred = null;
+    this.cleanupVerification();
+  },
+
+  cleanupVerification: function() {
+    if (!this.activeVerificationFlow) {
+      return;
+    }
+    this.activeVerificationFlow.cleanup();
+    this.activeVerificationFlow = null;
+  },
+
+  doVerification: function() {
+    this.activeVerificationFlow.doVerification()
+    .then(
+      (verificationResult) => {
+        log.debug("onVerificationResult ");
+        if (!verificationResult || !verificationResult.sessionToken ||
+            !verificationResult.msisdn) {
+          return this.rejectVerification(
+            ERROR_INTERNAL_INVALID_VERIFICATION_RESULT
+          );
+        }
+        this.resolveVerification(verificationResult);
+      }
+    )
+    .then(
+      null,
+      reason => {
+        // Verification timeout.
+        log.warn("doVerification " + reason);
+      }
+    );
+  },
+
+  _verificationFlow: function(aToVerify, aOrigin) {
+    log.debug("toVerify ${}", aToVerify);
+
+    // We create the corresponding verification flow and save its instance
+    // in case that we need to cancel it or retrigger it because the user
+    // requested its cancelation or a resend of the verification code.
+    if (aToVerify.verificationMethod.indexOf(SMS_MT) != -1 &&
+        aToVerify.msisdn &&
+        aToVerify.verificationDetails &&
+        aToVerify.verificationDetails.mtSender) {
+      this.activeVerificationFlow = new MobileIdentitySmsMtVerificationFlow(
+        aOrigin,
+        aToVerify.msisdn,
+        aToVerify.iccId,
+        aToVerify.serviceId === undefined, // external: the phone number does
+                                           // not seem to belong to any of the
+                                           // device SIM cards.
+        aToVerify.verificationDetails.mtSender,
+        this.ui,
+        this.client
+      );
+#ifdef MOZ_B2G_RIL
+    } else if (aToVerify.verificationMethod.indexOf(SMS_MO_MT) != -1 &&
+        aToVerify.serviceId &&
+        aToVerify.verificationDetails &&
+        aToVerify.verificationDetails.moVerifier &&
+        aToVerify.verificationDetails.mtSender) {
+
+      this.activeVerificationFlow = new MobileIdentitySmsMoMtVerificationFlow(
+        aOrigin,
+        aToVerify.serviceId,
+        aToVerify.iccId,
+        aToVerify.verificationDetails.mtSender,
+        aToVerify.verificationDetails.moVerifier,
+        this.ui,
+        this.client
+      );
+#endif
+    } else {
+      return Promise.reject(ERROR_INTERNAL_CANNOT_VERIFY_SELECTION);
+    }
+
+    if (!this.activeVerificationFlow) {
+      return Promise.reject(ERROR_INTERNAL_CANNOT_CREATE_VERIFICATION_FLOW);
+    }
+
+    this.activeVerificationDeferred = Promise.defer();
+    this.doVerification();
+    return this.activeVerificationDeferred.promise;
+  },
+
+  verificationFlow: function(aUserSelection, aOrigin) {
+    log.debug("verificationFlow ${}", aUserSelection);
+
+    if (!aUserSelection) {
+      return Promise.reject(ERROR_INTERNAL_INVALID_USER_SELECTION);
+    }
+
+    let serviceId = aUserSelection.serviceId || undefined;
+    // We check if the user entered phone number corresponds with any of the
+    // inserted SIMs known phone numbers.
+    if (aUserSelection.msisdn && this.iccInfo) {
+      for (let i = 0; i < this.iccInfo.length; i++) {
+        if (aUserSelection.msisdn == this.iccInfo[i].msisdn) {
+          serviceId = i;
+          break;
+        }
+      }
+    }
+
+    let toVerify = {};
+
+    if (serviceId !== undefined) {
+      log.debug("iccInfo ${}", this.iccInfo[serviceId]);
+      toVerify.serviceId = serviceId;
+      toVerify.iccId = this.iccInfo[serviceId].iccId;
+      toVerify.msisdn = this.iccInfo[serviceId].msisdn;
+      toVerify.verificationMethod =
+        this.iccInfo[serviceId].verificationMethods[0];
+      toVerify.verificationDetails =
+        this.iccInfo[serviceId].verificationDetails[toVerify.verificationMethod];
+      return this._verificationFlow(toVerify, aOrigin);
+    } else {
+      toVerify.msisdn = aUserSelection.msisdn;
+      return this.client.discover(aUserSelection.msisdn,
+                                  aUserSelection.mcc)
+      .then(
+        (discoverResult) => {
+          if (!discoverResult || !discoverResult.verificationMethods) {
+            return Promise.reject(ERROR_INTERNAL_UNEXPECTED);
+          }
+          log.debug("discoverResult ${}", discoverResult);
+          toVerify.verificationMethod = discoverResult.verificationMethods[0];
+          toVerify.verificationDetails =
+            discoverResult.verificationDetails[toVerify.verificationMethod];
+          return this._verificationFlow(toVerify, aOrigin);
+        }
+      );
+    }
+  },
+
+
+  /*********************************************************
+   * UI prompt functions.
+   ********************************************************/
+
+  // The phone number prompt will be used to confirm that the user wants to
+  // verify and share a known phone number and to allow her to introduce an
+  // external phone or to select between phone numbers or SIM cards (if the
+  // phones are not known) in a multi-SIM scenario.
+  // This prompt will be considered as the permission prompt and its choice
+  // will be remembered per origin by default.
+  prompt: function prompt(aPrincipal, aManifestURL, aPhoneInfo) {
+    log.debug("prompt " + aPrincipal + ", " + aManifestURL + ", " +
+              aPhoneInfo);
+
+    let phoneInfoArray = [];
+
+    if (aPhoneInfo) {
+      phoneInfoArray.push(aPhoneInfo);
+    }
+
+    if (this.iccInfo) {
+      for (let i = 0; i < this.iccInfo.length; i++) {
+        // If we don't know the msisdn, there is no previous credentials and
+        // a silent verification is not possible, we don't allow the user to
+        // choose this option.
+        if (!this.iccInfo[i].msisdn && !this.iccInfo[i].credentials &&
+            !this.iccInfo[i].canDoSilentVerification) {
+          continue;
+        }
+
+        let phoneInfo = new MobileIdentityUIGluePhoneInfo(
+          this.iccInfo[i].msisdn,
+          this.iccInfo[i].operator,
+          i,     // service ID
+          false, // external
+          false  // primary
+        );
+        phoneInfoArray.push(phoneInfo);
+      }
+    }
+
+    return this.ui.startFlow(aManifestURL, phoneInfoArray)
+    .then(
+      (result) => {
+        if (!result ||
+            (!result.phoneNumber && !result.serviceId)) {
+          return Promise.reject(ERROR_INTERNAL_INVALID_PROMPT_RESULT);
+        }
+
+        let msisdn;
+        let mcc;
+
+        // If the user selected one of the existing SIM cards we have to check
+        // that we either have the MSISDN for that SIM or we can do a silent
+        // verification that does not require us to have the MSISDN in advance.
+        if (result.serviceId) {
+          let icc = this.iccInfo[result.serviceId];
+          log.debug("icc ${}", icc);
+          if (!icc || !icc.msisdn && !icc.canDoSilentVerification) {
+            return Promise.reject(ERROR_INTERNAL_CANNOT_VERIFY_SELECTION);
+          }
+          msisdn = icc.msisdn;
+          mcc = icc.mcc;
+        } else {
+          msisdn = result.prefix ? result.prefix + result.phoneNumber
+                                 : result.phoneNumber;
+          mcc = result.mcc;
+        }
+
+        // We need to check that the selected phone number is valid and
+        // if it is not notify the UI about the error and allow the user to
+        // retry.
+        if (msisdn && mcc &&
+            !PhoneNumberUtils.parseWithMCC(msisdn, mcc)) {
+          this.ui.error(ERROR_INVALID_PHONE_NUMBER);
+          return this.prompt(aPrincipal, aManifestURL, aPhoneInfo);
+        }
+
+        log.debug("Selected msisdn (if any): " + msisdn + " - " + mcc);
+
+        // The user gave permission for the requester origin, so we store it.
+        this.addPermission(aPrincipal);
+
+        return {
+          msisdn: msisdn,
+          mcc: mcc,
+          serviceId: result.serviceId
+        };
+      }
+    );
+  },
+
+  promptAndVerify: function(aPrincipal, aManifestURL, aCreds) {
+    log.debug("promptAndVerify " + aPrincipal + ", " + aManifestURL +
+              ", ${}", aCreds);
+    let userSelection;
+
+    if (Services.io.offline) {
+      return Promise.reject(ERROR_OFFLINE);
+    }
+
+    // Before prompting the user we need to check with the server the
+    // phone number verification methods that are possible with the
+    // SIMs inserted in the device.
+    return this.getVerificationOptions()
+    .then(
+      () => {
+        // If we have an exisiting credentials, we add its associated
+        // phone number information to the list of choices to present
+        // to the user within the selection prompt.
+        let phoneInfo;
+        if (aCreds) {
+          phoneInfo = new MobileIdentityUIGluePhoneInfo(
+            aCreds.msisdn,
+            null,           // operator
+            null,           // service ID
+            !!aCreds.iccId, // external
+            true            // primary
+          );
+        }
+        return this.prompt(aPrincipal, aManifestURL, phoneInfo);
+      }
+    )
+    .then(
+      (promptResult) => {
+        log.debug("promptResult ${}", promptResult);
+        // If we had credentials and the user didn't change her
+        // selection we return them. Otherwise, we need to verify
+        // the new number.
+        if (promptResult.msisdn && aCreds &&
+            promptResult.msisdn == aCreds.msisdn) {
+          return aCreds;
+        }
+
+        // We might already have credentials for the user selected icc. In
+        // that case, we update the credentials store with the new origin and
+        // return the credentials.
+        if (promptResult.serviceId) {
+          let creds = this.iccInfo[promptResult.serviceId].credentials;
+          if (creds) {
+            this.credStore.add(creds.iccId, creds.msisdn, aPrincipal.origin,
+                               creds.sessionToken);
+            return creds;
+          }
+        }
+
+        // Or we might already have credentials for the selected phone
+        // number and so we do the same: update the credentials store with the
+        // new origin and return the credentials.
+        return this.credStore.getByMsisdn(promptResult.msisdn)
+        .then(
+          (creds) => {
+            if (creds) {
+              this.credStore.add(creds.iccId, creds.msisdn, aPrincipal.origin,
+                                 creds.sessionToken);
+              return creds;
+            }
+            // Otherwise, we need to verify the new number selected by the
+            // user.
+            return this.verificationFlow(promptResult, aPrincipal.origin);
+          }
+        );
+      }
+    );
+  },
+
+  /*********************************************************
+   * Assertion generation
+   ********************************************************/
+
+  generateAssertion: function(aCredentials, aOrigin) {
+    if (!aCredentials.sessionToken) {
+      return Promise.reject(ERROR_INTERNAL_INVALID_TOKEN);
+    }
+
+    let deferred = Promise.defer();
+
+    this.getKeyPair(aCredentials.sessionToken)
+    .then(
+      (keyPair) => {
+        log.debug("keyPair " + keyPair.serializedPublicKey);
+        let options = {
+          duration: ASSERTION_LIFETIME,
+          now: this.client.hawk.now(),
+          localtimeOffsetMsec: this.client.hawk.localtimeOffsetMsec
+        };
+
+        this.getCertificate(aCredentials.sessionToken,
+                            keyPair.serializedPublicKey)
+        .then(
+          (signedCert) => {
+            log.debug("generateAssertion " + signedCert);
+            jwcrypto.generateAssertion(signedCert, keyPair,
+                                       aOrigin, options,
+                                       (error, assertion) => {
+              if (error) {
+                log.error("Error generating assertion " + err);
+                deferred.reject(error);
+                return;
+              }
+              this.credStore.add(aCredentials.iccId,
+                                 aCredentials.msisdn,
+                                 aOrigin,
+                                 aCredentials.sessionToken)
+              .then(
+                () => {
+                  deferred.resolve(assertion);
+                }
+              );
+            });
+          }, deferred.reject
+        );
+      }
+    );
+
+    return deferred.promise;
+  },
+
+  getMobileIdAssertion: function(aPrincipal, aPromiseId) {
+    log.debug("getMobileIdAssertion ${}", aPrincipal);
+
+    let uri = Services.io.newURI(aPrincipal.origin, null, null);
+    let principal = securityManager.getAppCodebasePrincipal(
+      uri, aPrincipal.appid, aPrincipal.isInBrowserElement);
+    let manifestURL = appsService.getManifestURLByLocalId(aPrincipal.appId);
+
+    // First of all we look if we already have credentials for this origin.
+    // If we don't have credentials it means that it is the first time that
+    // the caller requested an assertion.
+    return this.credStore.getByOrigin(aPrincipal.origin)
+    .then(
+      (creds) => {
+        log.debug("creds ${creds} - ${origin}", { creds: creds,
+                                                  origin: aPrincipal.origin});
+        if (!creds || !creds.sessionToken) {
+          log.debug("No credentials");
+          return;
+        }
+
+        // It is possible that the ICC associated with the stored
+        // credentials is not present in the device anymore, so we ask the
+        // user if she still wants to use it anyway or if she prefers to use
+        // another phone number.
+        // If the credentials are associated with an external SIM or there is
+        // no SIM in the device, we just return the credentials.
+        if (this.iccInfo && creds.iccId) {
+          for (let i = 0; i < this.iccInfo.length; i++) {
+            if (this.iccInfo[i].iccId == creds.iccId) {
+              return creds;
+            }
+          }
+          // At this point we know that the SIM associated with the credentials
+          // is not present in the device any more, so we need to ask the user
+          // what to do.
+          return this.promptAndVerify(principal, manifestURL, creds);
+        }
+        return creds;
+      }
+    )
+    .then(
+      (creds) => {
+        // Even if we have credentails it is possible that the user has
+        // removed the permission to share its mobile id with this origin, so
+        // we check the permission and if it is not granted, we ask the user
+        // before generating and sharing the assertion.
+        // If we've just prompted the user in the previous step, the permission
+        // is already granted and stored so we just progress the credentials.
+        if (creds) {
+          if (this.hasPermission(principal)) {
+            return creds;
+          }
+          return this.promptAndVerify(principal, manifestURL, creds);
+        }
+        return this.promptAndVerify(principal, manifestURL);
+      }
+    )
+    .then(
+      (creds) => {
+        if (creds) {
+          return this.generateAssertion(creds, principal.origin);
+        }
+        return Promise.reject(ERROR_INTERNAL_CANNOT_GENERATE_ASSERTION);
+      }
+    )
+    .then(
+      (assertion) => {
+        if (!assertion) {
+          return Promise.reject(ERROR_INTERNAL_CANNOT_GENERATE_ASSERTION);
+        }
+
+        // Get the verified phone number from the assertion.
+        let segments = assertion.split(".");
+        if (!segments) {
+          return Promise.reject(ERROR_INVALID_ASSERTION);
+        }
+
+        // We need to translate the base64 alphabet used in JWT to our base64
+        // alphabet before calling atob.
+        let decodedPayload = JSON.parse(atob(segments[1].replace(/-/g, '+')
+                                                        .replace(/_/g, '/')));
+
+        if (!decodedPayload || !decodedPayload.verifiedMSISDN) {
+          return Promise.reject(ERROR_INVALID_ASSERTION);
+        }
+
+        this.ui.verified(decodedPayload.verifiedMSISDN);
+
+        let mm = this.messageManagers[aPromiseId];
+        mm.sendAsyncMessage("MobileId:GetAssertion:Return:OK", {
+          promiseId: aPromiseId,
+          result: assertion
+        });
+      }
+    )
+    .then(
+      null,
+      (error) => {
+        log.error("getMobileIdAssertion rejected with " + error);
+        // Notify the error to the UI.
+        this.ui.error(error);
+
+        let mm = this.messageManagers[aPromiseId];
+        mm.sendAsyncMessage("MobileId:GetAssertion:Return:KO", {
+          promiseId: aPromiseId,
+          error: error
+        });
+      }
+    );
+  },
+
+};
+
+MobileIdentityManager.init();
new file mode 100644
--- /dev/null
+++ b/services/mobileid/MobileIdentitySmsMoMtVerificationFlow.jsm
@@ -0,0 +1,89 @@
+/* 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 = ["MobileIdentitySmsMoMtVerificationFlow"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
+Cu.import("resource://gre/modules/MobileIdentitySmsVerificationFlow.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "smsService",
+                                   "@mozilla.org/sms/smsservice;1",
+                                   "nsISmsService");
+
+// In order to send messages through nsISmsService, we need to implement
+// nsIMobileMessageCallback, as the WebSMS API implementation is not usable
+// from JS.
+function SilentSmsRequest(aDeferred) {
+  this.deferred = aDeferred;
+}
+SilentSmsRequest.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileMessageCallback]),
+
+  classID: Components.ID("{ff46f1a8-e040-4ff4-98a7-d5a5b86a2c3e}"),
+
+  notifyMessageSent: function notifyMessageSent(aMessage) {
+    log.debug("Silent message successfully sent");
+    this.deferred.resolve(aMessage);
+  },
+
+  notifySendMessageFailed: function notifySendMessageFailed(aError) {
+    log.error("Error sending silent message " + aError);
+    this.deferred.reject(aError);
+  }
+};
+
+this.MobileIdentitySmsMoMtVerificationFlow = function(aOrigin,
+                                                      aServiceId,
+                                                      aIccId,
+                                                      aMtSender,
+                                                      aMoVerifier,
+                                                      aUI,
+                                                      aClient) {
+
+  log.debug("MobileIdentitySmsMoMtVerificationFlow");
+
+  MobileIdentitySmsVerificationFlow.call(this,
+                                         aOrigin,
+                                         null, //msisdn
+                                         aIccId,
+                                         aServiceId,
+                                         false, // external
+                                         aMtSender,
+                                         aMoVerifier,
+                                         aUI,
+                                         aClient,
+                                         this.smsVerifyStrategy);
+};
+
+this.MobileIdentitySmsMoMtVerificationFlow.prototype = {
+
+  __proto__: MobileIdentitySmsVerificationFlow.prototype,
+
+  smsVerifyStrategy: function() {
+    // In the MO+MT flow we need to send an SMS to the given moVerifier number
+    // so the server can find out our phone number to send an SMS back with a
+    // verification code.
+    let deferred = Promise.defer();
+    let silentSmsRequest = new SilentSmsRequest(deferred);
+
+    // The MO SMS body that the server expects contains the API endpoint for
+    // the MO verify request and the HAWK ID parameter derived via HKDF from
+    // the session token. These parameters should go unnamed and space limited.
+    let body = SMS_MO_MT_VERIFY + " " +
+               this.client._deriveHawkCredentials(this.sessionToken).id;
+    smsService.send(this.verificationOptions.serviceId,
+                    this.verificationOptions.moVerifier,
+                    body,
+                    true, // silent
+                    silentSmsRequest);
+    log.debug("Sending " + body + " to " + this.verificationOptions.moVerifier);
+    return deferred.promise;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/services/mobileid/MobileIdentitySmsMtVerificationFlow.jsm
@@ -0,0 +1,49 @@
+/* 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 = ["MobileIdentitySmsMtVerificationFlow"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
+Cu.import("resource://gre/modules/MobileIdentitySmsVerificationFlow.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.MobileIdentitySmsMtVerificationFlow = function(aOrigin,
+                                                    aMsisdn,
+                                                    aIccId,
+                                                    aExternal,
+                                                    aMtSender,
+                                                    aUI,
+                                                    aClient) {
+
+  log.debug("MobileIdentitySmsVerificationFlow " + aMsisdn + ", external: " +
+            aExternal);
+
+  MobileIdentitySmsVerificationFlow.call(this,
+                                         aOrigin,
+                                         aMsisdn,
+                                         aIccId,
+                                         null, // service ID
+                                         aExternal,
+                                         aMtSender,
+                                         null, // moVerifier
+                                         aUI,
+                                         aClient,
+                                         this.smsVerifyStrategy);
+};
+
+this.MobileIdentitySmsMtVerificationFlow.prototype = {
+
+  __proto__: MobileIdentitySmsVerificationFlow.prototype,
+
+  smsVerifyStrategy: function() {
+    return this.client.smsMtVerify(this.sessionToken,
+                                   this.verificationOptions.msisdn,
+                                   this.verificationOptions.external);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/services/mobileid/MobileIdentitySmsVerificationFlow.jsm
@@ -0,0 +1,120 @@
+/* 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 = ["MobileIdentitySmsVerificationFlow"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
+Cu.import("resource://gre/modules/MobileIdentityVerificationFlow.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+#ifdef MOZ_B2G_RIL
+XPCOMUtils.defineLazyServiceGetter(this, "smsService",
+                                   "@mozilla.org/sms/smsservice;1",
+                                   "nsISmsService");
+#endif
+
+this.MobileIdentitySmsVerificationFlow = function(aOrigin,
+                                                  aMsisdn,
+                                                  aIccId,
+                                                  aServiceId,
+                                                  aExternal,
+                                                  aMtSender,
+                                                  aMoVerifier,
+                                                  aUI,
+                                                  aClient,
+                                                  aVerifyStrategy) {
+
+  // SMS MT or SMS MO+MT specific verify strategy.
+  this.smsVerifyStrategy = aVerifyStrategy;
+
+  MobileIdentityVerificationFlow.call(this, {
+    origin: aOrigin,
+    msisdn: aMsisdn,
+    iccId: aIccId,
+    serviceId: aServiceId,
+    external: aExternal,
+    mtSender: aMtSender,
+    moVerifier: aMoVerifier
+  }, aUI, aClient, this._verifyStrategy, this._cleanupStrategy);
+};
+
+this.MobileIdentitySmsVerificationFlow.prototype = {
+
+  __proto__: MobileIdentityVerificationFlow.prototype,
+
+  observedSilentNumber: null,
+
+  onSilentSms: null,
+
+  _verifyStrategy: function() {
+    if (!this.smsVerifyStrategy) {
+      return Promise.reject(ERROR_INTERNAL_UNEXPECTED);
+    }
+
+    // Even if the user selection is given to us as a possible external phone
+    // number, it is also possible that the phone number introduced by the
+    // user belongs to one of the SIMs inserted in the device which MSISDN
+    // is unknown for us, so we always observe for incoming messages coming
+    // from the given mtSender.
+
+#ifdef MOZ_B2G_RIL
+    this.observedSilentNumber = this.verificationOptions.mtSender;
+    try {
+      smsService.addSilentNumber(this.observedSilentNumber);
+    } catch (e) {
+      log.warn("We are already listening for that number");
+    }
+
+    this.onSilentSms = (function(aSubject, aTopic, aData) {
+      log.debug("Got silent message " + aSubject.sender + " - " + aSubject.body);
+      // We might have observed a notification of an incoming silent message
+      // for other number. In that case, we just bail out.
+      if (aSubject.sender != this.observedSilentNumber) {
+        return;
+      }
+
+      // We got the SMS containing the verification code.
+
+      // If the phone number we are trying to verify is or can be an external
+      // phone number (meaning that it doesn't belong to any of the inserted
+      // SIMs) we will be receiving an human readable SMS containing a short
+      // verification code. In this case we need to parse the SMS body to
+      // extract the verification code.
+      // Otherwise, we just use the whole SMS body as it should contain a long
+      // verification code.
+      let verificationCode = aSubject.body;
+      if (this.verificationOptions.external) {
+        // We just take the numerical characters from the body.
+        verificationCode = aSubject.body.replace(/[^0-9]/g,'');
+      }
+
+      log.debug("Verification code: " + verificationCode);
+
+      this.verificationCodeDeferred.resolve(verificationCode);
+    }).bind(this);
+
+    Services.obs.addObserver(this.onSilentSms,
+                             SILENT_SMS_RECEIVED_TOPIC,
+                             false);
+    log.debug("Observing messages from " + this.observedSilentNumber);
+#endif
+
+    return this.smsVerifyStrategy();
+  },
+
+  _cleanupStrategy: function() {
+#ifdef MOZ_B2G_RIL
+    smsService.removeSilentNumber(this.observedSilentNumber);
+    Services.obs.removeObserver(this.onSilentSms,
+                                SILENT_SMS_RECEIVED_TOPIC);
+    this.observedSilentNumber = null;
+    this.onSilentSms = null;
+#endif
+  }
+};
new file mode 100644
--- /dev/null
+++ b/services/mobileid/MobileIdentityUIGlueCommon.jsm
@@ -0,0 +1,31 @@
+/* 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 = ["MobileIdentityUIGluePhoneInfo",
+                         "MobileIdentityUIGluePromptResult"];
+
+this.MobileIdentityUIGluePhoneInfo = function (aMsisdn, aOperator, aServiceId,
+                                               aExternal, aPrimary) {
+  this.msisdn = aMsisdn;
+  this.operator = aOperator;
+  this.serviceId = aServiceId;
+  // A phone number is considered "external" when it doesn't or we don't know
+  // if it does belong to any of the device SIM cards.
+  this.external = aExternal;
+  this.primary = aPrimary;
+}
+
+this.MobileIdentityUIGluePhoneInfo.prototype = {};
+
+this.MobileIdentityUIGluePromptResult = function (aPhoneNumber, aPrefix, aMcc,
+                                                  aServiceId) {
+  this.phoneNumber = aPhoneNumber;
+  this.prefix = aPrefix;
+  this.mcc = aMcc;
+  this.serviceId = aServiceId;
+}
+
+this.MobileIdentityUIGluePromptResult.prototype = {};
new file mode 100644
--- /dev/null
+++ b/services/mobileid/MobileIdentityVerificationFlow.jsm
@@ -0,0 +1,213 @@
+/* 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 = ["MobileIdentityVerificationFlow"];
+
+const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+
+Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.MobileIdentityVerificationFlow = function(aVerificationOptions,
+                                               aUI,
+                                               aClient,
+                                               aVerifyStrategy,
+                                               aCleanupStrategy) {
+  this.verificationOptions = aVerificationOptions;
+  this.ui = aUI;
+  this.client = aClient;
+  this.retries = VERIFICATIONCODE_RETRIES;
+  this.verifyStrategy = aVerifyStrategy;
+  this.cleanupStrategy = aCleanupStrategy;
+};
+
+MobileIdentityVerificationFlow.prototype = {
+
+  doVerification: function() {
+    log.debug("Start verification flow");
+    return this.register()
+    .then(
+      (registerResult) => {
+        log.debug("Register result ${}", registerResult);
+        if (!registerResult || !registerResult.msisdnSessionToken) {
+          return Promise.reject(ERROR_INTERNAL_UNEXPECTED);
+        }
+        this.sessionToken = registerResult.msisdnSessionToken;
+        return this._doVerification();
+      }
+    )
+  },
+
+  _doVerification: function() {
+    log.debug("_doVerification");
+    // We save the timestamp of the start of the verification timeout to be
+    // able to provide to the UI the remaining time on each retry.
+    if (!this.timer) {
+      log.debug("Creating verification code timer");
+      this.timerCreation = Date.now();
+      this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+      this.timer.initWithCallback(this.onVerificationCodeTimeout.bind(this),
+                                  VERIFICATIONCODE_TIMEOUT,
+                                  this.timer.TYPE_ONE_SHOT);
+    }
+
+    if (!this.verifyStrategy) {
+      return Promise.reject(ERROR_INTERNAL_INVALID_VERIFICATION_FLOW);
+    }
+
+    this.verificationCodeDeferred = Promise.defer();
+
+    this.verifyStrategy()
+    .then(
+      () => {
+        // If the verification flow can be for an external phone number,
+        // we need to ask the user for the verification code.
+        // In that case we don't do a notification about the verification
+        // process being done until the user enters the verification code
+        // in the UI.
+        if (this.verificationOptions.external) {
+          let timeLeft = 0;
+          if (this.timer) {
+            timeLeft = this.timerCreation + VERIFICATIONCODE_TIMEOUT -
+                       Date.now();
+          }
+          this.ui.verificationCodePrompt(this.retries,
+                                         VERIFICATIONCODE_TIMEOUT / 1000,
+                                         timeLeft / 1000)
+          .then(
+            (verificationCode) => {
+              if (!verificationCode) {
+                return this.verificationCodeDeferred.reject(
+                  ERROR_INTERNAL_INVALID_PROMPT_RESULT);
+              }
+              // If the user got the verification code that means that the
+              // introduced phone number didn't belong to any of the inserted
+              // SIMs.
+              this.ui.verify();
+              this.verificationCodeDeferred.resolve(verificationCode);
+            }
+          );
+        } else {
+          this.ui.verify();
+        }
+      },
+      (reason) => {
+        this.verificationCodeDeferred.reject(reason);
+      }
+    );
+    return this.verificationCodeDeferred.promise.then(
+      this.onVerificationCode.bind(this)
+    );
+  },
+
+  // When we receive a verification code from the UI, we check it against
+  // the server. If the verification code is incorrect, we decrease the
+  // number of retries left and allow the user to try again. If there is no
+  // possible retry left, we notify about this error so the UI can allow the
+  // user to request the resend of a new verification code.
+  onVerificationCode: function(aVerificationCode) {
+    log.debug("onVerificationCode " + aVerificationCode);
+    if (!aVerificationCode) {
+      this.ui.error(ERROR_INVALID_VERIFICATION_CODE);
+      return this._doVerification();
+    }
+
+    // Before checking the verification code against the server we set the
+    // "verifying" flag to queue timeout expiration events received before
+    // the server request is completed. If the server request is positive
+    // we will discard the timeout event, otherwise we will progress the
+    // event to the UI to allow the user to retry.
+    this.verifying = true;
+
+    return this.verifyCode(aVerificationCode)
+    .then(
+      (result) => {
+        if (!result) {
+          return Promise.reject(INTERNAL_UNEXPECTED);
+        }
+        // The code was correct!
+        // At this point the phone number is verified.
+        // We return the given verification options with the session token
+        // to be stored in the credentials store. With this data we will be
+        // asking the server to give us a certificate to generate assertions.
+        this.verificationOptions.sessionToken = this.sessionToken;
+        this.verificationOptions.msisdn = result.msisdn ||
+                                          this.verificationOptions.msisdn;
+        return this.verificationOptions;
+      },
+      (error) => {
+        log.error("Verification code error " + error);
+        this.retries--;
+        log.error("Retries left " + this.retries);
+        if (!this.retries) {
+          this.ui.error(ERROR_NO_RETRIES_LEFT);
+          return Promise.reject(ERROR_NO_RETRIES_LEFT);
+        }
+        this.verifying = false;
+        if (this.queuedTimeout) {
+          this.onVerificationCodeTimeout();
+        }
+        return this._doVerification();
+      }
+    );
+  },
+
+  onVerificationCodeTimeout: function() {
+    // It is possible that we get the timeout when we are checking a
+    // verification code with the server. In that case, we queue the
+    // timeout to be triggered after we receive the reply from the server
+    // if needed.
+    if (this.verifying) {
+      this.queuedTimeout = true;
+      return;
+    }
+
+    // When the verification process times out we do a clean up, reject
+    // the corresponding promise and notify the UI about the timeout.
+    if (this.verificationCodeDeferred) {
+      this.verificationCodeDeferred.reject(ERROR_VERIFICATION_CODE_TIMEOUT);
+    }
+    this.ui.error(ERROR_VERIFICATION_CODE_TIMEOUT);
+  },
+
+  register: function() {
+    return this.client.register();
+  },
+
+  verifyCode: function(aVerificationCode) {
+    return this.client.verifyCode(this.sessionToken, aVerificationCode);
+  },
+
+  unregister: function() {
+    return this.client.unregister(this.sessionToken);
+  },
+
+  cleanup: function(aUnregister = false) {
+    log.debug("Verification flow cleanup");
+
+    this.queuedTimeout = false;
+    this.retries = VERIFICATIONCODE_RETRIES;
+
+    if (this.timer) {
+      this.timer.cancel();
+      this.timer = null;
+    }
+
+    if (aUnregister) {
+      this.unregister().
+      then(
+        () => {
+          this.sessionToken = null;
+        }
+      );
+    }
+
+    if (this.cleanupStrategy) {
+      this.cleanupStrategy();
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/services/mobileid/interfaces/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+    'nsIMobileIdentityUIGlue.idl'
+]
+
+XPIDL_MODULE = 'services_mobileidentity'
new file mode 100644
--- /dev/null
+++ b/services/mobileid/interfaces/nsIMobileIdentityUIGlue.idl
@@ -0,0 +1,77 @@
+/* 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"
+
+[scriptable, uuid(6c4c5758-e041-4e0d-98da-67bb552f8018)]
+interface nsIMobileIdentityUIGlue : nsISupports
+{
+  /**
+  * Request the creation of a Mobile ID UI flow.
+  *
+  * The permission prompt starts the verification flow asking the user
+  * for permission to share her phone number and allowing her to choose
+  * an already known phone number, a SIM which phone number is unknown
+  * (even in a multi-SIM scenario) or an external phone number.
+  * Selecting a phone number implies giving permission to share it with the
+  * API caller, so the UI should be clear about this.
+  *
+  * @manifestURL manifest URL of the mobile ID requester.
+  * @iccInfo array of objects containing the information about the
+  *          SIM cards available in the device and that can be used for the
+  *          phone number verification and share process.
+  *
+  * Returns a Promise. An instance of nsIMobileIdentityUIGluePromptResult will
+  * be returned as result of the Promise or a single string containing an error
+  * in case of rejection.
+  */
+  jsval startFlow(in DOMString manifestURL, in jsval iccInfo);
+
+  /**
+   * Will prompt the user to enter a code used to verify a phone number.
+   * This will only be called if an external phone number is selected in
+   * startFlow().
+   *
+   * @retries number of retries left to validate a verification code.
+   * @timeout the verification code expires after the timeout fires. This is
+   *          the total life time of the verification code.
+   * @timeLeft we might call verificationCodePrompt more than once for the
+   *           same verification flow (i.e. when the verification code entered
+   *           by the user is incorrect) so we give to the UI the amount of
+   *           time left before the verification code expires.
+   *
+   * Returns a Promise. The value of the resolved promise will be the
+   * verification code introduced through the UI or an error in case of
+   * rejection of the promise.
+   */
+  jsval verificationCodePrompt(in short retries,
+                               in long timeout,
+                               in long timeLeft);
+
+  /**
+   * Notify the UI about the start of the verification process.
+   */
+  void verify();
+
+  /**
+   * Notify the UI about an error in the verification process.
+   */
+  void error(in DOMString error);
+
+  /**
+   * Notify the UI about the succesful phone number verification.
+   */
+  void verified(in DOMString verifiedPhoneNumber);
+
+  /**
+   * Callback to be called when the user cancels the verification flow via UI.
+   */
+  attribute jsval oncancel;
+
+  /**
+   * Callback to be called when the user requests a resend of a verification
+   * code.
+   */
+  attribute jsval onresendcode;
+};
new file mode 100644
--- /dev/null
+++ b/services/mobileid/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+PARALLEL_DIRS += ['interfaces']
+
+EXTRA_JS_MODULES += [
+    'MobileIdentityClient.jsm',
+    'MobileIdentityCommon.jsm',
+    'MobileIdentityCredentialsStore.jsm',
+    'MobileIdentitySmsMoMtVerificationFlow.jsm',
+    'MobileIdentitySmsMtVerificationFlow.jsm',
+    'MobileIdentityUIGlueCommon.jsm',
+    'MobileIdentityVerificationFlow.jsm'
+]
+
+EXTRA_PP_JS_MODULES += [
+    'MobileIdentityManager.jsm',
+    'MobileIdentitySmsVerificationFlow.jsm'
+]
--- a/services/moz.build
+++ b/services/moz.build
@@ -1,17 +1,17 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 PARALLEL_DIRS += [
     'common',
-    'crypto',
+    'crypto'
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
     # MOZ_SERVICES_HEALTHREPORT and therefore MOZ_DATA_REPORTING are
     # defined on Android, but these features are implemented using Java.
     if CONFIG['MOZ_SERVICES_HEALTHREPORT']:
         PARALLEL_DIRS += ['healthreport']
 
@@ -22,9 +22,12 @@ if CONFIG['MOZ_SERVICES_METRICS']:
     PARALLEL_DIRS += ['metrics']
 
 if CONFIG['MOZ_SERVICES_FXACCOUNTS']:
     PARALLEL_DIRS += ['fxaccounts']
 
 if CONFIG['MOZ_SERVICES_SYNC']:
     PARALLEL_DIRS += ['sync']
 
+if CONFIG['MOZ_B2G']:
+    PARALLEL_DIRS += ['mobileid']
+
 SPHINX_TREES['services'] = 'docs'