tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_remote_settings_signatures.js (29554B)


      1 /* import-globals-from ../../../common/tests/unit/head_helpers.js */
      2 "use strict";
      3 
      4 const PREF_SETTINGS_SERVER = "services.settings.server";
      5 const SIGNER_NAME = "onecrl.content-signature.mozilla.org";
      6 const TELEMETRY_COMPONENT = "remotesettings";
      7 
      8 const CERT_DIR = "test_remote_settings_signatures/";
      9 const CHAIN_FILES = ["collection_signing_ee.pem", "collection_signing_int.pem"];
     10 
     11 function getFileData(file) {
     12  const stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
     13    Ci.nsIFileInputStream
     14  );
     15  stream.init(file, -1, 0, 0);
     16  const data = NetUtil.readInputStreamToString(stream, stream.available());
     17  stream.close();
     18  return data;
     19 }
     20 
     21 function getCertChain() {
     22  const chain = [];
     23  for (let file of CHAIN_FILES) {
     24    chain.push(getFileData(do_get_file(CERT_DIR + file)));
     25  }
     26  return chain.join("\n");
     27 }
     28 
     29 let server;
     30 let client;
     31 
     32 add_setup(() => {
     33  // Signature verification is enabled by default. We use a custom signer
     34  // because these tests were originally written for OneCRL.
     35  client = RemoteSettings("signed", { signerName: SIGNER_NAME });
     36 
     37  Services.prefs.setStringPref("services.settings.loglevel", "debug");
     38 
     39  // Set up an HTTP Server
     40  server = new HttpServer();
     41  server.start(-1);
     42 
     43  registerCleanupFunction(() => {
     44    Services.prefs.clearUserPref("services.settings.loglevel");
     45    Services.prefs.clearUserPref(PREF_SETTINGS_SERVER);
     46    server.stop(() => {});
     47  });
     48 });
     49 
     50 add_task(async function test_check_signatures() {
     51  // First, perform a signature verification with known data and signature
     52  // to ensure things are working correctly
     53  let verifier = Cc[
     54    "@mozilla.org/security/contentsignatureverifier;1"
     55  ].createInstance(Ci.nsIContentSignatureVerifier);
     56 
     57  const emptyData = "[]";
     58  const emptySignature =
     59    "p384ecdsa=zbugm2FDitsHwk5-IWsas1PpWwY29f0Fg5ZHeqD8fzep7AVl2vfcaHA7LdmCZ28qZLOioGKvco3qT117Q4-HlqFTJM7COHzxGyU2MMJ0ZTnhJrPOC1fP3cVQjU1PTWi9";
     60 
     61  ok(
     62    await verifier.asyncVerifyContentSignature(
     63      emptyData,
     64      emptySignature,
     65      getCertChain(),
     66      SIGNER_NAME,
     67      Ci.nsIX509CertDB.AppXPCShellRoot
     68    )
     69  );
     70 
     71  const collectionData =
     72    '[{"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="}]';
     73  const collectionSignature =
     74    "p384ecdsa=f4pA2tYM5jQgWY6YUmhUwQiBLj6QO5sHLD_5MqLePz95qv-7cNCuQoZnPQwxoptDtW8hcWH3kLb0quR7SB-r82gkpR9POVofsnWJRA-ETb0BcIz6VvI3pDT49ZLlNg3p";
     75 
     76  ok(
     77    await verifier.asyncVerifyContentSignature(
     78      collectionData,
     79      collectionSignature,
     80      getCertChain(),
     81      SIGNER_NAME,
     82      Ci.nsIX509CertDB.AppXPCShellRoot
     83    )
     84  );
     85 });
     86 
     87 add_task(async function test_bad_signature_does_not_lead_to_empty_list() {
     88  Services.prefs.setStringPref(
     89    PREF_SETTINGS_SERVER,
     90    `http://localhost:${server.identity.primaryPort}/v1`
     91  );
     92  const x5u = `http://localhost:${server.identity.primaryPort}/x5u.pem`;
     93 
     94  const networkCalls = [];
     95 
     96  server.registerPathHandler(
     97    "/v1/buckets/monitor/collections/changes/changeset",
     98    (request, response) => {
     99      response.write(
    100        JSON.stringify({
    101          changes: [
    102            {
    103              bucket: "main",
    104              collection: "no-dump-no-local-data",
    105              last_modified: 42,
    106            },
    107          ],
    108        })
    109      );
    110      response.setHeader("Content-Type", "application/json; charset=UTF-8");
    111      response.setStatusLine(null, 200, "OK");
    112    }
    113  );
    114  server.registerPathHandler(
    115    "/v1/buckets/monitor/collections/changes/changeset",
    116    (request, response) => {
    117      networkCalls.push(request);
    118      response.write(
    119        JSON.stringify({
    120          changes: [
    121            {
    122              bucket: "main",
    123              collection: "no-dump-no-local-data",
    124              last_modified: 42,
    125            },
    126          ],
    127        })
    128      );
    129      response.setHeader("Content-Type", "application/json; charset=UTF-8");
    130      response.setStatusLine(null, 200, "OK");
    131    }
    132  );
    133  server.registerPathHandler(
    134    "/v1/buckets/main/collections/no-dump-no-local-data/changeset",
    135    (request, response) => {
    136      response.write(
    137        JSON.stringify({
    138          timestamp: 42,
    139          changes: [],
    140          metadata: {
    141            signatures: [
    142              {
    143                signature: "bad-signature",
    144                x5u,
    145              },
    146            ],
    147          },
    148        })
    149      );
    150      response.setHeader("Content-Type", "application/json; charset=UTF-8");
    151      response.setStatusLine(null, 200, "OK");
    152    }
    153  );
    154  server.registerPathHandler("/x5u.pem", (request, response) => {
    155    response.write(getCertChain()); // At least cert will be valid.
    156    response.setHeader("Content-Type", "text/plain; charset=UTF-8");
    157    response.setStatusLine(null, 200, "OK");
    158  });
    159 
    160  const clientEmpty = RemoteSettings("no-dump-no-local-data");
    161  clientEmpty.verifySignature = true; // default
    162 
    163  // Check that client.get() will initiate a sync,
    164  // and that it will throw since the signature is bad,
    165  // and not return an empty list (`emptyListFallback: false`)
    166  let error;
    167  try {
    168    await clientEmpty.get({
    169      emptyListFallback: false,
    170      syncIfEmpty: true, // default value
    171    });
    172  } catch (exc) {
    173    error = exc;
    174  }
    175  equal(error.name, "InvalidSignatureError");
    176 
    177  // Even running client.sync() will throw and won't leave
    178  // anything in the database.
    179  error = null;
    180  try {
    181    await clientEmpty.sync();
    182  } catch (exc) {
    183    error = exc;
    184  }
    185  equal(error.name, "InvalidSignatureError");
    186  equal(await clientEmpty.db.getLastModified(), null);
    187 
    188  // Call .get() again will initiate another sync.
    189  networkCalls.length = 0;
    190  try {
    191    await clientEmpty.get({
    192      emptyListFallback: false,
    193      syncIfEmpty: true, // default value
    194    });
    195  } catch (exc) {
    196    error = exc;
    197  }
    198  Assert.greater(networkCalls.length, 0, "Network calls were made");
    199  equal(error.name, "InvalidSignatureError");
    200 });
    201 
    202 add_task(async function test_check_synchronization_with_signatures() {
    203  const port = server.identity.primaryPort;
    204 
    205  const x5u = `http://localhost:${port}/test_remote_settings_signatures/test_cert_chain.pem`;
    206 
    207  // Telemetry reports.
    208  const TELEMETRY_SOURCE = client.identifier;
    209 
    210  function registerHandlers(responses) {
    211    function handleResponse(serverTimeMillis, request, response) {
    212      const key = `${request.method}:${request.path}?${request.queryString}`;
    213      const available = responses[key];
    214      const sampled = available.length > 1 ? available.shift() : available[0];
    215      if (!sampled) {
    216        do_throw(
    217          `unexpected ${request.method} request for ${request.path}?${request.queryString}`
    218        );
    219      }
    220 
    221      response.setStatusLine(
    222        null,
    223        sampled.status.status,
    224        sampled.status.statusText
    225      );
    226      // send the headers
    227      for (let headerLine of sampled.sampleHeaders) {
    228        let headerElements = headerLine.split(":");
    229        response.setHeader(headerElements[0], headerElements[1].trimLeft());
    230      }
    231 
    232      // set the server date
    233      response.setHeader("Date", new Date(serverTimeMillis).toUTCString());
    234 
    235      response.write(sampled.responseBody);
    236    }
    237 
    238    for (let key of Object.keys(responses)) {
    239      const keyParts = key.split(":");
    240      const valueParts = keyParts[1].split("?");
    241      const path = valueParts[0];
    242 
    243      server.registerPathHandler(path, handleResponse.bind(null, 2000));
    244    }
    245  }
    246 
    247  // set up prefs so the kinto updater talks to the test server
    248  Services.prefs.setStringPref(
    249    PREF_SETTINGS_SERVER,
    250    `http://localhost:${server.identity.primaryPort}/v1`
    251  );
    252 
    253  // These are records we'll use in the test collections
    254  const RECORD1 = {
    255    details: {
    256      bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
    257      created: "2016-01-18T14:43:37Z",
    258      name: "GlobalSign certs",
    259      who: ".",
    260      why: ".",
    261    },
    262    enabled: true,
    263    id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea",
    264    issuerName:
    265      "MHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNpZ24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRQ==",
    266    last_modified: 2000,
    267    serialNumber: "BAAAAAABA/A35EU=",
    268  };
    269 
    270  const RECORD2 = {
    271    details: {
    272      bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
    273      created: "2016-01-18T14:48:11Z",
    274      name: "GlobalSign certs",
    275      who: ".",
    276      why: ".",
    277    },
    278    enabled: true,
    279    id: "e3bd531e-1ee4-7407-27ce-6fdc9cecbbdc",
    280    issuerName:
    281      "MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB",
    282    last_modified: 3000,
    283    serialNumber: "BAAAAAABI54PryQ=",
    284  };
    285 
    286  const RECORD3 = {
    287    details: {
    288      bug: "https://bugzilla.mozilla.org/show_bug.cgi?id=1155145",
    289      created: "2016-01-18T14:48:11Z",
    290      name: "GlobalSign certs",
    291      who: ".",
    292      why: ".",
    293    },
    294    enabled: true,
    295    id: "c7c49b69-a4ab-418e-92a9-e1961459aa7f",
    296    issuerName:
    297      "MIGBMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTElMCMGA1UECxMcUHJpbWFyeSBPYmplY3QgUHVibGlzaGluZyBDQTEwMC4GA1UEAxMnR2xvYmFsU2lnbiBQcmltYXJ5IE9iamVjdCBQdWJsaXNoaW5nIENB",
    298    last_modified: 4000,
    299    serialNumber: "BAAAAAABI54PryQ=",
    300  };
    301 
    302  const RECORD1_DELETION = {
    303    deleted: true,
    304    enabled: true,
    305    id: "97fbf7c4-3ef2-f54f-0029-1ba6540c63ea",
    306    last_modified: 3500,
    307  };
    308 
    309  // Check that a signature on an empty collection is OK
    310  // We need to set up paths on the HTTP server to return specific data from
    311  // specific paths for each test. Here we prepare data for each response.
    312 
    313  // A cert chain response (this the cert chain that contains the signing
    314  // cert, the root and any intermediates in between). This is used in each
    315  // sync.
    316  const RESPONSE_CERT_CHAIN = {
    317    comment: "RESPONSE_CERT_CHAIN",
    318    sampleHeaders: ["Content-Type: text/plain; charset=UTF-8"],
    319    status: { status: 200, statusText: "OK" },
    320    responseBody: getCertChain(),
    321  };
    322 
    323  // A server settings response. This is used in each sync.
    324  const RESPONSE_SERVER_SETTINGS = {
    325    comment: "RESPONSE_SERVER_SETTINGS",
    326    sampleHeaders: [
    327      "Access-Control-Allow-Origin: *",
    328      "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
    329      "Content-Type: application/json; charset=UTF-8",
    330      "Server: waitress",
    331    ],
    332    status: { status: 200, statusText: "OK" },
    333    responseBody: JSON.stringify({
    334      settings: {
    335        batch_max_requests: 25,
    336      },
    337      url: `http://localhost:${port}/v1/`,
    338      documentation: "https://kinto.readthedocs.org/",
    339      version: "1.5.1",
    340      commit: "cbc6f58",
    341      hello: "kinto",
    342    }),
    343  };
    344 
    345  // This is the initial, empty state of the collection. This is only used
    346  // for the first sync.
    347  const RESPONSE_EMPTY_INITIAL = {
    348    comment: "RESPONSE_EMPTY_INITIAL",
    349    sampleHeaders: [
    350      "Content-Type: application/json; charset=UTF-8",
    351      'ETag: "1000"',
    352    ],
    353    status: { status: 200, statusText: "OK" },
    354    responseBody: JSON.stringify({
    355      timestamp: 1000,
    356      metadata: {
    357        signatures: [
    358          {
    359            x5u,
    360            signature:
    361              "vxuAg5rDCB-1pul4a91vqSBQRXJG_j7WOYUTswxRSMltdYmbhLRH8R8brQ9YKuNDF56F-w6pn4HWxb076qgKPwgcEBtUeZAO_RtaHXRkRUUgVzAr86yQL4-aJTbv3D6u",
    362          },
    363        ],
    364      },
    365      changes: [],
    366    }),
    367  };
    368 
    369  // Here, we map request method and path to the available responses
    370  const emptyCollectionResponses = {
    371    "GET:/test_remote_settings_signatures/test_cert_chain.pem?": [
    372      RESPONSE_CERT_CHAIN,
    373    ],
    374    "GET:/v1/?": [RESPONSE_SERVER_SETTINGS],
    375    "GET:/v1/buckets/main/collections/signed/changeset?_expected=1000": [
    376      RESPONSE_EMPTY_INITIAL,
    377    ],
    378  };
    379 
    380  //
    381  // 1.
    382  // - collection: undefined -> []
    383  // - timestamp: undefined -> 1000
    384  //
    385 
    386  // .. and use this map to register handlers for each path
    387  registerHandlers(emptyCollectionResponses);
    388 
    389  let startSnapshot = getUptakeTelemetrySnapshot(
    390    TELEMETRY_COMPONENT,
    391    TELEMETRY_SOURCE
    392  );
    393 
    394  // With all of this set up, we attempt a sync. This will resolve if all is
    395  // well and throw if something goes wrong.
    396  await client.maybeSync(1000);
    397 
    398  equal((await client.get()).length, 0);
    399 
    400  let endSnapshot = getUptakeTelemetrySnapshot(
    401    TELEMETRY_COMPONENT,
    402    TELEMETRY_SOURCE
    403  );
    404 
    405  // ensure that a success histogram is tracked when a succesful sync occurs.
    406  let expectedIncrements = {
    407    [UptakeTelemetry.STATUS.SYNC_START]: 1,
    408    [UptakeTelemetry.STATUS.SUCCESS]: 1,
    409  };
    410  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
    411 
    412  //
    413  // 2.
    414  // - collection: [] -> [RECORD2, RECORD1]
    415  // - timestamp: 1000 -> 3000
    416  //
    417  // Check that some additions (2 records) to the collection have a valid
    418  // signature.
    419 
    420  // This response adds two entries (RECORD1 and RECORD2) to the collection
    421  const RESPONSE_TWO_ADDED = {
    422    comment: "RESPONSE_TWO_ADDED",
    423    sampleHeaders: [
    424      "Content-Type: application/json; charset=UTF-8",
    425      'ETag: "3000"',
    426    ],
    427    status: { status: 200, statusText: "OK" },
    428    responseBody: JSON.stringify({
    429      timestamp: 3000,
    430      metadata: {
    431        signatures: [
    432          {
    433            x5u,
    434            signature:
    435              "dwhJeypadNIyzGj3QdI0KMRTPnHhFPF_j73mNrsPAHKMW46S2Ftf4BzsPMvPMB8h0TjDus13wo_R4l432DHe7tYyMIWXY0PBeMcoe5BREhFIxMxTsh9eGVXBD1e3UwRy",
    436          },
    437        ],
    438      },
    439      changes: [RECORD2, RECORD1],
    440    }),
    441  };
    442 
    443  const twoItemsResponses = {
    444    "GET:/v1/buckets/main/collections/signed/changeset?_expected=3000&_since=%221000%22":
    445      [RESPONSE_TWO_ADDED],
    446  };
    447  registerHandlers(twoItemsResponses);
    448  await client.maybeSync(3000);
    449 
    450  equal((await client.get()).length, 2);
    451 
    452  //
    453  // 3.
    454  // - collection: [RECORD2, RECORD1] -> [RECORD2, RECORD3]
    455  // - timestamp: 3000 -> 4000
    456  //
    457  // Check the collection with one addition and one removal has a valid
    458  // signature
    459  const THREE_ITEMS_SIG =
    460    "MIEmNghKnkz12UodAAIc3q_Y4a3IJJ7GhHF4JYNYmm8avAGyPM9fYU7NzVo94pzjotG7vmtiYuHyIX2rTHTbT587w0LdRWxipgFd_PC1mHiwUyjFYNqBBG-kifYk7kEw";
    461 
    462  // Remove RECORD1, add RECORD3
    463  const RESPONSE_ONE_ADDED_ONE_REMOVED = {
    464    comment: "RESPONSE_ONE_ADDED_ONE_REMOVED ",
    465    sampleHeaders: [
    466      "Content-Type: application/json; charset=UTF-8",
    467      'ETag: "4000"',
    468    ],
    469    status: { status: 200, statusText: "OK" },
    470    responseBody: JSON.stringify({
    471      timestamp: 4000,
    472      metadata: {
    473        signatures: [
    474          {
    475            x5u,
    476            signature: THREE_ITEMS_SIG,
    477          },
    478        ],
    479      },
    480      changes: [RECORD3, RECORD1_DELETION],
    481    }),
    482  };
    483 
    484  const oneAddedOneRemovedResponses = {
    485    "GET:/v1/buckets/main/collections/signed/changeset?_expected=4000&_since=%223000%22":
    486      [RESPONSE_ONE_ADDED_ONE_REMOVED],
    487  };
    488  registerHandlers(oneAddedOneRemovedResponses);
    489  await client.maybeSync(4000);
    490 
    491  equal((await client.get()).length, 2);
    492 
    493  //
    494  // 4.
    495  // - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
    496  // - timestamp: 4000 -> 4100
    497  //
    498  // Check the signature is still valid with no operation (no changes)
    499 
    500  // Leave the collection unchanged
    501  const RESPONSE_EMPTY_NO_UPDATE = {
    502    comment: "RESPONSE_EMPTY_NO_UPDATE ",
    503    sampleHeaders: [
    504      "Content-Type: application/json; charset=UTF-8",
    505      'ETag: "4000"',
    506    ],
    507    status: { status: 200, statusText: "OK" },
    508    responseBody: JSON.stringify({
    509      timestamp: 4000,
    510      metadata: {
    511        signatures: [
    512          {
    513            x5u,
    514            signature: THREE_ITEMS_SIG,
    515          },
    516        ],
    517      },
    518      changes: [],
    519    }),
    520  };
    521 
    522  const noOpResponses = {
    523    "GET:/v1/buckets/main/collections/signed/changeset?_expected=4100&_since=%224000%22":
    524      [RESPONSE_EMPTY_NO_UPDATE],
    525  };
    526  registerHandlers(noOpResponses);
    527  await client.maybeSync(4100);
    528 
    529  equal((await client.get()).length, 2);
    530 
    531  //
    532  // 5.
    533  // - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
    534  // - timestamp: 4000 -> 5000
    535  //
    536  // Check the collection is reset when the signature is invalid.
    537  // Client will:
    538  //   - Fetch metadata (with bad signature)
    539  //   - Perform the sync (fetch empty changes)
    540  //   - Refetch the metadata and the whole collection
    541  //   - Validate signature successfully, but with no changes to emit.
    542 
    543  const RESPONSE_COMPLETE_INITIAL = {
    544    comment: "RESPONSE_COMPLETE_INITIAL ",
    545    sampleHeaders: [
    546      "Content-Type: application/json; charset=UTF-8",
    547      'ETag: "4000"',
    548    ],
    549    status: { status: 200, statusText: "OK" },
    550    responseBody: JSON.stringify({
    551      timestamp: 4000,
    552      metadata: {
    553        signatures: [
    554          {
    555            x5u,
    556            signature: THREE_ITEMS_SIG,
    557          },
    558        ],
    559      },
    560      changes: [RECORD2, RECORD3],
    561    }),
    562  };
    563 
    564  const RESPONSE_EMPTY_NO_UPDATE_BAD_SIG = {
    565    ...RESPONSE_EMPTY_NO_UPDATE,
    566    responseBody: JSON.stringify({
    567      timestamp: 4000,
    568      metadata: {
    569        signatures: [
    570          {
    571            x5u,
    572            signature: "aW52YWxpZCBzaWduYXR1cmUK",
    573          },
    574        ],
    575      },
    576      changes: [],
    577    }),
    578  };
    579 
    580  const badSigGoodSigResponses = {
    581    // The first collection state is the three item collection (since
    582    // there was sync with no updates before) - but, since the signature is wrong,
    583    // another request will be made...
    584    "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=%224000%22":
    585      [RESPONSE_EMPTY_NO_UPDATE_BAD_SIG],
    586    // Subsequent signature returned is a valid one for the three item
    587    // collection.
    588    "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [
    589      RESPONSE_COMPLETE_INITIAL,
    590    ],
    591  };
    592 
    593  registerHandlers(badSigGoodSigResponses);
    594 
    595  startSnapshot = getUptakeTelemetrySnapshot(
    596    TELEMETRY_COMPONENT,
    597    TELEMETRY_SOURCE
    598  );
    599 
    600  let syncEventSent = false;
    601  client.on("sync", () => {
    602    syncEventSent = true;
    603  });
    604 
    605  await client.maybeSync(5000);
    606 
    607  equal((await client.get()).length, 2);
    608 
    609  endSnapshot = getUptakeTelemetrySnapshot(
    610    TELEMETRY_COMPONENT,
    611    TELEMETRY_SOURCE
    612  );
    613 
    614  // since we only fixed the signature, and no data was changed, the sync event
    615  // was not sent.
    616  equal(syncEventSent, false);
    617 
    618  // ensure that the failure count is incremented for a succesful sync with an
    619  // (initial) bad signature - only SERVICES_SETTINGS_SYNC_SIG_FAIL should
    620  // increment.
    621  expectedIncrements = {
    622    [UptakeTelemetry.STATUS.SYNC_START]: -2,
    623    [UptakeTelemetry.STATUS.SIGNATURE_ERROR]: 1,
    624  };
    625  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
    626 
    627  //
    628  // 6.
    629  // - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
    630  // - timestamp: 4000 -> 5000
    631  //
    632  // Check the collection is reset when the signature is invalid.
    633  // Client will:
    634  //   - Fetch metadata (with bad signature)
    635  //   - Perform the sync (fetch empty changes)
    636  //   - Refetch the whole collection and metadata
    637  //   - Sync will be no-op since local is equal to server, no changes to emit.
    638 
    639  const badSigGoodOldResponses = {
    640    // The first collection state is the current state (since there's no update
    641    // - but, since the signature is wrong, another request will be made)
    642    "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=%224000%22":
    643      [RESPONSE_EMPTY_NO_UPDATE_BAD_SIG],
    644    // The next request is for the full collection. This will be
    645    // checked against the valid signature and last_modified times will be
    646    // compared. Sync should be a no-op, even though the signature is good,
    647    // because the local collection is newer.
    648    "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [
    649      RESPONSE_EMPTY_INITIAL,
    650    ],
    651  };
    652 
    653  // ensure our collection hasn't been replaced with an older, empty one
    654  equal((await client.get()).length, 2, "collection was restored");
    655 
    656  registerHandlers(badSigGoodOldResponses);
    657 
    658  syncEventSent = false;
    659  client.on("sync", () => {
    660    syncEventSent = true;
    661  });
    662 
    663  await client.maybeSync(5000);
    664 
    665  // Local data was unchanged, since it was never than the one returned by the server,
    666  // thus the sync event is not sent.
    667  equal(syncEventSent, false, "event was not sent");
    668 
    669  //
    670  // 7.
    671  // - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
    672  // - timestamp: 4000 -> 5000
    673  //
    674  // Check that a tampered local DB will be overwritten and
    675  // sync event contain the appropriate data.
    676 
    677  const RESPONSE_COMPLETE_BAD_SIG = {
    678    ...RESPONSE_EMPTY_NO_UPDATE,
    679    responseBody: JSON.stringify({
    680      timestamp: 5000,
    681      metadata: {
    682        signatures: [
    683          {
    684            x5u,
    685            signature: "aW52YWxpZCBzaWduYXR1cmUK",
    686          },
    687        ],
    688      },
    689      changes: [RECORD2, RECORD3],
    690    }),
    691  };
    692 
    693  const badLocalContentGoodSigResponses = {
    694    "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=%223900%22":
    695      [RESPONSE_COMPLETE_BAD_SIG],
    696    "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [
    697      RESPONSE_COMPLETE_INITIAL,
    698    ],
    699  };
    700 
    701  registerHandlers(badLocalContentGoodSigResponses);
    702 
    703  // we create a local state manually here, in order to test that the sync event data
    704  // properly contains created, updated, and deleted records.
    705  // the local DB contains same id as RECORD2 and a fake record.
    706  // the final server collection contains RECORD2 and RECORD3
    707  const localId = "0602b1b2-12ab-4d3a-b6fb-593244e7b035";
    708  await client.db.importChanges(
    709    { signatures: [{ x5u, signature: "abc" }] },
    710    3900,
    711    [
    712      { ...RECORD2, last_modified: 1234567890, serialNumber: "abc" },
    713      { id: localId },
    714    ],
    715    {
    716      clear: true,
    717    }
    718  );
    719 
    720  let syncData = null;
    721  client.on("sync", ({ data }) => {
    722    syncData = data;
    723  });
    724 
    725  // Clear events snapshot.
    726  TelemetryTestUtils.assertEvents([], {}, { process: "dummy" });
    727 
    728  const TELEMETRY_EVENTS_FILTERS = {
    729    category: "uptake.remotecontent.result",
    730    method: "uptake",
    731  };
    732 
    733  // Events telemetry is sampled on released, use fake channel.
    734  await client.maybeSync(5000);
    735 
    736  // We should report a corruption_error.
    737  TelemetryTestUtils.assertEvents(
    738    [
    739      [
    740        "uptake.remotecontent.result",
    741        "uptake",
    742        "remotesettings",
    743        UptakeTelemetry.STATUS.SYNC_START,
    744        {
    745          source: client.identifier,
    746          trigger: "manual",
    747        },
    748      ],
    749      [
    750        "uptake.remotecontent.result",
    751        "uptake",
    752        "remotesettings",
    753        UptakeTelemetry.STATUS.CORRUPTION_ERROR,
    754        {
    755          source: client.identifier,
    756          duration: v => v > 0,
    757          trigger: "manual",
    758        },
    759      ],
    760    ],
    761    TELEMETRY_EVENTS_FILTERS
    762  );
    763 
    764  // The local data was corrupted, and the Telemetry status reflects it.
    765  // But the sync overwrote the bad data and was eventually a success.
    766  // Since local data was replaced, we use records IDs to determine
    767  // what was created and deleted. And bad local data will appear
    768  // in the sync event as deleted.
    769  equal(syncData.current.length, 2);
    770  equal(syncData.created.length, 1);
    771  equal(syncData.created[0].id, RECORD3.id);
    772  equal(syncData.updated.length, 1);
    773  equal(syncData.updated[0].old.serialNumber, "abc");
    774  equal(syncData.updated[0].new.serialNumber, RECORD2.serialNumber);
    775  equal(syncData.deleted.length, 1);
    776  equal(syncData.deleted[0].id, localId);
    777 
    778  //
    779  // 8.
    780  // - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3] (unchanged because of error)
    781  // - timestamp: 4000 -> 6000
    782  //
    783  // Check that a failing signature throws after retry, and that sync changes
    784  // are not applied.
    785 
    786  const RESPONSE_ONLY_RECORD4_BAD_SIG = {
    787    comment: "Create RECORD4",
    788    sampleHeaders: [
    789      "Content-Type: application/json; charset=UTF-8",
    790      'ETag: "6000"',
    791    ],
    792    status: { status: 200, statusText: "OK" },
    793    responseBody: JSON.stringify({
    794      timestamp: 6000,
    795      metadata: {
    796        signatures: [
    797          {
    798            x5u,
    799            signature: "aaaaaaaaaaaaaaaaaaaaaaaa", // sig verifier wants proper length or will crash.
    800          },
    801        ],
    802      },
    803      changes: [
    804        {
    805          id: "f765df30-b2f1-42f6-9803-7bd5a07b5098",
    806          last_modified: 6000,
    807        },
    808      ],
    809    }),
    810  };
    811  const RESPONSE_EMPTY_NO_UPDATE_BAD_SIG_6000 = {
    812    ...RESPONSE_EMPTY_NO_UPDATE,
    813    responseBody: JSON.stringify({
    814      timestamp: 6000,
    815      metadata: {
    816        signatures: [
    817          {
    818            x5u,
    819            signature: "aW52YWxpZCBzaWduYXR1cmUK",
    820          },
    821        ],
    822      },
    823      changes: [],
    824    }),
    825  };
    826  const allBadSigResponses = {
    827    "GET:/v1/buckets/main/collections/signed/changeset?_expected=6000&_since=%224000%22":
    828      [RESPONSE_EMPTY_NO_UPDATE_BAD_SIG_6000],
    829    "GET:/v1/buckets/main/collections/signed/changeset?_expected=6000": [
    830      RESPONSE_ONLY_RECORD4_BAD_SIG,
    831    ],
    832  };
    833 
    834  startSnapshot = getUptakeTelemetrySnapshot(
    835    TELEMETRY_COMPONENT,
    836    TELEMETRY_SOURCE
    837  );
    838  registerHandlers(allBadSigResponses);
    839  await Assert.rejects(
    840    client.maybeSync(6000),
    841    RemoteSettingsClient.InvalidSignatureError,
    842    "Sync failed as expected (bad signature after retry)"
    843  );
    844 
    845  // Ensure that the failure is reflected in the accumulated telemetry:
    846  endSnapshot = getUptakeTelemetrySnapshot(
    847    TELEMETRY_COMPONENT,
    848    TELEMETRY_SOURCE
    849  );
    850  expectedIncrements = {
    851    [UptakeTelemetry.STATUS.SYNC_START]: 1,
    852    [UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR]: 1,
    853  };
    854  checkUptakeTelemetry(startSnapshot, endSnapshot, expectedIncrements);
    855 
    856  // When signature fails after retry, the local data present before sync
    857  // should be maintained (if its signature is valid).
    858  ok(
    859    arrayEqual(
    860      (await client.get()).map(r => r.id),
    861      [RECORD3.id, RECORD2.id]
    862    ),
    863    "Local records were not changed"
    864  );
    865  // And local data should still be valid.
    866  await client.get({ verifySignature: true }); // Not raising.
    867 
    868  //
    869  // 9.
    870  // - collection: [RECORD2, RECORD3] -> [] (cleared)
    871  // - timestamp: 4000 -> 6000
    872  //
    873  // Check that local data is cleared during sync if signature is not valid.
    874 
    875  await client.db.create({
    876    id: "c6b19c67-2e0e-4a82-b7f7-1777b05f3e81",
    877    last_modified: 42,
    878    tampered: true,
    879  });
    880 
    881  await Assert.rejects(
    882    client.maybeSync(6000),
    883    RemoteSettingsClient.InvalidSignatureError,
    884    "Sync failed as expected (bad signature after retry)"
    885  );
    886 
    887  // Since local data was tampered, it was cleared.
    888  equal((await client.get()).length, 0, "Local database is now empty.");
    889 
    890  //
    891  // 10.
    892  // - collection: [RECORD2, RECORD3] -> [] (cleared)
    893  // - timestamp: 4000 -> 6000
    894  //
    895  // Check that local data is cleared during sync if signature is not valid.
    896 
    897  await client.db.create({
    898    id: "c6b19c67-2e0e-4a82-b7f7-1777b05f3e81",
    899    last_modified: 42,
    900    tampered: true,
    901  });
    902 
    903  await Assert.rejects(
    904    client.maybeSync(6000),
    905    RemoteSettingsClient.InvalidSignatureError,
    906    "Sync failed as expected (bad signature after retry)"
    907  );
    908  // Since local data was tampered, it was cleared.
    909  equal((await client.get()).length, 0, "Local database is now empty.");
    910 
    911  //
    912  // 11.
    913  // - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
    914  // - timestamp: 4000 -> 6000
    915  //
    916  // Check that local data is restored if signature was valid before sync.
    917  const sigCalls = [];
    918  let i = 0;
    919  client._verifier = {
    920    async asyncVerifyContentSignature(serialized) {
    921      sigCalls.push(serialized);
    922      console.log(`verify call ${i}`);
    923      return [
    924        false, // After importing changes.
    925        true, // When checking previous local data.
    926        false, // Still fail after retry.
    927        true, // When checking previous local data again.
    928      ][i++];
    929    },
    930  };
    931  // Create an extra record. It will have a valid signature locally
    932  // thanks to the verifier mock.
    933  await client.db.importChanges(
    934    {
    935      signatures: [{ x5u, signature: "aa" }],
    936    },
    937    4000,
    938    [
    939      {
    940        id: "extraId",
    941        last_modified: 42,
    942      },
    943    ]
    944  );
    945 
    946  equal((await client.get()).length, 1);
    947 
    948  // Now sync, but importing changes will have failing signature,
    949  // and so will retry (see `sigResults`).
    950  await Assert.rejects(
    951    client.maybeSync(6000),
    952    RemoteSettingsClient.InvalidSignatureError,
    953    "Sync failed as expected (bad signature after retry)"
    954  );
    955  equal(i, 4, "sync has retried as expected");
    956 
    957  // Make sure that we retried on a blank DB. The extra record should
    958  // have been deleted when we validated the signature the second time.
    959  // Since local data was tampered, it was cleared.
    960  ok(/extraId/.test(sigCalls[0]), "extra record when importing changes");
    961  ok(/extraId/.test(sigCalls[1]), "extra record when checking local");
    962  ok(!/extraId/.test(sigCalls[2]), "db was flushed before retry");
    963  ok(/extraId/.test(sigCalls[3]), "when checking local after retry");
    964 });