Bumping manifests a=b2g-bump
[gecko.git] / dom / apps / TrustedHostedAppsUtils.jsm
blob37cfbbe078b40fdb91361c9228b30e88638171e3
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 */
7 "use strict";
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.
30 let debug = Cu
31   .import("resource://gre/modules/AndroidLog.jsm", {})
32   .AndroidLog.d.bind(null, "TrustedHostedAppsUtils");
33 #else
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") :
38   () => {};
39 #endif
41 /**
42  * Verification functions for Trusted Hosted Apps.
43  */
44 this.TrustedHostedAppsUtils = {
46   /**
47    * Check if the given host is pinned in the CA pinning database.
48    */
49   isHostPinned: function (aUrl) {
50     let uri;
51     try {
52       uri = Services.io.newURI(aUrl, null, null);
53     } catch(e) {
54       debug("Host parsing failed: " + e);
55       return false;
56     }
58     // TODO: use nsSiteSecurityService.isSecureURI()
59     if (!uri.host || "https" != uri.scheme) {
60       return false;
61     }
63     // Check certificate pinning
64     let siteSecurityService;
65     try {
66       siteSecurityService = Cc["@mozilla.org/ssservice;1"]
67         .getService(Ci.nsISiteSecurityService);
68     } catch (e) {
69       debug("nsISiteSecurityService error: " + e);
70       // unrecoverable error, don't bug the user
71       throw "CERTDB_ERROR";
72     }
74     if (siteSecurityService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HPKP,
75                                          uri.host, 0)) {
76       debug("\tvalid certificate pinning for host: " + uri.host + "\n");
77       return true;
78     }
80     debug("\tHost NOT pinned: " + uri.host + "\n");
81     return false;
82   },
84   /**
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.
92    */
93   getCSPWhiteList: function(aCsp) {
94     let isValid = false;
95     let whiteList = [];
96     let requiredDirectives = [ "script-src", "style-src" ];
98     if (aCsp) {
99       let validDirectives = [];
100       let directives = aCsp.split(";");
101       // TODO: Use nsIContentSecurityPolicy
102       directives
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))
107         .forEach(aList => {
108           // aList[0] contains the directive name.
109           // aList[1..n] contains sources.
110           let directiveName = aList.shift();
111           let sources = aList;
113           if ((-1 == validDirectives.indexOf(directiveName))) {
114             validDirectives.push(directiveName);
115           }
116           whiteList.push(...sources.filter(
117              // 'self' is checked separately during manifest check
118             aSource => (aSource !="'self'" && whiteList.indexOf(aSource) == -1)
119           ));
120         });
122       // Check if all required directives are present.
123       isValid = requiredDirectives.length === validDirectives.length;
125       if (!isValid) {
126         debug("White list doesn't contain all required directives!");
127         whiteList = [];
128       }
129     }
131     debug("White list contains " + whiteList.length + " hosts");
132     return { list: whiteList, valid: isValid };
133   },
135   /**
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
140    */
141   verifyCSPWhiteList: function(aCsp) {
142     let domainWhitelist = this.getCSPWhiteList(aCsp);
143     if (!domainWhitelist.valid) {
144       debug("TRUSTED_APPLICATION_WHITELIST_PARSING_FAILED");
145       return false;
146     }
148     if (!domainWhitelist.list.every(aUrl => this.isHostPinned(aUrl))) {
149       debug("TRUSTED_APPLICATION_WHITELIST_VALIDATION_FAILED");
150       return false;
151     }
153     return true;
154   },
156   _verifySignedFile: function(aManifestStream, aSignatureStream, aCertDb) {
157     let deferred = Promise.defer();
159     let root = Ci.nsIX509CertDB.TrustedHostedAppPublicRoot;
160     try {
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;
169       }
170     } catch (ex) { }
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");
181         } else {
182           deferred.reject("MANIFEST_SIGNATURE_VERIFICATION_ERROR");
183         }
184       }
185     );
187     return deferred.promise;
188   },
190   verifySignedManifest: function(aApp, aAppId) {
191     let deferred = Promise.defer();
193     let certDb;
194     try {
195       certDb = Cc["@mozilla.org/security/x509certdb;1"]
196                  .getService(Ci.nsIX509CertDB);
197     } catch (e) {
198       debug("nsIX509CertDB error: " + e);
199       // unrecoverable error, don't bug the user
200       throw "CERTDB_ERROR";
201     }
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.
212     let signatureURL;
213     try {
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;
220     } catch(e) {
221       deferred.reject("SIGNATURE_PATH_INVALID");
222       return;
223     }
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");
235             return;
236           }
237           resolve(aInputStream);
238         };
240     Promise.all([
241       new Promise((resolve, reject) => {
242         NetUtil.asyncFetch(mRequestChannel,
243                            getAsyncFetchCallback(resolve, reject));
244       }),
245       new Promise((resolve, reject) => {
246         NetUtil.asyncFetch(sRequestChannel,
247                            getAsyncFetchCallback(resolve, reject));
248       })
249     ]).then(([aManifestStream, aSignatureStream]) => {
250       this._verifySignedFile(aManifestStream, aSignatureStream, certDb)
251         .then(deferred.resolve, deferred.reject);
252     }, deferred.reject);
254     return deferred.promise;
255   },
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");
263         return;
264       }
265       if (!this.verifyCSPWhiteList(aManifest.csp)) {
266         reject("TRUSTED_APPLICATION_WHITELIST_VALIDATION_FAILED");
267         return;
268       }
269       this.verifySignedManifest(aApp, aAppId).then(resolve, reject);
270     });
271   }