1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /* global Components, Services, dump, AppsUtils, NetUtil, XPCOMUtils */
9 const Cu = Components.utils;
10 const Cc = Components.classes;
11 const Ci = Components.interfaces;
12 const Cr = Components.results;
13 const signatureFileExtension = ".sig";
15 this.EXPORTED_SYMBOLS = ["TrustedHostedAppsUtils"];
17 Cu.import("resource://gre/modules/AppsUtils.jsm");
18 Cu.import("resource://gre/modules/Promise.jsm");
19 Cu.import("resource://gre/modules/Services.jsm");
20 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
22 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
23 "resource://gre/modules/NetUtil.jsm");
25 #ifdef MOZ_WIDGET_ANDROID
26 // On Android, define the "debug" function as a binding of the "d" function
27 // from the AndroidLog module so it gets the "debug" priority and a log tag.
28 // We always report debug messages on Android because it's unnecessary
29 // to restrict reporting, per bug 1003469.
31 .import("resource://gre/modules/AndroidLog.jsm", {})
32 .AndroidLog.d.bind(null, "TrustedHostedAppsUtils");
34 // Elsewhere, report debug messages only if dom.mozApps.debug is set to true.
35 // The pref is only checked once, on startup, so restart after changing it.
36 let debug = Services.prefs.getBoolPref("dom.mozApps.debug") ?
37 aMsg => dump("-*- TrustedHostedAppsUtils.jsm : " + aMsg + "\n") :
42 * Verification functions for Trusted Hosted Apps.
44 this.TrustedHostedAppsUtils = {
47 * Check if the given host is pinned in the CA pinning database.
49 isHostPinned: function (aUrl) {
52 uri = Services.io.newURI(aUrl, null, null);
54 debug("Host parsing failed: " + e);
58 // TODO: use nsSiteSecurityService.isSecureURI()
59 if (!uri.host || "https" != uri.scheme) {
63 // Check certificate pinning
64 let siteSecurityService;
66 siteSecurityService = Cc["@mozilla.org/ssservice;1"]
67 .getService(Ci.nsISiteSecurityService);
69 debug("nsISiteSecurityService error: " + e);
70 // unrecoverable error, don't bug the user
74 if (siteSecurityService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HPKP,
76 debug("\tvalid certificate pinning for host: " + uri.host + "\n");
80 debug("\tHost NOT pinned: " + uri.host + "\n");
85 * Take a CSP policy string as input and ensure that it contains at
86 * least the directives that are required ('script-src' and
87 * 'style-src'). If the CSP policy string is 'undefined' or does
88 * not contain some of the required csp directives the function will
89 * return empty list with status set to false. Otherwise a parsed
90 * list of the unique sources listed from the required csp
91 * directives is returned.
93 getCSPWhiteList: function(aCsp) {
96 let requiredDirectives = [ "script-src", "style-src" ];
99 let validDirectives = [];
100 let directives = aCsp.split(";");
101 // TODO: Use nsIContentSecurityPolicy
103 .map(aDirective => aDirective.trim().split(" "))
104 .filter(aList => aList.length > 1)
105 // we only restrict on requiredDirectives
106 .filter(aList => (requiredDirectives.indexOf(aList[0]) != -1))
108 // aList[0] contains the directive name.
109 // aList[1..n] contains sources.
110 let directiveName = aList.shift();
113 if ((-1 == validDirectives.indexOf(directiveName))) {
114 validDirectives.push(directiveName);
116 whiteList.push(...sources.filter(
117 // 'self' is checked separately during manifest check
118 aSource => (aSource !="'self'" && whiteList.indexOf(aSource) == -1)
122 // Check if all required directives are present.
123 isValid = requiredDirectives.length === validDirectives.length;
126 debug("White list doesn't contain all required directives!");
131 debug("White list contains " + whiteList.length + " hosts");
132 return { list: whiteList, valid: isValid };
136 * Verify that the given csp is valid:
137 * 1. contains required directives "script-src" and "style-src"
138 * 2. required directives contain only "https" URLs
139 * 3. domains of the restricted sources exist in the CA pinning database
141 verifyCSPWhiteList: function(aCsp) {
142 let domainWhitelist = this.getCSPWhiteList(aCsp);
143 if (!domainWhitelist.valid) {
144 debug("TRUSTED_APPLICATION_WHITELIST_PARSING_FAILED");
148 if (!domainWhitelist.list.every(aUrl => this.isHostPinned(aUrl))) {
149 debug("TRUSTED_APPLICATION_WHITELIST_VALIDATION_FAILED");
156 _verifySignedFile: function(aManifestStream, aSignatureStream, aCertDb) {
157 let deferred = Promise.defer();
159 let root = Ci.nsIX509CertDB.TrustedHostedAppPublicRoot;
161 // Check if we should use the test certificates.
162 // Please note that this should be changed if we ever allow chages to the
163 // prefs since that would create a way for an attacker to use the test
164 // root for real apps.
165 let useTrustedAppTestCerts = Services.prefs
166 .getBoolPref("dom.mozApps.use_trustedapp_test_certs");
167 if (useTrustedAppTestCerts) {
168 root = Ci.nsIX509CertDB.TrustedHostedAppTestRoot;
172 aCertDb.verifySignedManifestAsync(
173 root, aManifestStream, aSignatureStream,
174 function(aRv, aCert) {
175 debug("Signature verification returned code, cert & root: " + aRv + " " + aCert + " " + root);
176 if (Components.isSuccessCode(aRv)) {
177 deferred.resolve(aCert);
178 } else if (aRv == Cr.NS_ERROR_FILE_CORRUPTED ||
179 aRv == Cr.NS_ERROR_SIGNED_MANIFEST_FILE_INVALID) {
180 deferred.reject("MANIFEST_SIGNATURE_FILE_INVALID");
182 deferred.reject("MANIFEST_SIGNATURE_VERIFICATION_ERROR");
187 return deferred.promise;
190 verifySignedManifest: function(aApp, aAppId) {
191 let deferred = Promise.defer();
195 certDb = Cc["@mozilla.org/security/x509certdb;1"]
196 .getService(Ci.nsIX509CertDB);
198 debug("nsIX509CertDB error: " + e);
199 // unrecoverable error, don't bug the user
200 throw "CERTDB_ERROR";
203 let mRequestChannel = NetUtil.newChannel(aApp.manifestURL)
204 .QueryInterface(Ci.nsIHttpChannel);
205 mRequestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
206 mRequestChannel.notificationCallbacks =
207 AppsUtils.createLoadContext(aAppId, false);
209 // The manifest signature must be located at the same path as the
210 // manifest and have the same file name, only the file extension
211 // should differ. Any fragment or query parameter will be ignored.
214 let mURL = Cc["@mozilla.org/network/io-service;1"]
215 .getService(Ci.nsIIOService)
216 .newURI(aApp.manifestURL, null, null)
217 .QueryInterface(Ci.nsIURL);
218 signatureURL = mURL.prePath +
219 mURL.directory + mURL.fileBaseName + signatureFileExtension;
221 deferred.reject("SIGNATURE_PATH_INVALID");
225 let sRequestChannel = NetUtil.newChannel(signatureURL)
226 .QueryInterface(Ci.nsIHttpChannel);
227 sRequestChannel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
228 sRequestChannel.notificationCallbacks =
229 AppsUtils.createLoadContext(aAppId, false);
230 let getAsyncFetchCallback = (resolve, reject) =>
231 (aInputStream, aResult) => {
232 if (!Components.isSuccessCode(aResult)) {
233 debug("Failed to download file");
234 reject("MANIFEST_FILE_UNAVAILABLE");
237 resolve(aInputStream);
241 new Promise((resolve, reject) => {
242 NetUtil.asyncFetch(mRequestChannel,
243 getAsyncFetchCallback(resolve, reject));
245 new Promise((resolve, reject) => {
246 NetUtil.asyncFetch(sRequestChannel,
247 getAsyncFetchCallback(resolve, reject));
249 ]).then(([aManifestStream, aSignatureStream]) => {
250 this._verifySignedFile(aManifestStream, aSignatureStream, certDb)
251 .then(deferred.resolve, deferred.reject);
254 return deferred.promise;
257 verifyManifest: function(aApp, aAppId, aManifest) {
258 return new Promise((resolve, reject) => {
259 // sanity check on manifest host's CA (proper CA check with
260 // pinning is done by regular networking code)
261 if (!this.isHostPinned(aApp.manifestURL)) {
262 reject("TRUSTED_APPLICATION_HOST_CERTIFICATE_INVALID");
265 if (!this.verifyCSPWhiteList(aManifest.csp)) {
266 reject("TRUSTED_APPLICATION_WHITELIST_VALIDATION_FAILED");
269 this.verifySignedManifest(aApp, aAppId).then(resolve, reject);