"use strict";

const { Utils } = ChromeUtils.import("resource://services-settings/Utils.jsm");
const { RemoteSettings } = ChromeUtils.import(
  "resource://services-settings/remote-settings.js"
);
const { RemoteSecuritySettings } = ChromeUtils.import(
  "resource://gre/modules/psm/RemoteSecuritySettings.jsm"
);

const sss = Cc["@mozilla.org/ssservice;1"].getService(
  Ci.nsISiteSecurityService
);

const { PinningBlocklistClient } = RemoteSecuritySettings.init();

Services.prefs.setBoolPref("security.cert_pinning.hpkp.enabled", true);

add_task(async function test_uses_a_custom_signer() {
  Assert.notEqual(
    PinningBlocklistClient.signerName,
    RemoteSettings("not-specified").signerName
  );
});

add_task(async function test_pinning_has_initial_dump() {
  if (AppConstants.platform == "android") {
    // Skip test: we don't ship pinning dumps on Android (see package-manifest).
    return;
  }
  Assert.ok(
    await Utils.hasLocalDump(
      PinningBlocklistClient.bucketName,
      PinningBlocklistClient.collectionName
    )
  );
});

add_task(async function test_default_jexl_filter_is_used() {
  Assert.deepEqual(
    PinningBlocklistClient.filterFunc,
    RemoteSettings("not-specified").filterFunc
  );
});

add_task(async function test_no_pins_by_default() {
  // ensure our pins are all missing before we start
  ok(
    !sss.isSecureURI(
      sss.HEADER_HPKP,
      Services.io.newURI("https://one.example.com"),
      0
    )
  );
  ok(
    !sss.isSecureURI(
      sss.HEADER_HPKP,
      Services.io.newURI("https://two.example.com"),
      0
    )
  );
  ok(
    !sss.isSecureURI(
      sss.HEADER_HPKP,
      Services.io.newURI("https://three.example.com"),
      0
    )
  );
  ok(
    !sss.isSecureURI(
      sss.HEADER_HSTS,
      Services.io.newURI("https://four.example.com"),
      0
    )
  );
  ok(
    !sss.isSecureURI(
      sss.HEADER_HSTS,
      Services.io.newURI("https://five.example.com"),
      0
    )
  );
});

add_task(async function test_simple_pin_domain() {
  const current = [
    {
      pinType: "KeyPin",
      hostName: "one.example.com",
      includeSubdomains: false,
      expires: new Date().getTime() + 1000000,
      pins: [
        "cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=",
        "M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE=",
      ],
      versions: [Services.appinfo.version],
    },
  ];
  await PinningBlocklistClient.emit("sync", { data: { current } });

  // check that a pin exists for one.example.com
  ok(
    sss.isSecureURI(
      sss.HEADER_HPKP,
      Services.io.newURI("https://one.example.com"),
      0
    )
  );
});

add_task(async function test_existing_entries_are_erased() {
  const current = [];
  await PinningBlocklistClient.emit("sync", { data: { current } });

  // check that no pin exists for one.example.com
  ok(
    !sss.isSecureURI(
      sss.HEADER_HPKP,
      Services.io.newURI("https://one.example.com"),
      0
    )
  );
});

add_task(async function test_multiple_entries() {
  const current = [
    {
      pinType: "KeyPin",
      hostName: "two.example.com",
      includeSubdomains: false,
      expires: new Date().getTime() + 1000000,
      pins: [
        "cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=",
        "M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE=",
      ],
      versions: [Services.appinfo.version],
    },
    {
      pinType: "KeyPin",
      hostName: "three.example.com",
      includeSubdomains: false,
      expires: new Date().getTime() + 1000000,
      pins: [
        "cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=",
        "M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE=",
      ],
      versions: [
        Services.appinfo.version,
        "some other version that won't match",
      ],
    },
    {
      pinType: "KeyPin",
      hostName: "four.example.com",
      includeSubdomains: false,
      expires: new Date().getTime() + 1000000,
      pins: [
        "cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=",
        "M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE=",
      ],
      versions: ["some version that won't match"],
    },
    {
      pinType: "STSPin",
      hostName: "five.example.com",
      includeSubdomains: false,
      expires: new Date().getTime() + 1000000,
      versions: [Services.appinfo.version, "some version that won't match"],
    },
  ];
  await PinningBlocklistClient.emit("sync", { data: { current } });

  // check that a pin exists for two.example.com and three.example.com
  ok(
    sss.isSecureURI(
      sss.HEADER_HPKP,
      Services.io.newURI("https://two.example.com"),
      0
    )
  );
  ok(
    sss.isSecureURI(
      sss.HEADER_HPKP,
      Services.io.newURI("https://three.example.com"),
      0
    )
  );
  // check that a pin does not exist for four.example.com - it's in the
  // collection but the version should not match
  ok(
    !sss.isSecureURI(
      sss.HEADER_HPKP,
      Services.io.newURI("https://four.example.com"),
      0
    )
  );
  // Check that the HSTS preload added to the collection works...
  ok(
    sss.isSecureURI(
      sss.HEADER_HSTS,
      Services.io.newURI("https://five.example.com"),
      0
    )
  );
  // // ...and that includeSubdomains is honored
  ok(
    !sss.isSecureURI(
      sss.HEADER_HSTS,
      Services.io.newURI("https://subdomain.five.example.com"),
      0
    )
  );

  // Overwrite existing entries.
  current[current.length - 1].includeSubdomains = true;
  await PinningBlocklistClient.emit("sync", { data: { current } });
  // The STS entry for five.example.com now has includeSubdomains set;
  // ensure that the new includeSubdomains value is honored.
  ok(
    sss.isSecureURI(
      sss.HEADER_HSTS,
      Services.io.newURI("https://subdomain.five.example.com"),
      0
    )
  );
});

add_task(async function test_bad_entries() {
  const current = [
    {
      irrelevant: "this entry looks nothing whatsoever like a pin preload",
      pinType: "KeyPin",
    },
    {
      irrelevant: "this entry has data of the wrong type",
      pinType: "KeyPin",
      hostName: 3,
      includeSubdomains: "nonsense",
      expires: "more nonsense",
      pins: [1, 2, 3, 4],
    },
    {
      irrelevant: "this entry is missing the actual pins",
      pinType: "KeyPin",
      hostName: "missingpins.example.com",
      includeSubdomains: false,
      expires: new Date().getTime() + 1000000,
      versions: [Services.appinfo.version],
    },
    {
      pinType: "STSPin",
      hostName: "five.example.com",
      includeSubdomains: true,
      expires: new Date().getTime() + 1000000,
    }, // missing versions.
  ];
  // The event listener will catch any error, and won't throw.
  // See https://bugzilla.mozilla.org/show_bug.cgi?id=1554939
  await PinningBlocklistClient.emit("sync", { data: { current } });

  ok(
    !sss.isSecureURI(
      sss.HEADER_HPKP,
      Services.io.newURI("https://missingpins.example.com"),
      0
    )
  );

  // Check that the HSTS preload overwrites existing entries...
  // Version field is missing.
  ok(
    !sss.isSecureURI(
      sss.HEADER_HSTS,
      Services.io.newURI("https://five.example.com"),
      0
    )
  );
});
