forked from mirrors/gecko-dev
Bug 1954934 - Introduce signature verification checkpoint at startup - ESR128 port a=pascalc
Original Revision: https://phabricator.services.mozilla.com/D242193 Differential Revision: https://phabricator.services.mozilla.com/D242505
This commit is contained in:
parent
12ac351bf7
commit
fc8b599bf0
3 changed files with 231 additions and 0 deletions
|
|
@ -118,6 +118,16 @@ const STARTUP_MTIME_SCOPES = [
|
|||
const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions";
|
||||
const XPI_PERMISSION = "install";
|
||||
|
||||
// This preference name is from UpdateTimerManager.sys.mjs; it stores the
|
||||
// timestamp (seconds) of the last periodic signature check.
|
||||
const PREF_LAST_SIGNATURE_CHECK_TIME =
|
||||
"app.update.lastUpdateTime.xpi-signature-verification";
|
||||
const PREF_LAST_SIGNATURE_CHECKPOINT = "extensions.signatureCheckpoint";
|
||||
// SIGNATURE_CHECKPOINT should be incremented whenever an implementation change
|
||||
// happens that affects the validity of add-on signatures of already-installed
|
||||
// add-ons. This forces all add-on signatures to be verified again.
|
||||
const XPI_SIGNATURE_CHECKPOINT = 1;
|
||||
|
||||
const XPI_SIGNATURE_CHECK_PERIOD = 24 * 60 * 60;
|
||||
|
||||
const DB_SCHEMA = 36;
|
||||
|
|
@ -2728,6 +2738,23 @@ export var XPIProvider = {
|
|||
|
||||
AddonManagerPrivate.recordTimestamp("XPI_startup_end");
|
||||
|
||||
if (
|
||||
Services.prefs.getIntPref(PREF_LAST_SIGNATURE_CHECKPOINT, 0) !==
|
||||
XPI_SIGNATURE_CHECKPOINT
|
||||
) {
|
||||
Services.prefs.setIntPref(
|
||||
PREF_LAST_SIGNATURE_CHECKPOINT,
|
||||
XPI_SIGNATURE_CHECKPOINT
|
||||
);
|
||||
if (aAppChanged !== undefined) {
|
||||
XPIExports.XPIDatabase.verifySignatures();
|
||||
|
||||
// Mark timer as fired so that timerManager won't also retrigger the
|
||||
// same validation for the next XPI_SIGNATURE_CHECKPOINT seconds.
|
||||
const NOW_SECS = Math.round(Date.now() / 1000);
|
||||
Services.prefs.setIntPref(PREF_LAST_SIGNATURE_CHECK_TIME, NOW_SECS);
|
||||
}
|
||||
}
|
||||
lazy.timerManager.registerTimer(
|
||||
"xpi-signature-verification",
|
||||
() => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
// Enable signature checks for these tests
|
||||
gUseRealCertChecks = true;
|
||||
ExtensionTestUtils.mockAppInfo();
|
||||
|
||||
// Same as XPIProvider.sys.mjs.
|
||||
const XPI_SIGNATURE_CHECK_PERIOD = 24 * 60 * 60;
|
||||
const PREF_LAST_TIME = "app.update.lastUpdateTime.xpi-signature-verification";
|
||||
const PREF_LAST_SIGNATURE_CHECKPOINT = "extensions.signatureCheckpoint";
|
||||
|
||||
// Add/subtract a bit of time when testing time-related stuff, to avoid
|
||||
// intermittent test failures if the clock somehow skews.
|
||||
const CLOCK_SKEW = 2;
|
||||
|
||||
function nowSecs() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
const gUTM = Cc["@mozilla.org/updates/timer-manager;1"].getService(
|
||||
Ci.nsIUpdateTimerManager
|
||||
);
|
||||
|
||||
function skewTimestampForVisibilityInTest() {
|
||||
// The test repeatedly checks whether the time has been updated (to "now"),
|
||||
// and can only see the difference if there is a difference between the
|
||||
// previous timestamp and "now". Therefore we tweak the timestamp.
|
||||
let time = Services.prefs.getIntPref(PREF_LAST_TIME);
|
||||
time -= 1;
|
||||
Services.prefs.setIntPref(PREF_LAST_TIME, time);
|
||||
return time;
|
||||
}
|
||||
|
||||
add_setup(async () => {
|
||||
// Enable logging of UpdateTimerManager.sys.mjs for easier debugging.
|
||||
Services.prefs.setBoolPref("app.update.log", true);
|
||||
|
||||
// Pretend that every timer has already fired very recently. Otherwise all
|
||||
// other random kinds of activities are triggered on utm-test-init, while we
|
||||
// are only interested in the xpi-signature-verification timer.
|
||||
for (const { value } of Services.catMan.enumerateCategory("update-timer")) {
|
||||
const timerID = value.split(",")[2];
|
||||
Services.prefs.setIntPref(
|
||||
`app.update.lastUpdateTime.${timerID}`,
|
||||
nowSecs() - CLOCK_SKEW
|
||||
);
|
||||
}
|
||||
|
||||
// The internal repeating timer is fired as frequently as the most frequently
|
||||
// scheduled timer. Set a 1 second timer so that the test waits for at most
|
||||
// 1 second for a timer to fire, instead of e.g. 30 seconds.
|
||||
Services.prefs.setIntPref("app.update.timerMinimumDelay", 1);
|
||||
Services.prefs.setIntPref("app.update.timerFirstInterval", 1000);
|
||||
gUTM.registerTimer("test-dummy-ensure-quick-progress", () => {}, 1);
|
||||
|
||||
AddonTestUtils.on("addon-manager-shutdown", () => {
|
||||
// XPIProvider registers a timer at its startup(). That timer is never
|
||||
// unregistered anywhere, so we do it here.
|
||||
gUTM.unregisterTimer("xpi-signature-verification");
|
||||
});
|
||||
|
||||
// Now initialize the timer, so that the timer registered by XPIProvider's
|
||||
// startup() will actually do something.
|
||||
gUTM.QueryInterface(Ci.nsIObserver).observe(null, "utm-test-init", "");
|
||||
});
|
||||
|
||||
add_task(useAMOStageCert(), async function test_scheduled_signature_checks() {
|
||||
// This test checks whether signature verification is scheduled when expected,
|
||||
// where the following states are relevant:
|
||||
// - Whether this is a new or existing profile.
|
||||
// - Value of PREF_LAST_TIME.
|
||||
// - Value of PREF_LAST_SIGNATURE_CHECKPOINT.
|
||||
// - Signed state of add-on.
|
||||
// - Value of xpinstall.signatures.dev-root pref (to simulate invalid cert).
|
||||
//
|
||||
// There are two potential triggers of signature check:
|
||||
// - Previous check was too long ago (according to PREF_LAST_TIME).
|
||||
// - Browser "updated" bumped checkpoint (PREF_LAST_SIGNATURE_CHECKPOINT).
|
||||
//
|
||||
// Each significant state transition in this test starts with:
|
||||
// info("Simulating ... ");
|
||||
|
||||
// Pretend that we have already ran the update timer relatively recently,
|
||||
// so that the timer does not fire unexpectedly for unrelated reasons.
|
||||
const FIRST_RUN_VERIFICATION_TIME = nowSecs() - CLOCK_SKEW;
|
||||
Services.prefs.setIntPref(PREF_LAST_TIME, FIRST_RUN_VERIFICATION_TIME);
|
||||
|
||||
info("Simulating first run with new profile");
|
||||
Assert.ok(
|
||||
!Services.prefs.prefHasUserValue(PREF_LAST_SIGNATURE_CHECKPOINT),
|
||||
"Checkpoint not set before first run"
|
||||
);
|
||||
|
||||
await promiseStartupManager();
|
||||
|
||||
Assert.ok(
|
||||
Services.prefs.prefHasUserValue(PREF_LAST_SIGNATURE_CHECKPOINT),
|
||||
"Checkpoint set after first run"
|
||||
);
|
||||
|
||||
Assert.equal(
|
||||
Services.prefs.getIntPref(PREF_LAST_TIME),
|
||||
FIRST_RUN_VERIFICATION_TIME,
|
||||
"On startup of new profile, checkpoint is set without forced verification"
|
||||
);
|
||||
|
||||
await promiseInstallFile(do_get_file("data/signing_checks/signed1.xpi"));
|
||||
async function getSignedState() {
|
||||
// The addon ID is of signed1.xpi.
|
||||
let addon = await promiseAddonByID("test@somewhere.com");
|
||||
return addon.signedState;
|
||||
}
|
||||
Assert.equal(
|
||||
await getSignedState(),
|
||||
AddonManager.SIGNEDSTATE_SIGNED,
|
||||
"Signature correct after install"
|
||||
);
|
||||
|
||||
await promiseRestartManager();
|
||||
Assert.equal(
|
||||
Services.prefs.getIntPref(PREF_LAST_TIME),
|
||||
FIRST_RUN_VERIFICATION_TIME,
|
||||
"Signature verification not scheduled yet"
|
||||
);
|
||||
|
||||
info("Simulating that the add-on should be considered signed incorrectly");
|
||||
// Toggle the root cert, which would invalidate the signature.
|
||||
Services.prefs.setBoolPref("xpinstall.signatures.dev-root", false);
|
||||
|
||||
await promiseRestartManager();
|
||||
Assert.equal(
|
||||
Services.prefs.getIntPref(PREF_LAST_TIME),
|
||||
FIRST_RUN_VERIFICATION_TIME,
|
||||
"Signature verification not scheduled yet"
|
||||
);
|
||||
Assert.equal(
|
||||
await getSignedState(),
|
||||
AddonManager.SIGNEDSTATE_SIGNED,
|
||||
"Signature still correct despite different root (not verified yet)"
|
||||
);
|
||||
|
||||
await promiseShutdownManager();
|
||||
|
||||
info("Simulating that update check has happened too long ago");
|
||||
const TOO_LONG_AGO_TIME = nowSecs() - XPI_SIGNATURE_CHECK_PERIOD - CLOCK_SKEW;
|
||||
Services.prefs.setIntPref(PREF_LAST_TIME, TOO_LONG_AGO_TIME);
|
||||
|
||||
let verifiedPromise = TestUtils.topicObserved("xpi-signature-changed");
|
||||
await promiseStartupManager();
|
||||
Assert.notEqual(
|
||||
Services.prefs.getIntPref(PREF_LAST_TIME),
|
||||
FIRST_RUN_VERIFICATION_TIME,
|
||||
"Signature verification happens when the last check was too long ago"
|
||||
);
|
||||
await verifiedPromise;
|
||||
Assert.equal(
|
||||
await getSignedState(),
|
||||
AddonManager.SIGNEDSTATE_UNKNOWN,
|
||||
"Detected signature invalidation (due to bad root) after verification"
|
||||
);
|
||||
|
||||
const LAST_TIMER_TRIGGERED_TIME = skewTimestampForVisibilityInTest();
|
||||
|
||||
info("Simulating that the add-on should be considered signed correctly");
|
||||
// Toggle the root cert, which makes the signature valid again.
|
||||
Services.prefs.setBoolPref("xpinstall.signatures.dev-root", true);
|
||||
await promiseRestartManager();
|
||||
Assert.equal(
|
||||
Services.prefs.getIntPref(PREF_LAST_TIME),
|
||||
LAST_TIMER_TRIGGERED_TIME,
|
||||
"Signature verification skipped because it happened recently"
|
||||
);
|
||||
Assert.equal(
|
||||
await getSignedState(),
|
||||
AddonManager.SIGNEDSTATE_UNKNOWN,
|
||||
"Signature out of date because verification was skipped"
|
||||
);
|
||||
await promiseShutdownManager();
|
||||
|
||||
// Regression test for https://bugzilla.mozilla.org/show_bug.cgi?id=1954934
|
||||
// When the browser updates with an existing profile, and the checkpoint
|
||||
// differs, then signature verification must be forced.
|
||||
info("Simulating browser update with forced signature change");
|
||||
Services.prefs.clearUserPref(PREF_LAST_SIGNATURE_CHECKPOINT);
|
||||
let verifiedPromise2 = TestUtils.topicObserved("xpi-signature-changed");
|
||||
await promiseStartupManager();
|
||||
Assert.notEqual(
|
||||
Services.prefs.getIntPref(PREF_LAST_TIME),
|
||||
LAST_TIMER_TRIGGERED_TIME,
|
||||
"Signature verification timer should be reset when verification is forced"
|
||||
);
|
||||
await verifiedPromise2;
|
||||
Assert.equal(
|
||||
await getSignedState(),
|
||||
AddonManager.SIGNEDSTATE_SIGNED,
|
||||
"Signature is up to date again after forced verification on startup"
|
||||
);
|
||||
|
||||
await promiseShutdownManager();
|
||||
});
|
||||
|
|
@ -316,6 +316,8 @@ skip-if = [
|
|||
|
||||
["test_signed_verify.js"]
|
||||
|
||||
["test_signed_verify_timer.js"]
|
||||
|
||||
["test_sitePermsAddonProvider.js"]
|
||||
skip-if = [
|
||||
"appname == 'thunderbird'", # Disabled in extensions.manifest
|
||||
|
|
|
|||
Loading…
Reference in a new issue