Bug 829934 - Implement a hash verifier for webapp/mini manifest r=ferjm

This commit is contained in:
Fabrice Desré 2013-01-18 09:54:06 -08:00
Родитель 5b76466f52
Коммит 2ca010d058
2 изменённых файлов: 248 добавлений и 143 удалений

Просмотреть файл

@ -58,6 +58,8 @@ this.AppsUtils = {
updateTime: aApp.updateTime, updateTime: aApp.updateTime,
etag: aApp.etag, etag: aApp.etag,
packageEtag: aApp.packageEtag, packageEtag: aApp.packageEtag,
manifestHash: aApp.manifestHash,
packageHash: aApp.packageHash,
installerAppId: aApp.installerAppId || Ci.nsIScriptSecurityManager.NO_APP_ID, installerAppId: aApp.installerAppId || Ci.nsIScriptSecurityManager.NO_APP_ID,
installerIsBrowser: !!aApp.installerIsBrowser 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) { checkForUpdate: function(aData, aMm) {
debug("checkForUpdate for " + aData.manifestURL); debug("checkForUpdate for " + aData.manifestURL);
let id = this._appIdForManifestURL(aData.manifestURL); let id = this._appIdForManifestURL(aData.manifestURL);
@ -1336,12 +1407,15 @@ this.DOMApplicationRegistry = {
xhr.addEventListener("load", (function() { xhr.addEventListener("load", (function() {
debug("Got http status=" + xhr.status + " for " + aData.manifestURL); debug("Got http status=" + xhr.status + " for " + aData.manifestURL);
let oldHash = app.manifestHash;
if (xhr.status == 200) { if (xhr.status == 200) {
let manifest = xhr.response; let manifest = xhr.response;
if (manifest == null) { if (manifest == null) {
sendError("MANIFEST_PARSE_ERROR"); sendError("MANIFEST_PARSE_ERROR");
return; return;
} }
if (!AppsUtils.checkManifest(manifest, app)) { if (!AppsUtils.checkManifest(manifest, app)) {
sendError("INVALID_MANIFEST"); sendError("INVALID_MANIFEST");
return; return;
@ -1349,14 +1423,33 @@ this.DOMApplicationRegistry = {
sendError("INSTALL_FROM_DENIED"); sendError("INSTALL_FROM_DENIED");
return; return;
} else { } else {
let hash = this.computeManifestHash(manifest);
debug("Manifest hash = " + hash);
app.manifestHash = hash;
app.etag = xhr.getResponseHeader("Etag"); app.etag = xhr.getResponseHeader("Etag");
debug("at update got app etag=" + app.etag); debug("at update got app etag=" + app.etag);
app.lastCheckedUpdate = Date.now(); app.lastCheckedUpdate = Date.now();
if (app.origin.startsWith("app://")) { 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 { } else {
this._readManifests([{ id: id }], (function(aResult) { 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)); }).bind(this));
} }
} }
@ -1364,7 +1457,7 @@ this.DOMApplicationRegistry = {
// The manifest has not changed. // The manifest has not changed.
if (app.origin.startsWith("app://")) { if (app.origin.startsWith("app://")) {
// If the app is a packaged app, we just send a 'downloadapplied' // If the app is a packaged app, we just send a 'downloadapplied'
// event. // or downloadavailable event.
app.lastCheckedUpdate = Date.now(); app.lastCheckedUpdate = Date.now();
aData.event = app.downloadAvailable ? "downloadavailable" aData.event = app.downloadAvailable ? "downloadavailable"
: "downloadapplied"; : "downloadapplied";
@ -1467,6 +1560,7 @@ this.DOMApplicationRegistry = {
sendError("INVALID_SECURITY_LEVEL"); sendError("INVALID_SECURITY_LEVEL");
} else { } else {
app.etag = xhr.getResponseHeader("Etag"); app.etag = xhr.getResponseHeader("Etag");
app.manifestHash = this.computeManifestHash(app.manifest);
// We allow bypassing the install confirmation process to facilitate // We allow bypassing the install confirmation process to facilitate
// automation. // automation.
let prefName = "dom.mozApps.auto_confirm_install"; let prefName = "dom.mozApps.auto_confirm_install";
@ -1521,6 +1615,7 @@ this.DOMApplicationRegistry = {
sendError("MANIFEST_PARSE_ERROR"); sendError("MANIFEST_PARSE_ERROR");
return; return;
} }
if (!(AppsUtils.checkManifest(manifest, app) && if (!(AppsUtils.checkManifest(manifest, app) &&
manifest.package_path)) { manifest.package_path)) {
sendError("INVALID_MANIFEST"); sendError("INVALID_MANIFEST");
@ -1528,6 +1623,7 @@ this.DOMApplicationRegistry = {
sendError("INSTALL_FROM_DENIED"); sendError("INSTALL_FROM_DENIED");
} else { } else {
app.etag = xhr.getResponseHeader("Etag"); app.etag = xhr.getResponseHeader("Etag");
app.manifestHash = this.computeManifestHash(manifest);
debug("at install package got app etag=" + app.etag); debug("at install package got app etag=" + app.etag);
Services.obs.notifyObservers(aMm, "webapps-ask-install", Services.obs.notifyObservers(aMm, "webapps-ask-install",
JSON.stringify(aData)); JSON.stringify(aData));
@ -1952,151 +2048,158 @@ this.DOMApplicationRegistry = {
return; return;
} }
if (requestChannel.responseStatus == 304) { self.computeFileHash(zipFile, function onHashComputed(aHash) {
// The package's Etag has not changed. debug("packageHash=" + aHash);
// We send a "applied" event right away. let newPackage = (requestChannel.responseStatus != 304) &&
app.downloading = false; (aHash != app.packageHash);
app.downloadAvailable = false;
app.downloadSize = 0; if (!newPackage) {
app.installState = "installed"; // The package's Etag or hash has not changed.
app.readyToApplyDownload = false; // We send a "applied" event right away.
self.broadcastMessage("Webapps:PackageEvent", { app.downloading = false;
type: "applied", app.downloadAvailable = false;
manifestURL: aApp.manifestURL, app.downloadSize = 0;
app: app }); app.installState = "installed";
// Save the updated registry, and cleanup the tmp directory. app.readyToApplyDownload = false;
self._saveApps(); self.broadcastMessage("Webapps:PackageEvent", {
let file = FileUtils.getFile("TmpD", ["webapps", id], false); type: "applied",
if (file && file.exists()) { manifestURL: aApp.manifestURL,
file.remove(true); 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; 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;
try { try {
let isSigned; certdb = Cc["@mozilla.org/security/x509certdb;1"]
if (Components.isSuccessCode(aRv)) { .getService(Ci.nsIX509CertDB);
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);
}
} catch (e) { } catch (e) {
// Something bad happened when reading the package. cleanup("CERTDB_ERROR");
if (typeof e == 'object') { return;
Cu.reportError("Error while reading package:" + e);
cleanup("INVALID_PACKAGE");
} else {
cleanup(e);
}
} finally {
AppDownloadManager.remove(aApp.manifestURL);
if (zipReader)
zipReader.close();
} }
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();
}
});
}); });
} }
}); });