зеркало из https://github.com/mozilla/gecko-dev.git
Bug 829934 - Implement a hash verifier for webapp/mini manifest r=ferjm
This commit is contained in:
Родитель
5b76466f52
Коммит
2ca010d058
|
@ -58,6 +58,8 @@ this.AppsUtils = {
|
|||
updateTime: aApp.updateTime,
|
||||
etag: aApp.etag,
|
||||
packageEtag: aApp.packageEtag,
|
||||
manifestHash: aApp.manifestHash,
|
||||
packageHash: aApp.packageHash,
|
||||
installerAppId: aApp.installerAppId || Ci.nsIScriptSecurityManager.NO_APP_ID,
|
||||
installerIsBrowser: !!aApp.installerIsBrowser
|
||||
};
|
||||
|
|
|
@ -1124,6 +1124,77 @@ this.DOMApplicationRegistry = {
|
|||
}
|
||||
},
|
||||
|
||||
// Returns the MD5 hash of a file, doing async IO off the main thread.
|
||||
computeFileHash: function computeFileHash(aFile, aCallback) {
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
const CHUNK_SIZE = 16384;
|
||||
|
||||
// Return the two-digit hexadecimal code for a byte.
|
||||
function toHexString(charCode) {
|
||||
return ("0" + charCode.toString(16)).slice(-2);
|
||||
}
|
||||
|
||||
let hasher = Cc["@mozilla.org/security/hash;1"]
|
||||
.createInstance(Ci.nsICryptoHash);
|
||||
// We want to use the MD5 algorithm.
|
||||
hasher.init(hasher.MD5);
|
||||
|
||||
OS.File.open(aFile.path, { read: true }).then(
|
||||
function opened(file) {
|
||||
let readChunk = function readChunk() {
|
||||
file.read(CHUNK_SIZE).then(
|
||||
function readSuccess(array) {
|
||||
hasher.update(array, array.length);
|
||||
if (array.length == CHUNK_SIZE) {
|
||||
readChunk();
|
||||
} else {
|
||||
// We're passing false to get the binary hash and not base64.
|
||||
let hash = hasher.finish(false);
|
||||
// convert the binary hash data to a hex string.
|
||||
aCallback([toHexString(hash.charCodeAt(i)) for (i in hash)]
|
||||
.join(""));
|
||||
}
|
||||
},
|
||||
function readError() {
|
||||
debug("Error reading " + aFile.path);
|
||||
aCallback(null);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
readChunk();
|
||||
},
|
||||
function openError() {
|
||||
debug("Error opening " + aFile.path);
|
||||
aCallback(null);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// Returns the MD5 hash of the manifest.
|
||||
computeManifestHash: function(aManifest) {
|
||||
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
|
||||
.createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
converter.charset = "UTF-8";
|
||||
let result = {};
|
||||
// Data is an array of bytes.
|
||||
let data = converter.convertToByteArray(JSON.stringify(aManifest), result);
|
||||
|
||||
let hasher = Cc["@mozilla.org/security/hash;1"]
|
||||
.createInstance(Ci.nsICryptoHash);
|
||||
hasher.init(hasher.MD5);
|
||||
hasher.update(data, data.length);
|
||||
// We're passing false to get the binary hash and not base64.
|
||||
let hash = hasher.finish(false);
|
||||
|
||||
function toHexString(charCode) {
|
||||
return ("0" + charCode.toString(16)).slice(-2);
|
||||
}
|
||||
|
||||
// Convert the binary hash data to a hex string.
|
||||
return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
|
||||
},
|
||||
|
||||
checkForUpdate: function(aData, aMm) {
|
||||
debug("checkForUpdate for " + aData.manifestURL);
|
||||
let id = this._appIdForManifestURL(aData.manifestURL);
|
||||
|
@ -1336,12 +1407,15 @@ this.DOMApplicationRegistry = {
|
|||
|
||||
xhr.addEventListener("load", (function() {
|
||||
debug("Got http status=" + xhr.status + " for " + aData.manifestURL);
|
||||
let oldHash = app.manifestHash;
|
||||
|
||||
if (xhr.status == 200) {
|
||||
let manifest = xhr.response;
|
||||
if (manifest == null) {
|
||||
sendError("MANIFEST_PARSE_ERROR");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AppsUtils.checkManifest(manifest, app)) {
|
||||
sendError("INVALID_MANIFEST");
|
||||
return;
|
||||
|
@ -1349,14 +1423,33 @@ this.DOMApplicationRegistry = {
|
|||
sendError("INSTALL_FROM_DENIED");
|
||||
return;
|
||||
} else {
|
||||
let hash = this.computeManifestHash(manifest);
|
||||
debug("Manifest hash = " + hash);
|
||||
app.manifestHash = hash;
|
||||
|
||||
app.etag = xhr.getResponseHeader("Etag");
|
||||
debug("at update got app etag=" + app.etag);
|
||||
app.lastCheckedUpdate = Date.now();
|
||||
if (app.origin.startsWith("app://")) {
|
||||
updatePackagedApp.call(this, manifest);
|
||||
if (oldHash != hash) {
|
||||
updatePackagedApp.call(this, manifest);
|
||||
} else {
|
||||
// Like if we got a 304, just send a 'downloadapplied'
|
||||
// or downloadavailable event.
|
||||
aData.event = app.downloadAvailable ? "downloadavailable"
|
||||
: "downloadapplied";
|
||||
aData.app = {
|
||||
lastCheckedUpdate: app.lastCheckedUpdate
|
||||
}
|
||||
aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:OK", aData);
|
||||
this._saveApps();
|
||||
}
|
||||
} else {
|
||||
this._readManifests([{ id: id }], (function(aResult) {
|
||||
updateHostedApp.call(this, aResult[0].manifest, manifest);
|
||||
// Update only the appcache if the manifest has not changed
|
||||
// based on the hash value.
|
||||
updateHostedApp.call(this, aResult[0].manifest,
|
||||
oldHash == hash ? null : manifest);
|
||||
}).bind(this));
|
||||
}
|
||||
}
|
||||
|
@ -1364,7 +1457,7 @@ this.DOMApplicationRegistry = {
|
|||
// The manifest has not changed.
|
||||
if (app.origin.startsWith("app://")) {
|
||||
// If the app is a packaged app, we just send a 'downloadapplied'
|
||||
// event.
|
||||
// or downloadavailable event.
|
||||
app.lastCheckedUpdate = Date.now();
|
||||
aData.event = app.downloadAvailable ? "downloadavailable"
|
||||
: "downloadapplied";
|
||||
|
@ -1467,6 +1560,7 @@ this.DOMApplicationRegistry = {
|
|||
sendError("INVALID_SECURITY_LEVEL");
|
||||
} else {
|
||||
app.etag = xhr.getResponseHeader("Etag");
|
||||
app.manifestHash = this.computeManifestHash(app.manifest);
|
||||
// We allow bypassing the install confirmation process to facilitate
|
||||
// automation.
|
||||
let prefName = "dom.mozApps.auto_confirm_install";
|
||||
|
@ -1521,6 +1615,7 @@ this.DOMApplicationRegistry = {
|
|||
sendError("MANIFEST_PARSE_ERROR");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(AppsUtils.checkManifest(manifest, app) &&
|
||||
manifest.package_path)) {
|
||||
sendError("INVALID_MANIFEST");
|
||||
|
@ -1528,6 +1623,7 @@ this.DOMApplicationRegistry = {
|
|||
sendError("INSTALL_FROM_DENIED");
|
||||
} else {
|
||||
app.etag = xhr.getResponseHeader("Etag");
|
||||
app.manifestHash = this.computeManifestHash(manifest);
|
||||
debug("at install package got app etag=" + app.etag);
|
||||
Services.obs.notifyObservers(aMm, "webapps-ask-install",
|
||||
JSON.stringify(aData));
|
||||
|
@ -1952,151 +2048,158 @@ this.DOMApplicationRegistry = {
|
|||
return;
|
||||
}
|
||||
|
||||
if (requestChannel.responseStatus == 304) {
|
||||
// The package's Etag has not changed.
|
||||
// We send a "applied" event right away.
|
||||
app.downloading = false;
|
||||
app.downloadAvailable = false;
|
||||
app.downloadSize = 0;
|
||||
app.installState = "installed";
|
||||
app.readyToApplyDownload = false;
|
||||
self.broadcastMessage("Webapps:PackageEvent", {
|
||||
type: "applied",
|
||||
manifestURL: aApp.manifestURL,
|
||||
app: app });
|
||||
// Save the updated registry, and cleanup the tmp directory.
|
||||
self._saveApps();
|
||||
let file = FileUtils.getFile("TmpD", ["webapps", id], false);
|
||||
if (file && file.exists()) {
|
||||
file.remove(true);
|
||||
self.computeFileHash(zipFile, function onHashComputed(aHash) {
|
||||
debug("packageHash=" + aHash);
|
||||
let newPackage = (requestChannel.responseStatus != 304) &&
|
||||
(aHash != app.packageHash);
|
||||
|
||||
if (!newPackage) {
|
||||
// The package's Etag or hash has not changed.
|
||||
// We send a "applied" event right away.
|
||||
app.downloading = false;
|
||||
app.downloadAvailable = false;
|
||||
app.downloadSize = 0;
|
||||
app.installState = "installed";
|
||||
app.readyToApplyDownload = false;
|
||||
self.broadcastMessage("Webapps:PackageEvent", {
|
||||
type: "applied",
|
||||
manifestURL: aApp.manifestURL,
|
||||
app: app });
|
||||
// Save the updated registry, and cleanup the tmp directory.
|
||||
self._saveApps();
|
||||
let file = FileUtils.getFile("TmpD", ["webapps", id], false);
|
||||
if (file && file.exists()) {
|
||||
file.remove(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let certdb;
|
||||
try {
|
||||
certdb = Cc["@mozilla.org/security/x509certdb;1"]
|
||||
.getService(Ci.nsIX509CertDB);
|
||||
} catch (e) {
|
||||
cleanup("CERTDB_ERROR");
|
||||
return;
|
||||
}
|
||||
|
||||
certdb.openSignedJARFileAsync(zipFile, function(aRv, aZipReader) {
|
||||
let zipReader;
|
||||
let certdb;
|
||||
try {
|
||||
let isSigned;
|
||||
if (Components.isSuccessCode(aRv)) {
|
||||
isSigned = true;
|
||||
zipReader = aZipReader;
|
||||
} else if (aRv != Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED) {
|
||||
throw "INVALID_SIGNATURE";
|
||||
} else {
|
||||
isSigned = false;
|
||||
zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
|
||||
.createInstance(Ci.nsIZipReader);
|
||||
zipReader.open(zipFile);
|
||||
}
|
||||
|
||||
// XXX Security: You CANNOT safely add a new app store for
|
||||
// installing privileged apps just by modifying this pref and
|
||||
// adding the signing cert for that store to the cert trust
|
||||
// database. *Any* origin listed can install apps signed with
|
||||
// *any* certificate trusted; we don't try to maintain a strong
|
||||
// association between certificate with installOrign. The
|
||||
// expectation here is that in production builds the pref will
|
||||
// contain exactly one origin. However, in custom development
|
||||
// builds it may contain more than one origin so we can test
|
||||
// different stages (dev, staging, prod) of the same app store.
|
||||
//
|
||||
// Only allow signed apps to be installed from a whitelist of
|
||||
// domains, and require all packages installed from any of the
|
||||
// domains on the whitelist to be signed. This is a stopgap until
|
||||
// we have a real story for handling multiple app stores signing
|
||||
// apps.
|
||||
let signedAppOriginsStr =
|
||||
Services.prefs.getCharPref(
|
||||
"dom.mozApps.signed_apps_installable_from");
|
||||
let isSignedAppOrigin
|
||||
= signedAppOriginsStr.split(",").indexOf(aApp.installOrigin) > -1;
|
||||
if (!isSigned && isSignedAppOrigin) {
|
||||
// Packaged apps installed from these origins must be signed;
|
||||
// if not, assume somebody stripped the signature.
|
||||
throw "INVALID_SIGNATURE";
|
||||
} else if (isSigned && !isSignedAppOrigin) {
|
||||
// Other origins are *prohibited* from installing signed apps.
|
||||
// One reason is that our app revociation mechanism requires
|
||||
// strong cooperation from the host of the mini-manifest, which
|
||||
// we assume to be under the control of the install origin,
|
||||
// even if it has a different origin.
|
||||
throw "INSTALL_FROM_DENIED";
|
||||
}
|
||||
|
||||
if (!zipReader.hasEntry("manifest.webapp")) {
|
||||
throw "MISSING_MANIFEST";
|
||||
}
|
||||
|
||||
let istream = zipReader.getInputStream("manifest.webapp");
|
||||
|
||||
// Obtain a converter to read from a UTF-8 encoded input stream.
|
||||
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
|
||||
.createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
converter.charset = "UTF-8";
|
||||
|
||||
let manifest = JSON.parse(converter.ConvertToUnicode(NetUtil.readInputStreamToString(istream,
|
||||
istream.available()) || ""));
|
||||
|
||||
// Call checkManifest before compareManifests, as checkManifest
|
||||
// will normalize some attributes that has already been normalized
|
||||
// for aManifest during checkForUpdate.
|
||||
if (!AppsUtils.checkManifest(manifest, app)) {
|
||||
throw "INVALID_MANIFEST";
|
||||
}
|
||||
|
||||
if (!AppsUtils.compareManifests(manifest,
|
||||
aManifest._manifest)) {
|
||||
throw "MANIFEST_MISMATCH";
|
||||
}
|
||||
|
||||
if (!AppsUtils.checkInstallAllowed(manifest, aApp.installOrigin)) {
|
||||
throw "INSTALL_FROM_DENIED";
|
||||
}
|
||||
|
||||
let maxStatus = isSigned ? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED
|
||||
: Ci.nsIPrincipal.APP_STATUS_INSTALLED;
|
||||
|
||||
if (AppsUtils.getAppManifestStatus(manifest) > maxStatus) {
|
||||
throw "INVALID_SECURITY_LEVEL";
|
||||
}
|
||||
aApp.appStatus = AppsUtils.getAppManifestStatus(manifest);
|
||||
// Save the new Etag for the package.
|
||||
try {
|
||||
app.packageEtag = requestChannel.getResponseHeader("Etag");
|
||||
debug("Package etag=" + app.packageEtag);
|
||||
} catch (e) {
|
||||
// in https://bugzilla.mozilla.org/show_bug.cgi?id=825218
|
||||
// we'll fail gracefully in this case
|
||||
// for now, just going on
|
||||
app.packageEtag = null;
|
||||
debug("Can't find an etag, this should not happen");
|
||||
}
|
||||
|
||||
if (aOnSuccess) {
|
||||
aOnSuccess(id, manifest);
|
||||
}
|
||||
certdb = Cc["@mozilla.org/security/x509certdb;1"]
|
||||
.getService(Ci.nsIX509CertDB);
|
||||
} catch (e) {
|
||||
// Something bad happened when reading the package.
|
||||
if (typeof e == 'object') {
|
||||
Cu.reportError("Error while reading package:" + e);
|
||||
cleanup("INVALID_PACKAGE");
|
||||
} else {
|
||||
cleanup(e);
|
||||
}
|
||||
} finally {
|
||||
AppDownloadManager.remove(aApp.manifestURL);
|
||||
if (zipReader)
|
||||
zipReader.close();
|
||||
cleanup("CERTDB_ERROR");
|
||||
return;
|
||||
}
|
||||
|
||||
certdb.openSignedJARFileAsync(zipFile, function(aRv, aZipReader) {
|
||||
let zipReader;
|
||||
try {
|
||||
let isSigned;
|
||||
if (Components.isSuccessCode(aRv)) {
|
||||
isSigned = true;
|
||||
zipReader = aZipReader;
|
||||
} else if (aRv != Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED) {
|
||||
throw "INVALID_SIGNATURE";
|
||||
} else {
|
||||
isSigned = false;
|
||||
zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
|
||||
.createInstance(Ci.nsIZipReader);
|
||||
zipReader.open(zipFile);
|
||||
}
|
||||
|
||||
// XXX Security: You CANNOT safely add a new app store for
|
||||
// installing privileged apps just by modifying this pref and
|
||||
// adding the signing cert for that store to the cert trust
|
||||
// database. *Any* origin listed can install apps signed with
|
||||
// *any* certificate trusted; we don't try to maintain a strong
|
||||
// association between certificate with installOrign. The
|
||||
// expectation here is that in production builds the pref will
|
||||
// contain exactly one origin. However, in custom development
|
||||
// builds it may contain more than one origin so we can test
|
||||
// different stages (dev, staging, prod) of the same app store.
|
||||
//
|
||||
// Only allow signed apps to be installed from a whitelist of
|
||||
// domains, and require all packages installed from any of the
|
||||
// domains on the whitelist to be signed. This is a stopgap until
|
||||
// we have a real story for handling multiple app stores signing
|
||||
// apps.
|
||||
let signedAppOriginsStr =
|
||||
Services.prefs.getCharPref(
|
||||
"dom.mozApps.signed_apps_installable_from");
|
||||
let isSignedAppOrigin
|
||||
= signedAppOriginsStr.split(",").indexOf(aApp.installOrigin) > -1;
|
||||
if (!isSigned && isSignedAppOrigin) {
|
||||
// Packaged apps installed from these origins must be signed;
|
||||
// if not, assume somebody stripped the signature.
|
||||
throw "INVALID_SIGNATURE";
|
||||
} else if (isSigned && !isSignedAppOrigin) {
|
||||
// Other origins are *prohibited* from installing signed apps.
|
||||
// One reason is that our app revociation mechanism requires
|
||||
// strong cooperation from the host of the mini-manifest, which
|
||||
// we assume to be under the control of the install origin,
|
||||
// even if it has a different origin.
|
||||
throw "INSTALL_FROM_DENIED";
|
||||
}
|
||||
|
||||
if (!zipReader.hasEntry("manifest.webapp")) {
|
||||
throw "MISSING_MANIFEST";
|
||||
}
|
||||
|
||||
let istream = zipReader.getInputStream("manifest.webapp");
|
||||
|
||||
// Obtain a converter to read from a UTF-8 encoded input stream.
|
||||
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
|
||||
.createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
converter.charset = "UTF-8";
|
||||
|
||||
let manifest = JSON.parse(converter.ConvertToUnicode(NetUtil.readInputStreamToString(istream,
|
||||
istream.available()) || ""));
|
||||
|
||||
// Call checkManifest before compareManifests, as checkManifest
|
||||
// will normalize some attributes that has already been normalized
|
||||
// for aManifest during checkForUpdate.
|
||||
if (!AppsUtils.checkManifest(manifest, app)) {
|
||||
throw "INVALID_MANIFEST";
|
||||
}
|
||||
|
||||
if (!AppsUtils.compareManifests(manifest,
|
||||
aManifest._manifest)) {
|
||||
throw "MANIFEST_MISMATCH";
|
||||
}
|
||||
|
||||
if (!AppsUtils.checkInstallAllowed(manifest, aApp.installOrigin)) {
|
||||
throw "INSTALL_FROM_DENIED";
|
||||
}
|
||||
|
||||
let maxStatus = isSigned ? Ci.nsIPrincipal.APP_STATUS_PRIVILEGED
|
||||
: Ci.nsIPrincipal.APP_STATUS_INSTALLED;
|
||||
|
||||
if (AppsUtils.getAppManifestStatus(manifest) > maxStatus) {
|
||||
throw "INVALID_SECURITY_LEVEL";
|
||||
}
|
||||
app.appStatus = AppsUtils.getAppManifestStatus(manifest);
|
||||
app.packageHash = aHash;
|
||||
// Save the new Etag for the package.
|
||||
try {
|
||||
app.packageEtag = requestChannel.getResponseHeader("Etag");
|
||||
debug("Package etag=" + app.packageEtag);
|
||||
} catch (e) {
|
||||
// in https://bugzilla.mozilla.org/show_bug.cgi?id=825218
|
||||
// we'll fail gracefully in this case
|
||||
// for now, just going on
|
||||
app.packageEtag = null;
|
||||
debug("Can't find an etag, this should not happen");
|
||||
}
|
||||
|
||||
if (aOnSuccess) {
|
||||
aOnSuccess(id, manifest);
|
||||
}
|
||||
} catch (e) {
|
||||
// Something bad happened when reading the package.
|
||||
if (typeof e == 'object') {
|
||||
Cu.reportError("Error while reading package:" + e);
|
||||
cleanup("INVALID_PACKAGE");
|
||||
} else {
|
||||
cleanup(e);
|
||||
}
|
||||
} finally {
|
||||
AppDownloadManager.remove(aApp.manifestURL);
|
||||
if (zipReader)
|
||||
zipReader.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче