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 206648 e3dc24f425734635f3f6d663d2339c4234027d86
parent 206647 8a38d3372bf2c525802e46ccd368abb60f04dc2f
child 206649 3c02a0eaeb59d28e86d4dbfd18a8fb089367a217
push id3741
push userasasaki@mozilla.com
push dateMon, 21 Jul 2014 20:25:18 +0000
treeherdermozilla-beta@4d6f46f5af68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh, jedp
bugs988469
milestone32.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 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'