зеркало из https://github.com/mozilla/gecko-dev.git
816 строки
25 KiB
JavaScript
816 строки
25 KiB
JavaScript
/* import-globals-from ../../../common/tests/unit/head_helpers.js */
|
|
"use strict";
|
|
|
|
const PREF_SETTINGS_SERVER = "services.settings.server";
|
|
const SIGNER_NAME = "onecrl.content-signature.mozilla.org";
|
|
const TELEMETRY_COMPONENT = "remotesettings";
|
|
|
|
const CERT_DIR = "test_remote_settings_signatures/";
|
|
const CHAIN_FILES = ["collection_signing_ee.pem", "collection_signing_int.pem"];
|
|
|
|
function getFileData(file) {
|
|
const stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
|
|
Ci.nsIFileInputStream
|
|
);
|
|
stream.init(file, -1, 0, 0);
|
|
const data = NetUtil.readInputStreamToString(stream, stream.available());
|
|
stream.close();
|
|
return data;
|
|
}
|
|
|
|
function getCertChain() {
|
|
const chain = [];
|
|
for (let file of CHAIN_FILES) {
|
|
chain.push(getFileData(do_get_file(CERT_DIR + file)));
|
|
}
|
|
return chain.join("\n");
|
|
}
|
|
|
|
let server;
|
|
let client;
|
|
|
|
add_setup(() => {
|
|
// Signature verification is enabled by default. We use a custom signer
|
|
// because these tests were originally written for OneCRL.
|
|
client = RemoteSettings("signed", { signerName: SIGNER_NAME });
|
|
|
|
Services.prefs.setStringPref("services.settings.loglevel", "debug");
|
|
|
|
// Set up an HTTP Server
|
|
server = new HttpServer();
|
|
server.start(-1);
|
|
|
|
// Pretend we are in nightly channel to make sure all telemetry events are sent.
|
|
let oldGetChannel = Policy.getChannel;
|
|
Policy.getChannel = () => "nightly";
|
|
|
|
registerCleanupFunction(() => {
|
|
Policy.getChannel = oldGetChannel;
|
|
server.stop(() => {});
|
|
});
|
|
});
|
|
|
|
add_task(async function test_check_signatures() {
|
|
// First, perform a signature verification with known data and signature
|
|
// to ensure things are working correctly
|
|
let verifier = Cc[
|
|
"@mozilla.org/security/contentsignatureverifier;1"
|
|
].createInstance(Ci.nsIContentSignatureVerifier);
|
|
|
|
const emptyData = "[]";
|
|
const emptySignature =
|
|
"p384ecdsa=zbugm2FDitsHwk5-IWsas1PpWwY29f0Fg5ZHeqD8fzep7AVl2vfcaHA7LdmCZ28qZLOioGKvco3qT117Q4-HlqFTJM7COHzxGyU2MMJ0ZTnhJrPOC1fP3cVQjU1PTWi9";
|
|
|
|
ok(
|
|
await verifier.asyncVerifyContentSignature(
|
|
emptyData,
|
|
emptySignature,
|
|
getCertChain(),
|
|
SIGNER_NAME,
|
|
Ci.nsIX509CertDB.AppXPCShellRoot
|
|
)
|
|
);
|
|
|
|
const collectionData =
|
|
'[{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:43:37Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"97fbf7c4-3ef2-f54f-0029-1ba6540c63ea","issuerName":"MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==","last_modified":2000,"serialNumber":"BAAAAAABA/A35EU="},{"details":{"bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1155145","created":"2016-01-18T14:48:11Z","name":"GlobalSign certs","who":".","why":"."},"enabled":true,"id":"e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc","issuerName":"MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB","last_modified":3000,"serialNumber":"BAAAAAABI54PryQ="}]';
|
|
const collectionSignature =
|
|
"p384ecdsa=f4pA2tYM5jQgWY6YUmhUwQiBLj6QO5sHLD_5MqLePz95qv-7cNCuQoZnPQwxoptDtW8hcWH3kLb0quR7SB-r82gkpR9POVofsnWJRA-ETb0BcIz6VvI3pDT49ZLlNg3p";
|
|
|
|
ok(
|
|
await verifier.asyncVerifyContentSignature(
|
|
collectionData,
|
|
collectionSignature,
|
|
getCertChain(),
|
|
SIGNER_NAME,
|
|
Ci.nsIX509CertDB.AppXPCShellRoot
|
|
)
|
|
);
|
|
});
|
|
|
|
add_task(async function test_check_synchronization_with_signatures() {
|
|
const port = server.identity.primaryPort;
|
|
|
|
const x5u = `http://localhost:${port}/test_remote_settings_signatures/test_cert_chain.pem`;
|
|
|
|
// Telemetry reports.
|
|
const TELEMETRY_SOURCE = client.identifier;
|
|
|
|
function registerHandlers(responses) {
|
|
function handleResponse(serverTimeMillis, request, response) {
|
|
const key = `${request.method}:${request.path}?${request.queryString}`;
|
|
const available = responses[key];
|
|
const sampled = available.length > 1 ? available.shift() : available[0];
|
|
if (!sampled) {
|
|
do_throw(
|
|
`unexpected ${request.method} request for ${request.path}?${request.queryString}`
|
|
);
|
|
}
|
|
|
|
response.setStatusLine(
|
|
null,
|
|
sampled.status.status,
|
|
sampled.status.statusText
|
|
);
|
|
// send the headers
|
|
for (let headerLine of sampled.sampleHeaders) {
|
|
let headerElements = headerLine.split(":");
|
|
response.setHeader(headerElements[0], headerElements[1].trimLeft());
|
|
}
|
|
|
|
// set the server date
|
|
response.setHeader("Date", new Date(serverTimeMillis).toUTCString());
|
|
|
|
response.write(sampled.responseBody);
|
|
}
|
|
|
|
for (let key of Object.keys(responses)) {
|
|
const keyParts = key.split(":");
|
|
const valueParts = keyParts[1].split("?");
|
|
const path = valueParts[0];
|
|
|
|
server.registerPathHandler(path, handleResponse.bind(null, 2000));
|
|
}
|
|
}
|
|
|
|
// set up prefs so the kinto updater talks to the test server
|
|
Services.prefs.setStringPref(
|
|
PREF_SETTINGS_SERVER,
|
|
`http://localhost:${server.identity.primaryPort}/v1`
|
|
);
|
|
|
|
// These are records we'll use in the test collections
|
|
const RECORD1 = {
|
|
details: {
|
|
bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
|
|
created: "2016-01-18T14:43:37Z",
|
|
name: "GlobalSign certs",
|
|
who: ".",
|
|
why: ".",
|
|
},
|
|
enabled: true,
|
|
id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea",
|
|
issuerName:
|
|
"MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==",
|
|
last_modified: 2000,
|
|
serialNumber: "BAAAAAABA/A35EU=",
|
|
};
|
|
|
|
const RECORD2 = {
|
|
details: {
|
|
bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
|
|
created: "2016-01-18T14:48:11Z",
|
|
name: "GlobalSign certs",
|
|
who: ".",
|
|
why: ".",
|
|
},
|
|
enabled: true,
|
|
id: "e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc",
|
|
issuerName:
|
|
"MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB",
|
|
last_modified: 3000,
|
|
serialNumber: "BAAAAAABI54PryQ=",
|
|
};
|
|
|
|
const RECORD3 = {
|
|
details: {
|
|
bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
|
|
created: "2016-01-18T14:48:11Z",
|
|
name: "GlobalSign certs",
|
|
who: ".",
|
|
why: ".",
|
|
},
|
|
enabled: true,
|
|
id: "c7c49b69-a4ab-418e-92a9-e1961459aa7f",
|
|
issuerName:
|
|
"MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB",
|
|
last_modified: 4000,
|
|
serialNumber: "BAAAAAABI54PryQ=",
|
|
};
|
|
|
|
const RECORD1_DELETION = {
|
|
deleted: true,
|
|
enabled: true,
|
|
id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea",
|
|
last_modified: 3500,
|
|
};
|
|
|
|
// Check that a signature on an empty collection is OK
|
|
// We need to set up paths on the HTTP server to return specific data from
|
|
// specific paths for each test. Here we prepare data for each response.
|
|
|
|
// A cert chain response (this the cert chain that contains the signing
|
|
// cert, the root and any intermediates in between). This is used in each
|
|
// sync.
|
|
const RESPONSE_CERT_CHAIN = {
|
|
comment: "RESPONSE_CERT_CHAIN",
|
|
sampleHeaders: ["Content-Type: text/plain; charset=UTF-8"],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: getCertChain(),
|
|
};
|
|
|
|
// A server settings response. This is used in each sync.
|
|
const RESPONSE_SERVER_SETTINGS = {
|
|
comment: "RESPONSE_SERVER_SETTINGS",
|
|
sampleHeaders: [
|
|
"Access-Control-Allow-Origin: *",
|
|
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
"Server: waitress",
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: JSON.stringify({
|
|
settings: {
|
|
batch_max_requests: 25,
|
|
},
|
|
url: `http://localhost:${port}/v1/`,
|
|
documentation: "https://kinto.readthedocs.org/",
|
|
version: "1.5.1",
|
|
commit: "cbc6f58",
|
|
hello: "kinto",
|
|
}),
|
|
};
|
|
|
|
// This is the initial, empty state of the collection. This is only used
|
|
// for the first sync.
|
|
const RESPONSE_EMPTY_INITIAL = {
|
|
comment: "RESPONSE_EMPTY_INITIAL",
|
|
sampleHeaders: [
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
'ETag: "1000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: JSON.stringify({
|
|
timestamp: 1000,
|
|
metadata: {
|
|
signature: {
|
|
x5u,
|
|
signature:
|
|
"vxuAg5rDCB-1pul4a91vqSBQRXJG_j7WOYUTswxRSMltdYmbhLRH8R8brQ9YKuNDF56F-w6pn4HWxb076qgKPwgcEBtUeZAO_RtaHXRkRUUgVzAr86yQL4-aJTbv3D6u",
|
|
},
|
|
},
|
|
changes: [],
|
|
}),
|
|
};
|
|
|
|
// Here, we map request method and path to the available responses
|
|
const emptyCollectionResponses = {
|
|
"GET:/test_remote_settings_signatures/test_cert_chain.pem?": [
|
|
RESPONSE_CERT_CHAIN,
|
|
],
|
|
"GET:/v1/?": [RESPONSE_SERVER_SETTINGS],
|
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=1000": [
|
|
RESPONSE_EMPTY_INITIAL,
|
|
],
|
|
};
|
|
|
|
//
|
|
// 1.
|
|
// - collection: undefined -> []
|
|
// - timestamp: undefined -> 1000
|
|
//
|
|
|
|
// .. and use this map to register handlers for each path
|
|
registerHandlers(emptyCollectionResponses);
|
|
|
|
let startSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
TELEMETRY_SOURCE
|
|
);
|
|
|
|
// With all of this set up, we attempt a sync. This will resolve if all is
|
|
// well and throw if something goes wrong.
|
|
await client.maybeSync(1000);
|
|
|
|
equal((await client.get()).length, 0);
|
|
|
|
let endSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
TELEMETRY_SOURCE
|
|
);
|
|
|
|
// ensure that a success histogram is tracked when a succesful sync occurs.
|
|
let expectedIncrements = { [UptakeTelemetry.STATUS.SUCCESS]: 1 };
|
|
checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
|
|
|
|
//
|
|
// 2.
|
|
// - collection: [] -> [RECORD2, RECORD1]
|
|
// - timestamp: 1000 -> 3000
|
|
//
|
|
// Check that some additions (2 records) to the collection have a valid
|
|
// signature.
|
|
|
|
// This response adds two entries (RECORD1 and RECORD2) to the collection
|
|
const RESPONSE_TWO_ADDED = {
|
|
comment: "RESPONSE_TWO_ADDED",
|
|
sampleHeaders: [
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
'ETag: "3000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: JSON.stringify({
|
|
timestamp: 3000,
|
|
metadata: {
|
|
signature: {
|
|
x5u,
|
|
signature:
|
|
"dwhJeypadNIyzGj3QdI0KMRTPnHhFPF_j73mNrsPAHKMW46S2Ftf4BzsPMvPMB8h0TjDus13wo_R4l432DHe7tYyMIWXY0PBeMcoe5BREhFIxMxTsh9eGVXBD1e3UwRy",
|
|
},
|
|
},
|
|
changes: [RECORD2, RECORD1],
|
|
}),
|
|
};
|
|
|
|
const twoItemsResponses = {
|
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=3000&_since=%221000%22":
|
|
[RESPONSE_TWO_ADDED],
|
|
};
|
|
registerHandlers(twoItemsResponses);
|
|
await client.maybeSync(3000);
|
|
|
|
equal((await client.get()).length, 2);
|
|
|
|
//
|
|
// 3.
|
|
// - collection: [RECORD2, RECORD1] -> [RECORD2, RECORD3]
|
|
// - timestamp: 3000 -> 4000
|
|
//
|
|
// Check the collection with one addition and one removal has a valid
|
|
// signature
|
|
const THREE_ITEMS_SIG =
|
|
"MIEmNghKnkz12UodAAIc3q_Y4a3IJJ7GhHF4JYNYmm8avAGyPM9fYU7NzVo94pzjotG7vmtiYuHyIX2rTHTbT587w0LdRWxipgFd_PC1mHiwUyjFYNqBBG-kifYk7kEw";
|
|
|
|
// Remove RECORD1, add RECORD3
|
|
const RESPONSE_ONE_ADDED_ONE_REMOVED = {
|
|
comment: "RESPONSE_ONE_ADDED_ONE_REMOVED ",
|
|
sampleHeaders: [
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
'ETag: "4000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: JSON.stringify({
|
|
timestamp: 4000,
|
|
metadata: {
|
|
signature: {
|
|
x5u,
|
|
signature: THREE_ITEMS_SIG,
|
|
},
|
|
},
|
|
changes: [RECORD3, RECORD1_DELETION],
|
|
}),
|
|
};
|
|
|
|
const oneAddedOneRemovedResponses = {
|
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=4000&_since=%223000%22":
|
|
[RESPONSE_ONE_ADDED_ONE_REMOVED],
|
|
};
|
|
registerHandlers(oneAddedOneRemovedResponses);
|
|
await client.maybeSync(4000);
|
|
|
|
equal((await client.get()).length, 2);
|
|
|
|
//
|
|
// 4.
|
|
// - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
|
|
// - timestamp: 4000 -> 4100
|
|
//
|
|
// Check the signature is still valid with no operation (no changes)
|
|
|
|
// Leave the collection unchanged
|
|
const RESPONSE_EMPTY_NO_UPDATE = {
|
|
comment: "RESPONSE_EMPTY_NO_UPDATE ",
|
|
sampleHeaders: [
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
'ETag: "4000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: JSON.stringify({
|
|
timestamp: 4000,
|
|
metadata: {
|
|
signature: {
|
|
x5u,
|
|
signature: THREE_ITEMS_SIG,
|
|
},
|
|
},
|
|
changes: [],
|
|
}),
|
|
};
|
|
|
|
const noOpResponses = {
|
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=4100&_since=%224000%22":
|
|
[RESPONSE_EMPTY_NO_UPDATE],
|
|
};
|
|
registerHandlers(noOpResponses);
|
|
await client.maybeSync(4100);
|
|
|
|
equal((await client.get()).length, 2);
|
|
|
|
console.info("---------------------------------------------------------");
|
|
//
|
|
// 5.
|
|
// - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
|
|
// - timestamp: 4000 -> 5000
|
|
//
|
|
// Check the collection is reset when the signature is invalid.
|
|
// Client will:
|
|
// - Fetch metadata (with bad signature)
|
|
// - Perform the sync (fetch empty changes)
|
|
// - Refetch the metadata and the whole collection
|
|
// - Validate signature successfully, but with no changes to emit.
|
|
|
|
const RESPONSE_COMPLETE_INITIAL = {
|
|
comment: "RESPONSE_COMPLETE_INITIAL ",
|
|
sampleHeaders: [
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
'ETag: "4000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: JSON.stringify({
|
|
timestamp: 4000,
|
|
metadata: {
|
|
signature: {
|
|
x5u,
|
|
signature: THREE_ITEMS_SIG,
|
|
},
|
|
},
|
|
changes: [RECORD2, RECORD3],
|
|
}),
|
|
};
|
|
|
|
const RESPONSE_EMPTY_NO_UPDATE_BAD_SIG = {
|
|
...RESPONSE_EMPTY_NO_UPDATE,
|
|
responseBody: JSON.stringify({
|
|
timestamp: 4000,
|
|
metadata: {
|
|
signature: {
|
|
x5u,
|
|
signature: "aW52YWxpZCBzaWduYXR1cmUK",
|
|
},
|
|
},
|
|
changes: [],
|
|
}),
|
|
};
|
|
|
|
const badSigGoodSigResponses = {
|
|
// The first collection state is the three item collection (since
|
|
// there was sync with no updates before) - but, since the signature is wrong,
|
|
// another request will be made...
|
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=%224000%22":
|
|
[RESPONSE_EMPTY_NO_UPDATE_BAD_SIG],
|
|
// Subsequent signature returned is a valid one for the three item
|
|
// collection.
|
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [
|
|
RESPONSE_COMPLETE_INITIAL,
|
|
],
|
|
};
|
|
|
|
registerHandlers(badSigGoodSigResponses);
|
|
|
|
startSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
TELEMETRY_SOURCE
|
|
);
|
|
|
|
let syncEventSent = false;
|
|
client.on("sync", () => {
|
|
syncEventSent = true;
|
|
});
|
|
|
|
await client.maybeSync(5000);
|
|
|
|
equal((await client.get()).length, 2);
|
|
|
|
endSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
TELEMETRY_SOURCE
|
|
);
|
|
|
|
// since we only fixed the signature, and no data was changed, the sync event
|
|
// was not sent.
|
|
equal(syncEventSent, false);
|
|
|
|
// ensure that the failure count is incremented for a succesful sync with an
|
|
// (initial) bad signature - only SERVICES_SETTINGS_SYNC_SIG_FAIL should
|
|
// increment.
|
|
expectedIncrements = { [UptakeTelemetry.STATUS.SIGNATURE_ERROR]: 1 };
|
|
checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
|
|
|
|
//
|
|
// 6.
|
|
// - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
|
|
// - timestamp: 4000 -> 5000
|
|
//
|
|
// Check the collection is reset when the signature is invalid.
|
|
// Client will:
|
|
// - Fetch metadata (with bad signature)
|
|
// - Perform the sync (fetch empty changes)
|
|
// - Refetch the whole collection and metadata
|
|
// - Sync will be no-op since local is equal to server, no changes to emit.
|
|
|
|
const badSigGoodOldResponses = {
|
|
// The first collection state is the current state (since there's no update
|
|
// - but, since the signature is wrong, another request will be made)
|
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=%224000%22":
|
|
[RESPONSE_EMPTY_NO_UPDATE_BAD_SIG],
|
|
// The next request is for the full collection. This will be
|
|
// checked against the valid signature and last_modified times will be
|
|
// compared. Sync should be a no-op, even though the signature is good,
|
|
// because the local collection is newer.
|
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [
|
|
RESPONSE_EMPTY_INITIAL,
|
|
],
|
|
};
|
|
|
|
// ensure our collection hasn't been replaced with an older, empty one
|
|
equal((await client.get()).length, 2, "collection was restored");
|
|
|
|
registerHandlers(badSigGoodOldResponses);
|
|
|
|
syncEventSent = false;
|
|
client.on("sync", () => {
|
|
syncEventSent = true;
|
|
});
|
|
|
|
await client.maybeSync(5000);
|
|
|
|
// Local data was unchanged, since it was never than the one returned by the server,
|
|
// thus the sync event is not sent.
|
|
equal(syncEventSent, false, "event was not sent");
|
|
|
|
//
|
|
// 7.
|
|
// - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
|
|
// - timestamp: 4000 -> 5000
|
|
//
|
|
// Check that a tampered local DB will be overwritten and
|
|
// sync event contain the appropriate data.
|
|
|
|
const RESPONSE_COMPLETE_BAD_SIG = {
|
|
...RESPONSE_EMPTY_NO_UPDATE,
|
|
responseBody: JSON.stringify({
|
|
timestamp: 5000,
|
|
metadata: {
|
|
signature: {
|
|
x5u,
|
|
signature: "aW52YWxpZCBzaWduYXR1cmUK",
|
|
},
|
|
},
|
|
changes: [RECORD2, RECORD3],
|
|
}),
|
|
};
|
|
|
|
const badLocalContentGoodSigResponses = {
|
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [
|
|
RESPONSE_COMPLETE_BAD_SIG,
|
|
RESPONSE_COMPLETE_INITIAL,
|
|
],
|
|
};
|
|
|
|
registerHandlers(badLocalContentGoodSigResponses);
|
|
|
|
// we create a local state manually here, in order to test that the sync event data
|
|
// properly contains created, updated, and deleted records.
|
|
// the local DB contains same id as RECORD2 and a fake record.
|
|
// the final server collection contains RECORD2 and RECORD3
|
|
const localId = "0602b1b2-12ab-4d3a-b6fb-593244e7b035";
|
|
await client.db.importChanges(
|
|
{ signature: { x5u, signature: "abc" } },
|
|
null,
|
|
[
|
|
{ ...RECORD2, last_modified: 1234567890, serialNumber: "abc" },
|
|
{ id: localId },
|
|
],
|
|
{
|
|
clear: true,
|
|
}
|
|
);
|
|
|
|
let syncData = null;
|
|
client.on("sync", ({ data }) => {
|
|
syncData = data;
|
|
});
|
|
|
|
// Clear events snapshot.
|
|
TelemetryTestUtils.assertEvents([], {}, { process: "dummy" });
|
|
|
|
const TELEMETRY_EVENTS_FILTERS = {
|
|
category: "uptake.remotecontent.result",
|
|
method: "uptake",
|
|
};
|
|
|
|
// Events telemetry is sampled on released, use fake channel.
|
|
await client.maybeSync(5000);
|
|
|
|
// We should report a corruption_error.
|
|
TelemetryTestUtils.assertEvents(
|
|
[
|
|
[
|
|
"uptake.remotecontent.result",
|
|
"uptake",
|
|
"remotesettings",
|
|
UptakeTelemetry.STATUS.CORRUPTION_ERROR,
|
|
{
|
|
source: client.identifier,
|
|
duration: v => v > 0,
|
|
trigger: "manual",
|
|
},
|
|
],
|
|
],
|
|
TELEMETRY_EVENTS_FILTERS
|
|
);
|
|
|
|
// The local data was corrupted, and the Telemetry status reflects it.
|
|
// But the sync overwrote the bad data and was eventually a success.
|
|
// Since local data was replaced, we use records IDs to determine
|
|
// what was created and deleted. And bad local data will appear
|
|
// in the sync event as deleted.
|
|
equal(syncData.current.length, 2);
|
|
equal(syncData.created.length, 1);
|
|
equal(syncData.created[0].id, RECORD3.id);
|
|
equal(syncData.updated.length, 1);
|
|
equal(syncData.updated[0].old.serialNumber, "abc");
|
|
equal(syncData.updated[0].new.serialNumber, RECORD2.serialNumber);
|
|
equal(syncData.deleted.length, 1);
|
|
equal(syncData.deleted[0].id, localId);
|
|
|
|
//
|
|
// 8.
|
|
// - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3] (unchanged because of error)
|
|
// - timestamp: 4000 -> 6000
|
|
//
|
|
// Check that a failing signature throws after retry, and that sync changes
|
|
// are not applied.
|
|
|
|
const RESPONSE_ONLY_RECORD4_BAD_SIG = {
|
|
comment: "Create RECORD4",
|
|
sampleHeaders: [
|
|
"Content-Type: application/json; charset=UTF-8",
|
|
'ETag: "6000"',
|
|
],
|
|
status: { status: 200, statusText: "OK" },
|
|
responseBody: JSON.stringify({
|
|
timestamp: 6000,
|
|
metadata: {
|
|
signature: {
|
|
x5u,
|
|
signature: "aaaaaaaaaaaaaaaaaaaaaaaa", // sig verifier wants proper length or will crash.
|
|
},
|
|
},
|
|
changes: [
|
|
{
|
|
id: "f765df30-b2f1-42f6-9803-7bd5a07b5098",
|
|
last_modified: 6000,
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
const RESPONSE_EMPTY_NO_UPDATE_BAD_SIG_6000 = {
|
|
...RESPONSE_EMPTY_NO_UPDATE,
|
|
responseBody: JSON.stringify({
|
|
timestamp: 6000,
|
|
metadata: {
|
|
signature: {
|
|
x5u,
|
|
signature: "aW52YWxpZCBzaWduYXR1cmUK",
|
|
},
|
|
},
|
|
changes: [],
|
|
}),
|
|
};
|
|
const allBadSigResponses = {
|
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=6000&_since=%224000%22":
|
|
[RESPONSE_EMPTY_NO_UPDATE_BAD_SIG_6000],
|
|
"GET:/v1/buckets/main/collections/signed/changeset?_expected=6000": [
|
|
RESPONSE_ONLY_RECORD4_BAD_SIG,
|
|
],
|
|
};
|
|
|
|
startSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
TELEMETRY_SOURCE
|
|
);
|
|
registerHandlers(allBadSigResponses);
|
|
await Assert.rejects(
|
|
client.maybeSync(6000),
|
|
RemoteSettingsClient.InvalidSignatureError,
|
|
"Sync failed as expected (bad signature after retry)"
|
|
);
|
|
|
|
// Ensure that the failure is reflected in the accumulated telemetry:
|
|
endSnapshot = getUptakeTelemetrySnapshot(
|
|
TELEMETRY_COMPONENT,
|
|
TELEMETRY_SOURCE
|
|
);
|
|
expectedIncrements = { [UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR]: 1 };
|
|
checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
|
|
|
|
// When signature fails after retry, the local data present before sync
|
|
// should be maintained (if its signature is valid).
|
|
ok(
|
|
arrayEqual(
|
|
(await client.get()).map(r => r.id),
|
|
[RECORD3.id, RECORD2.id]
|
|
),
|
|
"Local records were not changed"
|
|
);
|
|
// And local data should still be valid.
|
|
await client.get({ verifySignature: true }); // Not raising.
|
|
|
|
//
|
|
// 9.
|
|
// - collection: [RECORD2, RECORD3] -> [] (cleared)
|
|
// - timestamp: 4000 -> 6000
|
|
//
|
|
// Check that local data is cleared during sync if signature is not valid.
|
|
|
|
await client.db.create({
|
|
id: "c6b19c67-2e0e-4a82-b7f7-1777b05f3e81",
|
|
last_modified: 42,
|
|
tampered: true,
|
|
});
|
|
|
|
await Assert.rejects(
|
|
client.maybeSync(6000),
|
|
RemoteSettingsClient.InvalidSignatureError,
|
|
"Sync failed as expected (bad signature after retry)"
|
|
);
|
|
|
|
// Since local data was tampered, it was cleared.
|
|
equal((await client.get()).length, 0, "Local database is now empty.");
|
|
|
|
//
|
|
// 10.
|
|
// - collection: [RECORD2, RECORD3] -> [] (cleared)
|
|
// - timestamp: 4000 -> 6000
|
|
//
|
|
// Check that local data is cleared during sync if signature is not valid.
|
|
|
|
await client.db.create({
|
|
id: "c6b19c67-2e0e-4a82-b7f7-1777b05f3e81",
|
|
last_modified: 42,
|
|
tampered: true,
|
|
});
|
|
|
|
await Assert.rejects(
|
|
client.maybeSync(6000),
|
|
RemoteSettingsClient.InvalidSignatureError,
|
|
"Sync failed as expected (bad signature after retry)"
|
|
);
|
|
// Since local data was tampered, it was cleared.
|
|
equal((await client.get()).length, 0, "Local database is now empty.");
|
|
|
|
//
|
|
// 11.
|
|
// - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
|
|
// - timestamp: 4000 -> 6000
|
|
//
|
|
// Check that local data is restored if signature was valid before sync.
|
|
const sigCalls = [];
|
|
let i = 0;
|
|
client._verifier = {
|
|
async asyncVerifyContentSignature(serialized) {
|
|
sigCalls.push(serialized);
|
|
console.log(`verify call ${i}`);
|
|
return [
|
|
false, // After importing changes.
|
|
true, // When checking previous local data.
|
|
false, // Still fail after retry.
|
|
true, // When checking previous local data again.
|
|
][i++];
|
|
},
|
|
};
|
|
// Create an extra record. It will have a valid signature locally
|
|
// thanks to the verifier mock.
|
|
await client.db.importChanges(
|
|
{
|
|
signature: { x5u, signature: "aa" },
|
|
},
|
|
4000,
|
|
[
|
|
{
|
|
id: "extraId",
|
|
last_modified: 42,
|
|
},
|
|
]
|
|
);
|
|
|
|
equal((await client.get()).length, 1);
|
|
|
|
// Now sync, but importing changes will have failing signature,
|
|
// and so will retry (see `sigResults`).
|
|
await Assert.rejects(
|
|
client.maybeSync(6000),
|
|
RemoteSettingsClient.InvalidSignatureError,
|
|
"Sync failed as expected (bad signature after retry)"
|
|
);
|
|
equal(i, 4, "sync has retried as expected");
|
|
|
|
// Make sure that we retried on a blank DB. The extra record should
|
|
// have been deleted when we validated the signature the second time.
|
|
// Since local data was tampered, it was cleared.
|
|
ok(/extraId/.test(sigCalls[0]), "extra record when importing changes");
|
|
ok(/extraId/.test(sigCalls[1]), "extra record when checking local");
|
|
ok(!/extraId/.test(sigCalls[2]), "db was flushed before retry");
|
|
ok(/extraId/.test(sigCalls[3]), "when checking local after retry");
|
|
});
|