Bug 591070: Support specifying an XPI hash through the initial HTTPS request such that redirects to HTTP can be followed securely. r=robstrong, a=blocking-b6

This commit is contained in:
Dave Townsend 2010-09-07 12:16:35 -07:00
parent 4dd683b489
commit d67000418a
9 changed files with 284 additions and 70 deletions

View File

@ -4011,50 +4011,6 @@ var XPIDatabase = {
}
};
/**
* Handles callbacks for HTTP channels of XPI downloads. We support
* prompting for auth dialogs and, optionally, to ignore bad certs.
*
* @param aWindow
* An optional DOM Element related to the request
* @param aNeedBadCertHandling
* Whether we should handle bad certs or not
*/
function XPINotificationCallbacks(aWindow, aNeedBadCertHandling) {
this.window = aWindow;
// Verify that we don't end up on an insecure channel if we haven't got a
// hash to verify with (see bug 537761 for discussion)
this.needBadCertHandling = aNeedBadCertHandling;
if (this.needBadCertHandling) {
Components.utils.import("resource://gre/modules/CertUtils.jsm");
let requireBuiltIn = Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true);
this.badCertHandler = new BadCertHandler(!requireBuiltIn);
}
}
XPINotificationCallbacks.prototype = {
QueryInterface: function(iid) {
if (iid.equals(Ci.nsISupports) || iid.equals(Ci.nsIInterfaceRequestor))
return this;
throw Components.results.NS_ERROR_NO_INTERFACE;
},
getInterface: function(iid) {
if (iid.equals(Components.interfaces.nsIAuthPrompt2)) {
var factory = Cc["@mozilla.org/prompter;1"].
getService(Ci.nsIPromptFactory);
return factory.getPrompt(this.window, Ci.nsIAuthPrompt);
}
if (this.needBadCertHandling)
return this.badCertHandler.getInterface(iid);
throw Components.results.NS_ERROR_NO_INTERFACE;
},
};
/**
* Instantiates an AddonInstall and passes the new object to a callback when
* it is complete.
@ -4196,6 +4152,7 @@ AddonInstall.prototype = {
crypto: null,
hash: null,
loadGroup: null,
badCertHandler: null,
listeners: null,
name: null,
@ -4546,30 +4503,6 @@ AddonInstall.prototype = {
return;
}
this.crypto = Cc["@mozilla.org/security/hash;1"].
createInstance(Ci.nsICryptoHash);
if (this.hash) {
[alg, this.hash] = this.hash.split(":", 2);
try {
this.crypto.initWithString(alg);
}
catch (e) {
WARN("Unknown hash algorithm " + alg);
this.state = AddonManager.STATE_DOWNLOAD_FAILED;
this.error = AddonManager.ERROR_INCORRECT_HASH;
XPIProvider.removeActiveInstall(this);
AddonManagerPrivate.callInstallListeners("onDownloadFailed",
this.listeners, this.wrapper);
return;
}
}
else {
// We always need something to consume data from the inputstream passed
// to onDataAvailable so just create a dummy cryptohasher to do that.
this.crypto.initWithString("sha1");
}
try {
this.file = getTemporaryFile();
this.ownsTempFile = true;
@ -4592,9 +4525,12 @@ AddonInstall.prototype = {
createInstance(Ci.nsIStreamListenerTee);
listener.init(this, this.stream);
try {
Components.utils.import("resource://gre/modules/CertUtils.jsm");
let requireBuiltIn = Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true);
this.badCertHandler = new BadCertHandler(!requireBuiltIn);
this.channel = NetUtil.newChannel(this.sourceURI);
this.channel.notificationCallbacks =
new XPINotificationCallbacks(this.window, !this.hash);
this.channel.notificationCallbacks = this;
this.channel.QueryInterface(Ci.nsIHttpChannelInternal)
.forceAllowThirdPartyCookie = true;
this.channel.asyncOpen(listener, null);
@ -4626,12 +4562,61 @@ AddonInstall.prototype = {
}
},
/**
* Check the redirect response for a hash of the target XPI and verify that
* we don't end up on an insecure channel.
*
* @see nsIChannelEventSink
*/
asyncOnChannelRedirect: function(aOldChannel, aNewChannel, aFlags, aCallback) {
if (!this.hash && aOldChannel.originalURI.schemeIs("https") &&
aOldChannel instanceof Ci.nsIHttpChannel) {
try {
this.hash = aOldChannel.getResponseHeader("X-Target-Digest");
}
catch (e) {
}
}
// Verify that we don't end up on an insecure channel if we haven't got a
// hash to verify with (see bug 537761 for discussion)
if (!this.hash)
this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback);
else
aCallback.onRedirectVerifyCallback(Cr.NS_OK);
},
/**
* This is the first chance to get at real headers on the channel.
*
* @see nsIStreamListener
*/
onStartRequest: function AI_onStartRequest(aRequest, aContext) {
this.crypto = Cc["@mozilla.org/security/hash;1"].
createInstance(Ci.nsICryptoHash);
if (this.hash) {
[alg, this.hash] = this.hash.split(":", 2);
try {
this.crypto.initWithString(alg);
}
catch (e) {
WARN("Unknown hash algorithm " + alg);
this.state = AddonManager.STATE_DOWNLOAD_FAILED;
this.error = AddonManager.ERROR_INCORRECT_HASH;
XPIProvider.removeActiveInstall(this);
AddonManagerPrivate.callInstallListeners("onDownloadFailed",
this.listeners, this.wrapper);
aRequest.cancel(Cr.NS_BINDING_ABORTED);
return;
}
}
else {
// We always need something to consume data from the inputstream passed
// to onDataAvailable so just create a dummy cryptohasher to do that.
this.crypto.initWithString("sha1");
}
this.progress = 0;
if (aRequest instanceof Ci.nsIChannel) {
try {
@ -4652,6 +4637,7 @@ AddonInstall.prototype = {
onStopRequest: function AI_onStopRequest(aRequest, aContext, aStatus) {
this.stream.close();
this.channel = null;
this.badCerthandler = null;
Services.obs.removeObserver(this, "network:offline-about-to-go-offline");
// If the download was cancelled then all events will have already been sent
@ -4931,6 +4917,19 @@ AddonInstall.prototype = {
finally {
this.removeTemporaryFile();
}
},
getInterface: function(iid) {
if (iid.equals(Ci.nsIAuthPrompt2)) {
var factory = Cc["@mozilla.org/prompter;1"].
getService(Ci.nsIPromptFactory);
return factory.getPrompt(null, Ci.nsIAuthPrompt);
}
else if (iid.equals(Ci.nsIChannelEventSink)) {
return this;
}
return this.badCertHandler.getInterface(iid);
}
}

View File

@ -85,6 +85,11 @@ _BROWSER_FILES = head.js \
browser_cancel.js \
browser_multipackage.js \
browser_trigger_redirect.js \
browser_httphash.js \
browser_httphash2.js \
browser_httphash3.js \
browser_httphash4.js \
browser_httphash5.js \
unsigned.xpi \
signed.xpi \
signed2.xpi \
@ -105,6 +110,7 @@ _BROWSER_FILES = head.js \
triggerredirect.html \
authRedirect.sjs \
cookieRedirect.sjs \
hashRedirect.sjs \
bug540558.html \
$(NULL)

View File

@ -0,0 +1,39 @@
// ----------------------------------------------------------------------------
// Test whether an install succeeds when a valid hash is included in the HTTPS
// request
// This verifies bug 591070
function test() {
Harness.installEndedCallback = install_ended;
Harness.installsCompletedCallback = finish_test;
Harness.setup();
var pm = Services.perms;
pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs";
url += "?sha1:3d0dc22e1f394e159b08aaf5f0f97de4d5c65f4f|" + TESTROOT + "unsigned.xpi";
var triggers = encodeURIComponent(JSON.stringify({
"Unsigned XPI": {
URL: url,
toString: function() { return this.URL; }
}
}));
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
}
function install_ended(install, addon) {
install.cancel();
}
function finish_test(count) {
is(count, 1, "1 Add-on should have been successfully installed");
Services.perms.remove("example.com", "install");
Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
gBrowser.removeCurrentTab();
Harness.finish();
}

View File

@ -0,0 +1,39 @@
// ----------------------------------------------------------------------------
// Test whether an install fails when a invalid hash is included in the HTTPS
// request
// This verifies bug 591070
function test() {
Harness.downloadFailedCallback = download_failed;
Harness.installsCompletedCallback = finish_test;
Harness.setup();
var pm = Services.perms;
pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs";
url += "?sha1:foobar|" + TESTROOT + "unsigned.xpi";
var triggers = encodeURIComponent(JSON.stringify({
"Unsigned XPI": {
URL: url,
toString: function() { return this.URL; }
}
}));
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
}
function download_failed(install) {
is(install.error, AddonManager.ERROR_INCORRECT_HASH, "Download should fail");
}
function finish_test(count) {
is(count, 0, "0 Add-ons should have been successfully installed");
Services.perms.remove("example.com", "install");
Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
gBrowser.removeCurrentTab();
Harness.finish();
}

View File

@ -0,0 +1,39 @@
// ----------------------------------------------------------------------------
// Tests that the HTTPS hash is ignored when InstallTrigger is passed a hash.
// This verifies bug 591070
function test() {
Harness.installEndedCallback = install_ended;
Harness.installsCompletedCallback = finish_test;
Harness.setup();
var pm = Services.perms;
pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs";
url += "?sha1:foobar|" + TESTROOT + "unsigned.xpi";
var triggers = encodeURIComponent(JSON.stringify({
"Unsigned XPI": {
URL: url,
Hash: "sha1:3d0dc22e1f394e159b08aaf5f0f97de4d5c65f4f",
toString: function() { return this.URL; }
}
}));
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
}
function install_ended(install, addon) {
install.cancel();
}
function finish_test(count) {
is(count, 1, "1 Add-on should have been successfully installed");
Services.perms.remove("example.com", "install");
Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
gBrowser.removeCurrentTab();
Harness.finish();
}

View File

@ -0,0 +1,36 @@
// ----------------------------------------------------------------------------
// Test that hashes are ignored in the headers of HTTP requests
// This verifies bug 591070
function test() {
Harness.installEndedCallback = install_ended;
Harness.installsCompletedCallback = finish_test;
Harness.setup();
var pm = Services.perms;
pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
var url = "http://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs";
url += "?sha1:foobar|" + TESTROOT + "unsigned.xpi";
var triggers = encodeURIComponent(JSON.stringify({
"Unsigned XPI": {
URL: url,
toString: function() { return this.URL; }
}
}));
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
}
function install_ended(install, addon) {
install.cancel();
}
function finish_test(count) {
is(count, 1, "1 Add-on should have been successfully installed");
Services.perms.remove("example.com", "install");
gBrowser.removeCurrentTab();
Harness.finish();
}

View File

@ -0,0 +1,40 @@
// ----------------------------------------------------------------------------
// Test that only the first HTTPS hash is used
// This verifies bug 591070
function test() {
Harness.installEndedCallback = install_ended;
Harness.installsCompletedCallback = finish_test;
Harness.setup();
var pm = Services.perms;
pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs";
url += "?sha1:3d0dc22e1f394e159b08aaf5f0f97de4d5c65f4f|";
url += "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs";
url += "?sha1:foobar|" + TESTROOT + "unsigned.xpi";
var triggers = encodeURIComponent(JSON.stringify({
"Unsigned XPI": {
URL: url,
toString: function() { return this.URL; }
}
}));
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
}
function install_ended(install, addon) {
install.cancel();
}
function finish_test(count) {
is(count, 1, "1 Add-on should have been successfully installed");
Services.perms.remove("example.com", "install");
Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
gBrowser.removeCurrentTab();
Harness.finish();
}

View File

@ -0,0 +1,15 @@
// Simple script redirects takes the query part of te request and splits it on
// the | character. Anything before is included as the X-Target-Digest header
// the latter part is used as the url to redirect to
function handleRequest(request, response)
{
let pos = request.queryString.indexOf("|");
let header = request.queryString.substring(0, pos);
let url = request.queryString.substring(pos + 1);
response.setStatusLine(request.httpVersion, 302, "Found");
response.setHeader("X-Target-Digest", header);
response.setHeader("Location", url);
response.write("See " + url);
}

View File

@ -7,6 +7,7 @@ const XPINSTALL_URL = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul";
const PROMPT_URL = "chrome://global/content/commonDialog.xul";
const ADDONS_URL = "chrome://mozapps/content/extensions/extensions.xul";
const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
Components.utils.import("resource://gre/modules/AddonManager.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");