Bug 704988: Check the add-on hotfix is signed by a specific certificate. r=robstrong, r=Unfocused

This commit is contained in:
Dave Townsend 2011-12-16 12:04:28 -08:00
parent 9db4de22b4
commit 7e30293660
14 changed files with 480 additions and 51 deletions

View File

@ -82,6 +82,8 @@ pref("extensions.blocklist.itemURL", "https://addons.mozilla.org/%LOCALE%/%APP%/
pref("extensions.update.autoUpdateDefault", true);
pref("extensions.hotfix.id", "firefox-hotfix@mozilla.org");
pref("extensions.hotfix.cert.checkAttributes", true);
pref("extensions.hotfix.certs.1.sha1Fingerprint", "foo");
// Disable add-ons installed into the shared user and shared system areas by
// default. This does not include the application directory. See the SCOPE

View File

@ -396,6 +396,7 @@ user_pref("browser.safebrowsing.provider.0.updateURL", "http://%(server)s/safebr
// Point update checks to the local testing server for fast failures
user_pref("extensions.update.url", "http://%(server)s/extensions-dummy/updateURL");
user_pref("extensions.blocklist.url", "http://%(server)s/extensions-dummy/blocklistURL");
user_pref("extensions.hotfix.url", "http://%(server)s/extensions-dummy/hotfixURL");
// Make sure opening about:addons won't hit the network
user_pref("extensions.webservice.discoverURL", "http://%(server)s/extensions-dummy/discoveryURL");
// Make sure AddonRepository won't hit the network

View File

@ -56,6 +56,8 @@ const PREF_APP_UPDATE_AUTO = "app.update.auto";
const PREF_EM_HOTFIX_ID = "extensions.hotfix.id";
const PREF_EM_HOTFIX_LASTVERSION = "extensions.hotfix.lastVersion";
const PREF_EM_HOTFIX_URL = "extensions.hotfix.url";
const PREF_EM_CERT_CHECKATTRIBUTES = "extensions.hotfix.cert.checkAttributes";
const PREF_EM_HOTFIX_CERTS = "extensions.hotfix.certs.";
const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
const PREF_SELECTED_LOCALE = "general.useragent.locale";
@ -70,6 +72,8 @@ const TOOLKIT_ID = "toolkit@mozilla.org";
const VALID_TYPES_REGEXP = /^[\w\-]+$/;
Components.utils.import("resource://gre/modules/Services.jsm");
var CertUtils = {};
Components.utils.import("resource://gre/modules/CertUtils.jsm", CertUtils);
var EXPORTED_SYMBOLS = [ "AddonManager", "AddonManagerPrivate" ];
@ -831,6 +835,27 @@ var AddonManagerInternal = {
LOG("Downloading hotfix version " + update.version);
AddonManager.getInstallForURL(update.updateURL, function(aInstall) {
aInstall.addListener({
onDownloadEnded: function(aInstall) {
try {
if (!Services.prefs.getBoolPref(PREF_EM_CERT_CHECKATTRIBUTES))
return;
}
catch (e) {
// By default don't do certificate checks.
return;
}
try {
CertUtils.validateCert(aInstall.certificate,
CertUtils.readCertPrefs(PREF_EM_HOTFIX_CERTS));
}
catch (e) {
WARN("The hotfix add-on was not signed by the expected " +
"certificate and so will not be installed.");
aInstall.cancel();
}
},
onInstallEnded: function(aInstall) {
// Remember the last successfully installed version.
Services.prefs.setCharPref(PREF_EM_HOTFIX_LASTVERSION,

View File

@ -97,6 +97,7 @@ _TEST_FILES = \
head.js \
browser_bug557956.js \
browser_bug616841.js \
browser_hotfix.js \
browser_updatessl.js \
browser_installssl.js \
browser_newaddon.js \
@ -127,6 +128,10 @@ _TEST_RESOURCES = \
browser_eula.xml \
browser_purchase.xml \
discovery.html \
signed_hotfix.rdf \
signed_hotfix.xpi \
unsigned_hotfix.rdf \
unsigned_hotfix.xpi \
more_options.xul \
options.xul \
redirect.sjs \

View File

@ -0,0 +1,186 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
const PREF_EM_HOTFIX_ID = "extensions.hotfix.id";
const PREF_EM_HOTFIX_LASTVERSION = "extensions.hotfix.lastVersion";
const PREF_EM_HOTFIX_URL = "extensions.hotfix.url";
const PREF_EM_HOTFIX_CERTS = "extensions.hotfix.certs.";
const PREF_EM_CERT_CHECKATTRIBUTES = "extensions.hotfix.cert.checkAttributes";
const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts";
const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
const HOTFIX_ID = "hotfix@tests.mozilla.org";
var gNextTest;
var SuccessfulInstallListener = {
onDownloadCancelled: function(aInstall) {
ok(false, "Should not have seen the download cancelled");
is(aInstall.addon.id, HOTFIX_ID, "Should have seen the right add-on");
AddonManager.removeInstallListener(this);
gNextTest();
},
onInstallEnded: function(aInstall) {
ok(true, "Should have seen the install complete");
is(aInstall.addon.id, HOTFIX_ID, "Should have installed the right add-on");
AddonManager.removeInstallListener(this);
aInstall.addon.uninstall();
Services.prefs.clearUserPref(PREF_EM_HOTFIX_LASTVERSION);
gNextTest();
}
}
var FailedInstallListener = {
onDownloadCancelled: function(aInstall) {
ok(true, "Should have seen the download cancelled");
is(aInstall.addon.id, HOTFIX_ID, "Should have seen the right add-on");
AddonManager.removeInstallListener(this);
gNextTest();
},
onInstallEnded: function(aInstall) {
ok(false, "Should not have seen the install complete");
is(aInstall.addon.id, HOTFIX_ID, "Should have installed the right add-on");
AddonManager.removeInstallListener(this);
aInstall.addon.uninstall();
Services.prefs.clearUserPref(PREF_EM_HOTFIX_LASTVERSION);
gNextTest();
}
}
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, true);
Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
Services.prefs.setBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS, false);
Services.prefs.setCharPref(PREF_EM_HOTFIX_ID, HOTFIX_ID);
var oldURL = Services.prefs.getCharPref(PREF_EM_HOTFIX_URL);
Services.prefs.setCharPref(PREF_EM_HOTFIX_URL, TESTROOT + "signed_hotfix.rdf");
registerCleanupFunction(function() {
Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, false);
Services.prefs.clearUserPref(PREF_EM_HOTFIX_ID);
Services.prefs.setCharPref(PREF_EM_HOTFIX_URL, oldURL);
Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
Services.prefs.clearUserPref(PREF_UPDATE_REQUIREBUILTINCERTS);
Services.prefs.clearUserPref(PREF_EM_CERT_CHECKATTRIBUTES);
var prefs = Services.prefs.getChildList(PREF_EM_HOTFIX_CERTS);
prefs.forEach(Services.prefs.clearUserPref);
});
run_next_test();
}
function end_test() {
finish();
}
add_test(function check_no_cert_checks() {
Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, false);
AddonManager.addInstallListener(SuccessfulInstallListener);
gNextTest = run_next_test;
AddonManagerPrivate.backgroundUpdateCheck();
});
add_test(function check_wrong_cert_fingerprint() {
Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "foo");
AddonManager.addInstallListener(FailedInstallListener);
gNextTest = function() {
Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
run_next_test();
};
AddonManagerPrivate.backgroundUpdateCheck();
});
add_test(function check_right_cert_fingerprint() {
Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "3E:B9:4E:07:12:FE:3C:01:41:46:13:46:FC:84:52:1A:8C:BE:1D:A2");
AddonManager.addInstallListener(SuccessfulInstallListener);
gNextTest = function() {
Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
run_next_test();
};
AddonManagerPrivate.backgroundUpdateCheck();
});
add_test(function check_multi_cert_fingerprint_1() {
Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "3E:B9:4E:07:12:FE:3C:01:41:46:13:46:FC:84:52:1A:8C:BE:1D:A2");
Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "2.sha1Fingerprint", "foo");
AddonManager.addInstallListener(SuccessfulInstallListener);
gNextTest = function() {
Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "2.sha1Fingerprint");
run_next_test();
};
AddonManagerPrivate.backgroundUpdateCheck();
});
add_test(function check_multi_cert_fingerprint_2() {
Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "foo");
Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "2.sha1Fingerprint", "3E:B9:4E:07:12:FE:3C:01:41:46:13:46:FC:84:52:1A:8C:BE:1D:A2");
AddonManager.addInstallListener(SuccessfulInstallListener);
gNextTest = function() {
Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "2.sha1Fingerprint");
run_next_test();
};
AddonManagerPrivate.backgroundUpdateCheck();
});
add_test(function check_no_cert_no_checks() {
Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, false);
Services.prefs.setCharPref(PREF_EM_HOTFIX_URL, TESTROOT + "unsigned_hotfix.rdf");
AddonManager.addInstallListener(SuccessfulInstallListener);
gNextTest = run_next_test;
AddonManagerPrivate.backgroundUpdateCheck();
});
add_test(function check_no_cert_cert_fingerprint_check() {
Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "3E:B9:4E:07:12:FE:3C:01:41:46:13:46:FC:84:52:1A:8C:BE:1D:A2");
AddonManager.addInstallListener(FailedInstallListener);
gNextTest = function() {
Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
run_next_test();
};
AddonManagerPrivate.backgroundUpdateCheck();
});

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:extension:hotfix@tests.mozilla.org">
<em:updates>
<Seq>
<li>
<Description>
<em:version>1.0</em:version>
<em:targetApplication>
<Description>
<em:id>toolkit@mozilla.org</em:id>
<em:minVersion>0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
<em:updateLink>https://example.com/browser/toolkit/mozapps/extensions/test/browser/signed_hotfix.xpi</em:updateLink>
</Description>
</em:targetApplication>
</Description>
</li>
</Seq>
</em:updates>
</Description>
</RDF>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:extension:hotfix@tests.mozilla.org">
<em:updates>
<Seq>
<li>
<Description>
<em:version>1.0</em:version>
<em:targetApplication>
<Description>
<em:id>toolkit@mozilla.org</em:id>
<em:minVersion>0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
<em:updateLink>https://example.com/browser/toolkit/mozapps/extensions/test/browser/unsigned_hotfix.xpi</em:updateLink>
</Description>
</em:targetApplication>
</Description>
</li>
</Seq>
</em:updates>
</Description>
</RDF>

View File

@ -6,6 +6,8 @@
// The test extension uses an insecure update url.
Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
// Ignore any certificate requirements the app has set
Services.prefs.setBoolPref("extensions.hotfix.cert.checkAttributes", false);
do_load_httpd_js();
var testserver;

View File

@ -39,13 +39,116 @@
*
* ***** END LICENSE BLOCK ***** */
#endif
EXPORTED_SYMBOLS = [ "BadCertHandler", "checkCert" ];
EXPORTED_SYMBOLS = [ "BadCertHandler", "checkCert", "readCertPrefs", "validateCert" ];
const Ce = Components.Exception;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
Components.utils.import("resource://gre/modules/Services.jsm");
/**
* Reads a set of expected certificate attributes from preferences. The returned
* array can be passed to validateCert or checkCert to validate that a
* certificate matches the expected attributes. The preferences should look like
* this:
* prefix.1.attribute1
* prefix.1.attribute2
* prefix.2.attribute1
* etc.
* Each numeric branch contains a set of required attributes for a single
* certificate. Having multiple numeric branches means that multiple
* certificates would be accepted by validateCert.
*
* @param aPrefBranch
* The prefix for all preferences, should end with a ".".
* @return An array of JS objects with names / values corresponding to the
* expected certificate's attribute names / values.
*/
function readCertPrefs(aPrefBranch) {
if (Services.prefs.getBranch(aPrefBranch).getChildList("").length == 0)
return null;
let certs = [];
let counter = 1;
while (true) {
let prefBranchCert = Services.prefs.getBranch(aPrefBranch + counter + ".");
let prefCertAttrs = prefBranchCert.getChildList("");
if (prefCertAttrs.length == 0)
break;
let certAttrs = {};
for each (let prefCertAttr in prefCertAttrs)
certAttrs[prefCertAttr] = prefBranchCert.getCharPref(prefCertAttr);
certs.push(certAttrs);
counter++;
}
return certs;
}
/**
* Verifies that an nsIX509Cert matches the expected certificate attribute
* values.
*
* @param aCertificate
* The nsIX509Cert to compare to the expected attributes.
* @param aCerts
* An array of JS objects with names / values corresponding to the
* expected certificate's attribute names / values. If this is null or
* an empty array then no checks are performed.
* @throws NS_ERROR_ILLEGAL_VALUE if a certificate attribute name from the
* aCerts param does not exist or the value for a certificate attribute
* from the aCerts param is different than the expected value or
* aCertificate wasn't specified and aCerts is not null or an empty
* array.
*/
function validateCert(aCertificate, aCerts) {
// If there are no certificate requirements then just exit
if (!aCerts || aCerts.length == 0)
return;
if (!aCertificate) {
const missingCertErr = "A required certificate was not present.";
Cu.reportError(missingCertErr);
throw new Ce(missingCertErr, Cr.NS_ERROR_ILLEGAL_VALUE);
}
var errors = [];
for (var i = 0; i < aCerts.length; ++i) {
var error = false;
var certAttrs = aCerts[i];
for (var name in certAttrs) {
if (!(name in aCertificate)) {
error = true;
errors.push("Expected attribute '" + name + "' not present in " +
"certificate.");
break;
}
if (aCertificate[name] != certAttrs[name]) {
error = true;
errors.push("Expected certificate attribute '" + name + "' " +
"value incorrect, expected: '" + certAttrs[name] +
"', got: '" + aCertificate[name] + "'.");
break;
}
}
if (!error)
break;
}
if (error) {
errors.forEach(Cu.reportError);
const certCheckErr = "Certificate checks failed. See previous errors " +
"for details.";
Cu.reportError(certCheckErr);
throw new Ce(certCheckErr, Cr.NS_ERROR_ILLEGAL_VALUE);
}
}
/**
* Checks if the connection must be HTTPS and if so, only allows built-in
* certificates and validates application specified certificate attribute
@ -65,7 +168,7 @@ const Cu = Components.utils;
* @throws NS_ERROR_UNEXPECTED if a certificate is expected and the URI scheme
* is not https.
* NS_ERROR_ILLEGAL_VALUE if a certificate attribute name from the
* cert param does not exist or the value for a certificate attribute
* aCerts param does not exist or the value for a certificate attribute
* from the aCerts param is different than the expected value.
* NS_ERROR_ABORT if the certificate issuer is not built-in.
*/
@ -83,37 +186,7 @@ function checkCert(aChannel, aAllowNonBuiltInCerts, aCerts) {
aChannel.securityInfo.QueryInterface(Ci.nsISSLStatusProvider).
SSLStatus.QueryInterface(Ci.nsISSLStatus).serverCert;
if (aCerts) {
for (var i = 0; i < aCerts.length; ++i) {
var error = false;
var certAttrs = aCerts[i];
for (var name in certAttrs) {
if (!(name in cert)) {
error = true;
Cu.reportError("Expected attribute '" + name + "' not present in " +
"certificate.");
break;
}
if (cert[name] != certAttrs[name]) {
error = true;
Cu.reportError("Expected certificate attribute '" + name + "' " +
"value incorrect, expected: '" + certAttrs[name] +
"', got: '" + cert[name] + "'.");
break;
}
}
if (!error)
break;
}
if (error) {
const certCheckErr = "Certificate checks failed. See previous errors " +
"for details.";
Cu.reportError(certCheckErr);
throw new Ce(certCheckErr, Cr.NS_ERROR_ILLEGAL_VALUE);
}
}
validateCert(cert, aCerts);
if (aAllowNonBuiltInCerts === true)
return;

View File

@ -0,0 +1,97 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/CertUtils.jsm");
const PREF_PREFIX = "certutils.certs.";
function run_test() {
run_next_test();
}
function resetPrefs() {
var prefs = Services.prefs.getChildList(PREF_PREFIX);
prefs.forEach(Services.prefs.clearUserPref);
}
function attributes_match(aCert, aExpected) {
if (Object.keys(aCert).length != Object.keys(aExpected).length)
return false;
for (var attribute in aCert) {
if (!(attribute in aExpected))
return false;
if (aCert[attribute] != aExpected[attribute])
return false;
}
return true;
}
function test_results(aCerts, aExpected) {
do_check_eq(aCerts.length, aExpected.length);
for (var i = 0; i < aCerts.length; i++) {
if (!attributes_match(aCerts[i], aExpected[i])) {
dump("Attributes for certificate " + (i + 1) + " did not match expected attributes\n");
dump("Saw: " + aCerts[i].toSource() + "\n");
dump("Expected: " + aExpected[i].toSource() + "\n");
do_check_true(false);
}
}
}
add_test(function test_singleCert() {
Services.prefs.setCharPref(PREF_PREFIX + "1.attribute1", "foo");
Services.prefs.setCharPref(PREF_PREFIX + "1.attribute2", "bar");
var certs = readCertPrefs(PREF_PREFIX);
test_results(certs, [{
attribute1: "foo",
attribute2: "bar"
}]);
resetPrefs();
run_next_test();
});
add_test(function test_multipleCert() {
Services.prefs.setCharPref(PREF_PREFIX + "1.md5Fingerprint", "cf84a9a2a804e021f27cb5128fe151f4");
Services.prefs.setCharPref(PREF_PREFIX + "1.nickname", "1st cert");
Services.prefs.setCharPref(PREF_PREFIX + "2.md5Fingerprint", "9441051b7eb50e5ca2226095af710c1a");
Services.prefs.setCharPref(PREF_PREFIX + "2.nickname", "2nd cert");
var certs = readCertPrefs(PREF_PREFIX);
test_results(certs, [{
md5Fingerprint: "cf84a9a2a804e021f27cb5128fe151f4",
nickname: "1st cert"
}, {
md5Fingerprint: "9441051b7eb50e5ca2226095af710c1a",
nickname: "2nd cert"
}]);
resetPrefs();
run_next_test();
});
add_test(function test_skippedCert() {
Services.prefs.setCharPref(PREF_PREFIX + "1.issuerName", "Mozilla");
Services.prefs.setCharPref(PREF_PREFIX + "1.nickname", "1st cert");
Services.prefs.setCharPref(PREF_PREFIX + "2.issuerName", "Top CA");
Services.prefs.setCharPref(PREF_PREFIX + "2.nickname", "2nd cert");
Services.prefs.setCharPref(PREF_PREFIX + "4.issuerName", "Unknown CA");
Services.prefs.setCharPref(PREF_PREFIX + "4.nickname", "Ignored cert");
var certs = readCertPrefs(PREF_PREFIX);
test_results(certs, [{
issuerName: "Mozilla",
nickname: "1st cert"
}, {
issuerName: "Top CA",
nickname: "2nd cert"
}]);
resetPrefs();
run_next_test();
});

View File

@ -3,3 +3,5 @@ head =
tail =
[test_FileUtils.js]
[test_readCertPrefs.js]

View File

@ -2394,24 +2394,8 @@ Checker.prototype = {
var prefs = Services.prefs;
var certs = null;
if (!prefs.prefHasUserValue(PREF_APP_UPDATE_URL_OVERRIDE) &&
getPref("getBoolPref", PREF_APP_UPDATE_CERT_CHECKATTRS, true) &&
prefs.getBranch(PREF_APP_UPDATE_CERTS_BRANCH).getChildList("").length) {
certs = [];
let counter = 1;
while (true) {
let prefBranchCert = prefs.getBranch(PREF_APP_UPDATE_CERTS_BRANCH +
counter + ".");
let prefCertAttrs = prefBranchCert.getChildList("");
if (prefCertAttrs.length == 0)
break;
let certAttrs = {};
for each (let prefCertAttr in prefCertAttrs)
certAttrs[prefCertAttr] = prefBranchCert.getCharPref(prefCertAttr);
certs.push(certAttrs);
counter++;
}
getPref("getBoolPref", PREF_APP_UPDATE_CERT_CHECKATTRS, true)) {
certs = gCertUtils.readCertPrefs(PREF_APP_UPDATE_CERTS_BRANCH);
}
try {