Bug 1059216 - Verification of Trusted Hosted Apps manifest signature, part 1. r=dkeeler,rlb a=bajaj
authorVlatko Markovic <vlatko.markovic@sonymobile.com>
Mon, 22 Sep 2014 07:58:59 -0700
changeset 225024 3c4b8e3f6b53811ede5cd3cec64d9d7b80b43e8b
parent 225023 d883d8b4bfd6a3b37925356ed3c7a2c625171247
child 225025 9396b6408686eb3691283cbc924a5393a0ea2091
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdkeeler, rlb, bajaj
bugs1059216
milestone34.0a2
Bug 1059216 - Verification of Trusted Hosted Apps manifest signature, part 1. r=dkeeler,rlb a=bajaj
dom/apps/AppsUtils.jsm
dom/apps/StoreTrustAnchor.jsm
dom/apps/TrustedHostedAppsUtils.jsm
dom/apps/Webapps.jsm
js/xpconnect/src/xpc.msg
security/apps/AppSignatureVerification.cpp
security/apps/AppTrustDomain.cpp
security/manager/ssl/public/nsIX509CertDB.idl
xpcom/base/ErrorList.h
xpcom/base/nsError.h
--- a/dom/apps/AppsUtils.jsm
+++ b/dom/apps/AppsUtils.jsm
@@ -17,17 +17,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/FileUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "WebappOSUtils",
   "resource://gre/modules/WebappOSUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
   "resource://gre/modules/NetUtil.jsm");
 
-// Shared code for AppsServiceChild.jsm, Webapps.jsm and Webapps.js
+// Shared code for AppsServiceChild.jsm, TrustedHostedAppsUtils.jsm,
+// Webapps.jsm and Webapps.js
 
 this.EXPORTED_SYMBOLS =
   ["AppsUtils", "ManifestHelper", "isAbsoluteURI", "mozIApplication"];
 
 function debug(s) {
   //dump("-*- AppsUtils.jsm: " + s + "\n");
 }
 
@@ -111,16 +112,94 @@ function _setAppProperties(aObj, aApp) {
 this.AppsUtils = {
   // Clones a app, without the manifest.
   cloneAppObject: function(aApp) {
     let obj = {};
     _setAppProperties(obj, aApp);
     return obj;
   },
 
+  // Creates a nsILoadContext object with a given appId and isBrowser flag.
+  createLoadContext: function createLoadContext(aAppId, aIsBrowser) {
+    return {
+       associatedWindow: null,
+       topWindow : null,
+       appId: aAppId,
+       isInBrowserElement: aIsBrowser,
+       usePrivateBrowsing: false,
+       isContent: false,
+
+       isAppOfType: function(appType) {
+         throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+       },
+
+       QueryInterface: XPCOMUtils.generateQI([Ci.nsILoadContext,
+                                              Ci.nsIInterfaceRequestor,
+                                              Ci.nsISupports]),
+       getInterface: function(iid) {
+         if (iid.equals(Ci.nsILoadContext))
+           return this;
+         throw Cr.NS_ERROR_NO_INTERFACE;
+       }
+     }
+  },
+
+  // Sends data downloaded from aRequestChannel to a file
+  // identified by aId and aFileName.
+  getFile: function(aRequestChannel, aId, aFileName) {
+    let deferred = Promise.defer();
+
+    // Staging the file in TmpD until all the checks are done.
+    let file = FileUtils.getFile("TmpD", ["webapps", aId, aFileName], true);
+
+    // We need an output stream to write the channel content to the out file.
+    let outputStream = Cc["@mozilla.org/network/file-output-stream;1"]
+                         .createInstance(Ci.nsIFileOutputStream);
+    // write, create, truncate
+    outputStream.init(file, 0x02 | 0x08 | 0x20, parseInt("0664", 8), 0);
+    let bufferedOutputStream =
+      Cc['@mozilla.org/network/buffered-output-stream;1']
+        .createInstance(Ci.nsIBufferedOutputStream);
+    bufferedOutputStream.init(outputStream, 1024);
+
+    // Create a listener that will give data to the file output stream.
+    let listener = Cc["@mozilla.org/network/simple-stream-listener;1"]
+                     .createInstance(Ci.nsISimpleStreamListener);
+
+    listener.init(bufferedOutputStream, {
+      onStartRequest: function(aRequest, aContext) {
+        // Nothing to do there anymore.
+      },
+
+      onStopRequest: function(aRequest, aContext, aStatusCode) {
+        bufferedOutputStream.close();
+        outputStream.close();
+
+        if (!Components.isSuccessCode(aStatusCode)) {
+          deferred.reject({ msg: "NETWORK_ERROR", downloadAvailable: true});
+          return;
+        }
+
+        // If we get a 4XX or a 5XX http status, bail out like if we had a
+        // network error.
+        let responseStatus = aRequestChannel.responseStatus;
+        if (responseStatus >= 400 && responseStatus <= 599) {
+          // unrecoverable error, don't bug the user
+          deferred.reject({ msg: "NETWORK_ERROR", downloadAvailable: false});
+          return;
+        }
+
+        deferred.resolve(file);
+      }
+    });
+    aRequestChannel.asyncOpen(listener, null);
+
+    return deferred.promise;
+  },
+
   getAppByManifestURL: function getAppByManifestURL(aApps, aManifestURL) {
     debug("getAppByManifestURL " + aManifestURL);
     // This could be O(1) if |webapps| was a dictionary indexed on manifestURL
     // which should be the unique app identifier.
     // It's currently O(n).
     for (let id in aApps) {
       let app = aApps[id];
       if (app.manifestURL == aManifestURL) {
--- a/dom/apps/StoreTrustAnchor.jsm
+++ b/dom/apps/StoreTrustAnchor.jsm
@@ -11,16 +11,18 @@ this.EXPORTED_SYMBOLS = [
   "TrustedRootCertificate"
 ];
 
 const APP_TRUSTED_ROOTS= ["AppMarketplaceProdPublicRoot",
                           "AppMarketplaceProdReviewersRoot",
                           "AppMarketplaceDevPublicRoot",
                           "AppMarketplaceDevReviewersRoot",
                           "AppMarketplaceStageRoot",
+                          "TrustedHostedAppPublicRoot",
+                          "TrustedHostedAppTestRoot",
                           "AppXPCShellRoot"];
 
 this.TrustedRootCertificate = {
   _index: Ci.nsIX509CertDB.AppMarketplaceProdPublicRoot,
   get index() {
     return this._index;
   },
   set index(aIndex) {
--- a/dom/apps/TrustedHostedAppsUtils.jsm
+++ b/dom/apps/TrustedHostedAppsUtils.jsm
@@ -1,23 +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/. */
 
-/* global Components, Services, dump */
+/* global Components, Services, dump, AppsUtils, NetUtil, XPCOMUtils */
 
 "use strict";
 
 const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
+const Cr = Components.results;
+const signatureFileExtension = ".sig";
 
 this.EXPORTED_SYMBOLS = ["TrustedHostedAppsUtils"];
 
+Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+  "resource://gre/modules/NetUtil.jsm");
 
 #ifdef MOZ_WIDGET_ANDROID
 // On Android, define the "debug" function as a binding of the "d" function
 // from the AndroidLog module so it gets the "debug" priority and a log tag.
 // We always report debug messages on Android because it's unnecessary
 // to restrict reporting, per bug 1003469.
 let debug = Cu
   .import("resource://gre/modules/AndroidLog.jsm", {})
@@ -60,17 +68,18 @@ this.TrustedHostedAppsUtils = {
       siteSecurityService = Cc["@mozilla.org/ssservice;1"]
         .getService(Ci.nsISiteSecurityService);
     } catch (e) {
       debug("nsISiteSecurityService error: " + e);
       // unrecoverable error, don't bug the user
       throw "CERTDB_ERROR";
     }
 
-    if (siteSecurityService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HPKP, uri.host, 0)) {
+    if (siteSecurityService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HPKP,
+                                         uri.host, 0)) {
       debug("\tvalid certificate pinning for host: " + uri.host + "\n");
       return true;
     }
 
     debug("\tHost NOT pinned: " + uri.host + "\n");
     return false;
   },
 
@@ -95,17 +104,17 @@ this.TrustedHostedAppsUtils = {
       directives
         .map(aDirective => aDirective.trim().split(" "))
         .filter(aList => aList.length > 1)
         // we only restrict on requiredDirectives
         .filter(aList => (requiredDirectives.indexOf(aList[0]) != -1))
         .forEach(aList => {
           // aList[0] contains the directive name.
           // aList[1..n] contains sources.
-          let directiveName = aList.shift()
+          let directiveName = aList.shift();
           let sources = aList;
 
           if ((-1 == validDirectives.indexOf(directiveName))) {
             validDirectives.push(directiveName);
           }
           whiteList.push(...sources.filter(
              // 'self' is checked separately during manifest check
             aSource => (aSource !="'self'" && whiteList.indexOf(aSource) == -1)
@@ -139,10 +148,110 @@ this.TrustedHostedAppsUtils = {
     }
 
     if (!domainWhitelist.list.every(aUrl => this.isHostPinned(aUrl))) {
       debug("TRUSTED_APPLICATION_WHITELIST_VALIDATION_FAILED");
       return false;
     }
 
     return true;
+  },
+
+  _verifySignedFile: function(aManifestStream, aSignatureStream, aCertDb) {
+    let deferred = Promise.defer();
+
+    let root = Ci.nsIX509CertDB.TrustedHostedAppPublicRoot;
+    try {
+      // Check if we should use the test certificates.
+      // Please note that this should be changed if we ever allow chages to the
+      // prefs since that would create a way for an attacker to use the test
+      // root for real apps.
+      let useTrustedAppTestCerts = Services.prefs
+        .getBoolPref("dom.mozApps.use_trustedapp_test_certs");
+      if (useTrustedAppTestCerts) {
+        root = Ci.nsIX509CertDB.TrustedHostedAppTestRoot;
+      }
+    } catch (ex) { }
+
+    aCertDb.verifySignedManifestAsync(
+      root, aManifestStream, aSignatureStream,
+      function(aRv, aCert) {
+        if (Components.isSuccessCode(aRv)) {
+          deferred.resolve(aCert);
+        } else if (aRv == Cr.NS_ERROR_FILE_CORRUPTED ||
+                   aRv == Cr.NS_ERROR_SIGNED_MANIFEST_FILE_INVALID) {
+          deferred.reject("MANIFEST_SIGNATURE_FILE_INVALID");
+        } else {
+          deferred.reject("MANIFEST_SIGNATURE_VERIFICATION_ERROR");
+        }
+      }
+    );
+
+    return deferred.promise;
+  },
+
+  verifySignedManifest: function(aApp, aAppId) {
+    let deferred = Promise.defer();
+
+    let certDb;
+    try {
+      certDb = Cc["@mozilla.org/security/x509certdb;1"]
+                 .getService(Ci.nsIX509CertDB);
+    } catch (e) {
+      debug("nsIX509CertDB error: " + e);
+      // unrecoverable error, don't bug the user
+      throw "CERTDB_ERROR";
+    }
+
+    let mRequestChannel = NetUtil.newChannel(aApp.manifestURL)
+                                 .QueryInterface(Ci.nsIHttpChannel);
+    mRequestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+    mRequestChannel.notificationCallbacks =
+      AppsUtils.createLoadContext(aAppId, false);
+
+    // The manifest signature must be located at the same path as the
+    // manifest and have the same file name, only the file extension
+    // should differ. Any fragment or query parameter will be ignored.
+    let signatureURL;
+    try {
+      let mURL = Cc["@mozilla.org/network/io-service;1"]
+        .getService(Ci.nsIIOService)
+        .newURI(aApp.manifestURL, null, null)
+        .QueryInterface(Ci.nsIURL);
+      signatureURL = mURL.prePath +
+        mURL.directory + mURL.fileBaseName + signatureFileExtension;
+    } catch(e) {
+      deferred.reject("SIGNATURE_PATH_INVALID");
+      return;
+    }
+
+    let sRequestChannel = NetUtil.newChannel(signatureURL)
+      .QueryInterface(Ci.nsIHttpChannel);
+    sRequestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+    sRequestChannel.notificationCallbacks =
+      AppsUtils.createLoadContext(aAppId, false);
+    let getAsyncFetchCallback = (resolve, reject) =>
+        (aInputStream, aResult) => {
+          if (!Components.isSuccessCode(aResult)) {
+            debug("Failed to download file");
+            reject("MANIFEST_FILE_UNAVAILABLE");
+            return;
+          }
+          resolve(aInputStream);
+        };
+
+    Promise.all([
+      new Promise((resolve, reject) => {
+        NetUtil.asyncFetch(mRequestChannel,
+                           getAsyncFetchCallback(resolve, reject));
+      }),
+      new Promise((resolve, reject) => {
+        NetUtil.asyncFetch(sRequestChannel,
+                           getAsyncFetchCallback(resolve, reject));
+      })
+    ]).then(([aManifestStream, aSignatureStream]) => {
+      this._verifySignedFile(aManifestStream, aSignatureStream, certDb)
+        .then(deferred.resolve, deferred.reject);
+    }, deferred.reject);
+
+    return deferred.promise;
   }
 };
--- a/dom/apps/Webapps.jsm
+++ b/dom/apps/Webapps.jsm
@@ -2003,17 +2003,17 @@ this.DOMApplicationRegistry = {
         xhr.setRequestHeader(aHeader.name, aHeader.value);
       });
       xhr.responseType = "json";
       if (app.etag) {
         debug("adding manifest etag:" + app.etag);
         xhr.setRequestHeader("If-None-Match", app.etag);
       }
       xhr.channel.notificationCallbacks =
-        this.createLoadContext(app.installerAppId, app.installerIsBrowser);
+        AppsUtils.createLoadContext(app.installerAppId, app.installerIsBrowser);
 
       xhr.addEventListener("load", onload.bind(this, xhr, oldManifest), false);
       xhr.addEventListener("error", (function() {
         sendError("NETWORK_ERROR");
       }).bind(this), false);
 
       debug("Checking manifest at " + aData.manifestURL);
       xhr.send(null);
@@ -2034,40 +2034,16 @@ this.DOMApplicationRegistry = {
         extraHeaders.push({ name: "X-MOZ-B2G-DEVICE",
                             value: device || "unknown" });
       }
 #endif
       doRequest.call(this, aResult[0].manifest, extraHeaders);
     });
   },
 
-  // Creates a nsILoadContext object with a given appId and isBrowser flag.
-  createLoadContext: function createLoadContext(aAppId, aIsBrowser) {
-    return {
-       associatedWindow: null,
-       topWindow : null,
-       appId: aAppId,
-       isInBrowserElement: aIsBrowser,
-       usePrivateBrowsing: false,
-       isContent: false,
-
-       isAppOfType: function(appType) {
-         throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-       },
-
-       QueryInterface: XPCOMUtils.generateQI([Ci.nsILoadContext,
-                                              Ci.nsIInterfaceRequestor,
-                                              Ci.nsISupports]),
-       getInterface: function(iid) {
-         if (iid.equals(Ci.nsILoadContext))
-           return this;
-         throw Cr.NS_ERROR_NO_INTERFACE;
-       }
-     }
-  },
 
   updatePackagedApp: Task.async(function*(aData, aId, aApp, aNewManifest) {
     debug("updatePackagedApp");
 
     // Store the new update manifest.
     let dir = this._getAppDir(aId).path;
     let manFile = OS.Path.join(dir, "staged-update.webapp");
     yield this._writeFile(manFile, JSON.stringify(aNewManifest));
@@ -2324,18 +2300,18 @@ this.DOMApplicationRegistry = {
       }
       return;
     }
 
     let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
                 .createInstance(Ci.nsIXMLHttpRequest);
     xhr.open("GET", app.manifestURL, true);
     xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
-    xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId,
-                                                               aData.isBrowser);
+    xhr.channel.notificationCallbacks = AppsUtils.createLoadContext(aData.appId,
+                                                                    aData.isBrowser);
     xhr.responseType = "json";
 
     xhr.addEventListener("load", (function() {
       if (xhr.status == 200) {
         if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin,
                                                 xhr.getResponseHeader("content-type"))) {
           sendError("INVALID_MANIFEST_CONTENT_TYPE");
           return;
@@ -2437,18 +2413,18 @@ this.DOMApplicationRegistry = {
       }
       return;
     }
 
     let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
                 .createInstance(Ci.nsIXMLHttpRequest);
     xhr.open("GET", app.manifestURL, true);
     xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
-    xhr.channel.notificationCallbacks = this.createLoadContext(aData.appId,
-                                                               aData.isBrowser);
+    xhr.channel.notificationCallbacks = AppsUtils.createLoadContext(aData.appId,
+                                                                    aData.isBrowser);
     xhr.responseType = "json";
 
     xhr.addEventListener("load", (function() {
       if (xhr.status == 200) {
         if (!AppsUtils.checkManifestContentType(app.installOrigin, app.origin,
                                                 xhr.getResponseHeader("content-type"))) {
           sendError("INVALID_MANIFEST_CONTENT_TYPE");
           return;
@@ -3201,62 +3177,25 @@ this.DOMApplicationRegistry = {
       eventType: "progress",
       manifestURL: aNewApp.manifestURL
     });
   },
 
   _getPackage: function(aRequestChannel, aId, aOldApp, aNewApp) {
     let deferred = Promise.defer();
 
-    // Staging the zip in TmpD until all the checks are done.
-    let zipFile =
-      FileUtils.getFile("TmpD", ["webapps", aId, "application.zip"], true);
-
-    // We need an output stream to write the channel content to the zip file.
-    let outputStream = Cc["@mozilla.org/network/file-output-stream;1"]
-                         .createInstance(Ci.nsIFileOutputStream);
-    // write, create, truncate
-    outputStream.init(zipFile, 0x02 | 0x08 | 0x20, parseInt("0664", 8), 0);
-    let bufferedOutputStream =
-      Cc['@mozilla.org/network/buffered-output-stream;1']
-        .createInstance(Ci.nsIBufferedOutputStream);
-    bufferedOutputStream.init(outputStream, 1024);
-
-    // Create a listener that will give data to the file output stream.
-    let listener = Cc["@mozilla.org/network/simple-stream-listener;1"]
-                     .createInstance(Ci.nsISimpleStreamListener);
-
-    listener.init(bufferedOutputStream, {
-      onStartRequest: function(aRequest, aContext) {
-        // Nothing to do there anymore.
-      },
-
-      onStopRequest: function(aRequest, aContext, aStatusCode) {
-        bufferedOutputStream.close();
-        outputStream.close();
-
-        if (!Components.isSuccessCode(aStatusCode)) {
-          deferred.reject("NETWORK_ERROR");
-          return;
-        }
-
-        // If we get a 4XX or a 5XX http status, bail out like if we had a
-        // network error.
-        let responseStatus = aRequestChannel.responseStatus;
-        if (responseStatus >= 400 && responseStatus <= 599) {
-          // unrecoverable error, don't bug the user
-          aOldApp.downloadAvailable = false;
-          deferred.reject("NETWORK_ERROR");
-          return;
-        }
-
-        deferred.resolve(zipFile);
+    AppsUtils.getFile(aRequestChannel, aId, "application.zip").then((aFile) => {
+      deferred.resolve(aFile);
+    }, function(rejectStatus) {
+      debug("Failed to download package file: " + rejectStatus.msg);
+      if (!rejectStatus.downloadAvailable) {
+        aOldApp.downloadAvailable = false;
       }
+      deferred.reject(rejectStatus.msg);
     });
-    aRequestChannel.asyncOpen(listener, null);
 
     // send a first progress event to correctly set the DOM object's properties
     this._sendDownloadProgressEvent(aNewApp, 0);
 
     return deferred.promise;
   },
 
   /**
--- a/js/xpconnect/src/xpc.msg
+++ b/js/xpconnect/src/xpc.msg
@@ -198,16 +198,19 @@ XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_NOT_SIGN
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY      , "An entry in the JAR has been modified after the JAR was signed.")
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY      , "An entry in the JAR has not been signed.")
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_MISSING       , "An entry is missing from the JAR file.")
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_WRONG_SIGNATURE     , "The JAR's signature is wrong.")
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE     , "An entry in the JAR is too large.")
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_INVALID       , "An entry in the JAR is invalid.")
 XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_MANIFEST_INVALID    , "The JAR's manifest or signature file is invalid.")
 
+/* Codes related to signed manifests */
+XPC_MSG_DEF(NS_ERROR_SIGNED_APP_MANIFEST_INVALID   , "The signed app manifest or signature file is invalid.")
+
 /* Codes for printing-related errors. */
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_NO_PRINTER_AVAILABLE , "No printers available.")
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_NAME_NOT_FOUND       , "The selected printer could not be found.")
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_COULD_NOT_OPEN_FILE  , "Failed to open output file for print to file.")
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_STARTDOC             , "Printing failed while starting the print job.")
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_ENDDOC               , "Printing failed while completing the print job.")
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_STARTPAGE            , "Printing failed while starting a new page.")
 XPC_MSG_DEF(NS_ERROR_GFX_PRINTER_DOC_IS_BUSY          , "Cannot print this document yet, it is still being loaded.")
--- a/security/apps/AppSignatureVerification.cpp
+++ b/security/apps/AppSignatureVerification.cpp
@@ -7,34 +7,39 @@
 #ifdef MOZ_LOGGING
 #define FORCE_PR_LOG 1
 #endif
 
 #include "nsNSSCertificateDB.h"
 
 #include "pkix/pkix.h"
 #include "pkix/pkixnss.h"
+#include "pkix/ScopedPtr.h"
 #include "mozilla/RefPtr.h"
 #include "CryptoTask.h"
 #include "AppTrustDomain.h"
 #include "nsComponentManagerUtils.h"
 #include "nsCOMPtr.h"
 #include "nsDataSignatureVerifier.h"
 #include "nsHashKeys.h"
 #include "nsIFile.h"
+#include "nsIFileStreams.h"
 #include "nsIInputStream.h"
 #include "nsIStringEnumerator.h"
 #include "nsIZipReader.h"
+#include "nsNetUtil.h"
 #include "nsNSSCertificate.h"
 #include "nsProxyRelease.h"
+#include "NSSCertDBTrustDomain.h"
 #include "nsString.h"
 #include "nsTHashtable.h"
 
 #include "base64.h"
 #include "certdb.h"
+#include "nssb64.h"
 #include "secmime.h"
 #include "plstr.h"
 #include "prlog.h"
 
 using namespace mozilla::pkix;
 using namespace mozilla;
 using namespace mozilla::psm;
 
@@ -763,16 +768,92 @@ OpenSignedAppFile(AppTrustedRoot aTruste
       nsNSSCertificate::Create(CERT_LIST_HEAD(builtChain)->cert);
     NS_ENSURE_TRUE(signerCert, NS_ERROR_OUT_OF_MEMORY);
     signerCert.forget(aSignerCert);
   }
 
   return NS_OK;
 }
 
+nsresult
+VerifySignedManifest(AppTrustedRoot aTrustedRoot,
+                     nsIInputStream* aManifestStream,
+                     nsIInputStream* aSignatureStream,
+                     /*out, optional */ nsIX509Cert** aSignerCert)
+{
+  NS_ENSURE_ARG(aManifestStream);
+  NS_ENSURE_ARG(aSignatureStream);
+
+  if (aSignerCert) {
+    *aSignerCert = nullptr;
+  }
+
+  // Load signature file in buffer
+  ScopedAutoSECItem signatureBuffer;
+  nsresult rv = ReadStream(aSignatureStream, signatureBuffer);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+  signatureBuffer.type = siBuffer;
+
+  // Load manifest file in buffer
+  ScopedAutoSECItem manifestBuffer;
+  rv = ReadStream(aManifestStream, manifestBuffer);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  // Calculate SHA1 digest of the manifest buffer
+  Digest manifestCalculatedDigest;
+  rv = manifestCalculatedDigest.DigestBuf(SEC_OID_SHA1,
+                                          manifestBuffer.data,
+                                          manifestBuffer.len - 1); // buffer is null terminated
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  // Get base64 encoded string from manifest buffer digest
+  ScopedPtr<char, PORT_Free_string> base64EncDigest(NSSBase64_EncodeItem(nullptr,
+    nullptr, 0, const_cast<SECItem*>(&manifestCalculatedDigest.get())));
+  if (NS_WARN_IF(!base64EncDigest)) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+
+  // Calculate SHA1 digest of the base64 encoded string
+  Digest doubleDigest;
+  rv = doubleDigest.DigestBuf(SEC_OID_SHA1,
+                              reinterpret_cast<uint8_t*>(base64EncDigest.get()),
+                              strlen(base64EncDigest.get()));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  // Verify the manifest signature (signed digest of the base64 encoded string)
+  ScopedCERTCertList builtChain;
+  rv = VerifySignature(aTrustedRoot, signatureBuffer,
+                       doubleDigest.get(), builtChain);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  // Return the signer's certificate to the reader if they want it.
+  if (aSignerCert) {
+    MOZ_ASSERT(CERT_LIST_HEAD(builtChain));
+    nsCOMPtr<nsIX509Cert> signerCert =
+      nsNSSCertificate::Create(CERT_LIST_HEAD(builtChain)->cert);
+    if (NS_WARN_IF(!signerCert)) {
+      return NS_ERROR_OUT_OF_MEMORY;
+    }
+
+    signerCert.forget(aSignerCert);
+  }
+
+  return NS_OK;
+}
+
 class OpenSignedAppFileTask MOZ_FINAL : public CryptoTask
 {
 public:
   OpenSignedAppFileTask(AppTrustedRoot aTrustedRoot, nsIFile* aJarFile,
                         nsIOpenSignedAppFileCallback* aCallback)
     : mTrustedRoot(aTrustedRoot)
     , mJarFile(aJarFile)
     , mCallback(new nsMainThreadPtrHolder<nsIOpenSignedAppFileCallback>(aCallback))
@@ -798,22 +879,75 @@ private:
 
   const AppTrustedRoot mTrustedRoot;
   const nsCOMPtr<nsIFile> mJarFile;
   nsMainThreadPtrHandle<nsIOpenSignedAppFileCallback> mCallback;
   nsCOMPtr<nsIZipReader> mZipReader; // out
   nsCOMPtr<nsIX509Cert> mSignerCert; // out
 };
 
+class VerifySignedmanifestTask MOZ_FINAL : public CryptoTask
+{
+public:
+  VerifySignedmanifestTask(AppTrustedRoot aTrustedRoot,
+                           nsIInputStream* aManifestStream,
+                           nsIInputStream* aSignatureStream,
+                           nsIVerifySignedManifestCallback* aCallback)
+    : mTrustedRoot(aTrustedRoot)
+    , mManifestStream(aManifestStream)
+    , mSignatureStream(aSignatureStream)
+    , mCallback(
+      new nsMainThreadPtrHolder<nsIVerifySignedManifestCallback>(aCallback))
+  {
+  }
+
+private:
+  virtual nsresult CalculateResult() MOZ_OVERRIDE
+  {
+    return VerifySignedManifest(mTrustedRoot, mManifestStream,
+                                mSignatureStream, getter_AddRefs(mSignerCert));
+  }
+
+  // nsNSSCertificate implements nsNSSShutdownObject, so there's nothing that
+  // needs to be released
+  virtual void ReleaseNSSResources() { }
+
+  virtual void CallCallback(nsresult rv)
+  {
+    (void) mCallback->VerifySignedManifestFinished(rv, mSignerCert);
+  }
+
+  const AppTrustedRoot mTrustedRoot;
+  const nsCOMPtr<nsIInputStream> mManifestStream;
+  const nsCOMPtr<nsIInputStream> mSignatureStream;
+  nsMainThreadPtrHandle<nsIVerifySignedManifestCallback> mCallback;
+  nsCOMPtr<nsIX509Cert> mSignerCert; // out
+};
+
 } // unnamed namespace
 
 NS_IMETHODIMP
 nsNSSCertificateDB::OpenSignedAppFileAsync(
   AppTrustedRoot aTrustedRoot, nsIFile* aJarFile,
   nsIOpenSignedAppFileCallback* aCallback)
 {
   NS_ENSURE_ARG_POINTER(aJarFile);
   NS_ENSURE_ARG_POINTER(aCallback);
   RefPtr<OpenSignedAppFileTask> task(new OpenSignedAppFileTask(aTrustedRoot,
                                                                aJarFile,
                                                                aCallback));
   return task->Dispatch("SignedJAR");
 }
+
+NS_IMETHODIMP
+nsNSSCertificateDB::VerifySignedManifestAsync(
+  AppTrustedRoot aTrustedRoot, nsIInputStream* aManifestStream,
+  nsIInputStream* aSignatureStream, nsIVerifySignedManifestCallback* aCallback)
+{
+  NS_ENSURE_ARG_POINTER(aManifestStream);
+  NS_ENSURE_ARG_POINTER(aSignatureStream);
+  NS_ENSURE_ARG_POINTER(aCallback);
+
+  RefPtr<VerifySignedmanifestTask> task(
+    new VerifySignedmanifestTask(aTrustedRoot, aManifestStream,
+                                 aSignatureStream, aCallback));
+  return task->Dispatch("SignedManifest");
+}
--- a/security/apps/AppTrustDomain.cpp
+++ b/security/apps/AppTrustDomain.cpp
@@ -19,16 +19,19 @@
 
 // Generated in Makefile.in
 #include "marketplace-prod-public.inc"
 #include "marketplace-prod-reviewers.inc"
 #include "marketplace-dev-public.inc"
 #include "marketplace-dev-reviewers.inc"
 #include "marketplace-stage.inc"
 #include "xpcshell.inc"
+// Trusted Hosted Apps Certificates
+#include "manifest-signing-root.inc"
+#include "manifest-signing-test-root.inc"
 
 using namespace mozilla::pkix;
 
 #ifdef PR_LOGGING
 extern PRLogModuleInfo* gPIPNSSLog;
 #endif
 
 namespace mozilla { namespace psm {
@@ -74,16 +77,26 @@ AppTrustDomain::SetTrustedRoot(AppTruste
       trustedDER.len = mozilla::ArrayLength(marketplaceStageRoot);
       break;
 
     case nsIX509CertDB::AppXPCShellRoot:
       trustedDER.data = const_cast<uint8_t*>(xpcshellRoot);
       trustedDER.len = mozilla::ArrayLength(xpcshellRoot);
       break;
 
+    case nsIX509CertDB::TrustedHostedAppPublicRoot:
+      trustedDER.data = const_cast<uint8_t*>(trustedAppPublicRoot);
+      trustedDER.len = mozilla::ArrayLength(trustedAppPublicRoot);
+      break;
+
+    case nsIX509CertDB::TrustedHostedAppTestRoot:
+      trustedDER.data = const_cast<uint8_t*>(trustedAppTestRoot);
+      trustedDER.len = mozilla::ArrayLength(trustedAppTestRoot);
+      break;
+
     default:
       PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
       return SECFailure;
   }
 
   mTrustedRoot = CERT_NewTempCertificate(CERT_GetDefaultCertDB(),
                                          &trustedDER, nullptr, false, true);
   if (!mTrustedRoot) {
--- a/security/manager/ssl/public/nsIX509CertDB.idl
+++ b/security/manager/ssl/public/nsIX509CertDB.idl
@@ -7,36 +7,44 @@
 #include "nsISupports.idl"
 
 interface nsIArray;
 interface nsIX509Cert;
 interface nsIFile;
 interface nsIInterfaceRequestor;
 interface nsIZipReader;
 interface nsIX509CertList;
+interface nsIInputStream;
 
 %{C++
 #define NS_X509CERTDB_CONTRACTID "@mozilla.org/security/x509certdb;1"
 %}
 
 typedef uint32_t AppTrustedRoot;
 
 [scriptable, function, uuid(fc2b60e5-9a07-47c2-a2cd-b83b68a660ac)]
 interface nsIOpenSignedAppFileCallback : nsISupports
 {
   void openSignedAppFileFinished(in nsresult rv,
                                  in nsIZipReader aZipReader,
                                  in nsIX509Cert aSignerCert);
 };
 
+[scriptable, function, uuid(3d6a9c87-5c5f-46fc-9410-96da6092f0f2)]
+interface nsIVerifySignedManifestCallback : nsISupports
+{
+  void verifySignedManifestFinished(in nsresult rv,
+                                    in nsIX509Cert aSignerCert);
+};
+
 /**
  * This represents a service to access and manipulate
  * X.509 certificates stored in a database.
  */
-[scriptable, uuid(dd6e4af8-23bb-41d9-a1e3-9ce925429f2f)]
+[scriptable, uuid(8b01c2af-3a44-44d3-8ea5-51c2455e6c4b)]
 interface nsIX509CertDB : nsISupports {
 
   /**
    *  Constants that define which usages a certificate
    *  is trusted for.
    */
   const unsigned long UNTRUSTED       =      0;
   const unsigned long TRUSTED_SSL     = 1 << 0;
@@ -296,20 +304,38 @@ interface nsIX509CertDB : nsISupports {
    *  first step in opening the JAR.
    */
   const AppTrustedRoot AppMarketplaceProdPublicRoot = 1;
   const AppTrustedRoot AppMarketplaceProdReviewersRoot = 2;
   const AppTrustedRoot AppMarketplaceDevPublicRoot = 3;
   const AppTrustedRoot AppMarketplaceDevReviewersRoot = 4;
   const AppTrustedRoot AppMarketplaceStageRoot = 5;
   const AppTrustedRoot AppXPCShellRoot = 6;
+  const AppTrustedRoot TrustedHostedAppPublicRoot = 7;
+  const AppTrustedRoot TrustedHostedAppTestRoot = 8;
   void openSignedAppFileAsync(in AppTrustedRoot trustedRoot,
                               in nsIFile aJarFile,
                               in nsIOpenSignedAppFileCallback callback);
 
+  /**
+   * Given streams containing a signature and a manifest file, verifies
+   * that the signature is valid for the manifest. The signature must
+   * come from a certificate that is trusted for code signing and that
+   * was issued by the given trusted root.
+   *
+   *  On success, NS_OK and the trusted certificate that signed the
+   *  Manifest are returned.
+   *
+   *  On failure, an error code is returned.
+   */
+  void verifySignedManifestAsync(in AppTrustedRoot trustedRoot,
+                                 in nsIInputStream aManifestStream,
+                                 in nsIInputStream aSignatureStream,
+                                 in nsIVerifySignedManifestCallback callback);
+
   /*
    * Add a cert to a cert DB from a binary string.
    *
    * @param certDER The raw DER encoding of a certificate.
    * @param aTrust decoded by CERT_DecodeTrustString. 3 comma separated characters,
    *                indicating SSL, Email, and Obj signing trust
    * @param aName name of the cert for display purposes.
    */
--- a/xpcom/base/ErrorList.h
+++ b/xpcom/base/ErrorList.h
@@ -869,16 +869,23 @@
   ERROR(NS_ERROR_DOM_BLUETOOTH_PARM_INVALID,              FAILURE(7)),
   ERROR(NS_ERROR_DOM_BLUETOOTH_UNHANDLED,                 FAILURE(8)),
   ERROR(NS_ERROR_DOM_BLUETOOTH_AUTH_FAILURE,              FAILURE(9)),
   ERROR(NS_ERROR_DOM_BLUETOOTH_RMT_DEV_DOWN,              FAILURE(10)),
   ERROR(NS_ERROR_DOM_BLUETOOTH_AUTH_REJECTED,             FAILURE(11)),
 #undef MODULE
 
   /* ======================================================================= */
+  /* 38: NS_ERROR_MODULE_SIGNED_APP */
+  /* ======================================================================= */
+#define MODULE NS_ERROR_MODULE_SIGNED_APP
+  ERROR(NS_ERROR_SIGNED_APP_MANIFEST_INVALID,   FAILURE(1)),
+#undef MODULE
+
+  /* ======================================================================= */
   /* 51: NS_ERROR_MODULE_GENERAL */
   /* ======================================================================= */
 #define MODULE NS_ERROR_MODULE_GENERAL
   /* Error code used internally by the incremental downloader to cancel the
    * network channel when the download is already complete. */
   ERROR(NS_ERROR_DOWNLOAD_COMPLETE,      FAILURE(1)),
   /* Error code used internally by the incremental downloader to cancel the
    * network channel when the response to a range request is 200 instead of
--- a/xpcom/base/nsError.h
+++ b/xpcom/base/nsError.h
@@ -67,16 +67,17 @@
 #define NS_ERROR_MODULE_STORAGE    30
 #define NS_ERROR_MODULE_SCHEMA     31
 #define NS_ERROR_MODULE_DOM_FILE   32
 #define NS_ERROR_MODULE_DOM_INDEXEDDB 33
 #define NS_ERROR_MODULE_DOM_FILEHANDLE 34
 #define NS_ERROR_MODULE_SIGNED_JAR 35
 #define NS_ERROR_MODULE_DOM_FILESYSTEM 36
 #define NS_ERROR_MODULE_DOM_BLUETOOTH 37
+#define NS_ERROR_MODULE_SIGNED_APP 38
 
 /* NS_ERROR_MODULE_GENERAL should be used by modules that do not
  * care if return code values overlap. Callers of methods that
  * return such codes should be aware that they are not
  * globally unique. Implementors should be careful about blindly
  * returning codes from other modules that might also use
  * the generic base.
  */