/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

// This file consists of unit tests for cert_storage (whereas test_cert_storage.js is more of an
// integration test).

do_get_profile();

if (AppConstants.MOZ_NEW_CERT_STORAGE) {
  this.certStorage = Cc["@mozilla.org/security/certstorage;1"].getService(
    Ci.nsICertStorage
  );
}

async function addCerts(certInfos) {
  let result = await new Promise(resolve => {
    certStorage.addCerts(certInfos, resolve);
  });
  Assert.equal(result, Cr.NS_OK, "addCerts should succeed");
}

async function removeCertsByHashes(hashesBase64) {
  let result = await new Promise(resolve => {
    certStorage.removeCertsByHashes(hashesBase64, resolve);
  });
  Assert.equal(result, Cr.NS_OK, "removeCertsByHashes should succeed");
}

function getLongString(uniquePart, length) {
  return String(uniquePart).padStart(length, "0");
}

class CertInfo {
  constructor(cert, subject) {
    this.cert = btoa(cert);
    this.subject = btoa(subject);
    this.trust = Ci.nsICertStorage.TRUST_INHERIT;
  }
}
if (AppConstants.MOZ_NEW_CERT_STORAGE) {
  CertInfo.prototype.QueryInterface = ChromeUtils.generateQI([Ci.nsICertInfo]);
}

add_task(
  {
    skip_if: () => !AppConstants.MOZ_NEW_CERT_STORAGE,
  },
  async function test_common_subject() {
    let someCert1 = new CertInfo(
      "some certificate bytes 1",
      "some common subject"
    );
    let someCert2 = new CertInfo(
      "some certificate bytes 2",
      "some common subject"
    );
    let someCert3 = new CertInfo(
      "some certificate bytes 3",
      "some common subject"
    );
    await addCerts([someCert1, someCert2, someCert3]);
    let storedCerts = certStorage.findCertsBySubject(
      stringToArray("some common subject")
    );
    let storedCertsAsStrings = storedCerts.map(arrayToString);
    let expectedCerts = [
      "some certificate bytes 1",
      "some certificate bytes 2",
      "some certificate bytes 3",
    ];
    Assert.deepEqual(
      storedCertsAsStrings.sort(),
      expectedCerts.sort(),
      "should find expected certs"
    );

    await addCerts([
      new CertInfo("some other certificate bytes", "some other subject"),
    ]);
    storedCerts = certStorage.findCertsBySubject(
      stringToArray("some common subject")
    );
    storedCertsAsStrings = storedCerts.map(arrayToString);
    Assert.deepEqual(
      storedCertsAsStrings.sort(),
      expectedCerts.sort(),
      "should still find expected certs"
    );

    let storedOtherCerts = certStorage.findCertsBySubject(
      stringToArray("some other subject")
    );
    let storedOtherCertsAsStrings = storedOtherCerts.map(arrayToString);
    let expectedOtherCerts = ["some other certificate bytes"];
    Assert.deepEqual(
      storedOtherCertsAsStrings,
      expectedOtherCerts,
      "should have other certificate"
    );
  }
);

add_task(
  {
    skip_if: () => !AppConstants.MOZ_NEW_CERT_STORAGE,
  },
  async function test_many_entries() {
    const NUM_CERTS = 500;
    const CERT_LENGTH = 3000;
    const SUBJECT_LENGTH = 40;
    let certs = [];
    for (let i = 0; i < NUM_CERTS; i++) {
      certs.push(
        new CertInfo(
          getLongString(i, CERT_LENGTH),
          getLongString(i, SUBJECT_LENGTH)
        )
      );
    }
    await addCerts(certs);
    for (let i = 0; i < NUM_CERTS; i++) {
      let subject = stringToArray(getLongString(i, SUBJECT_LENGTH));
      let storedCerts = certStorage.findCertsBySubject(subject);
      Assert.equal(
        storedCerts.length,
        1,
        "should have 1 certificate (lots of data test)"
      );
      let storedCertAsString = arrayToString(storedCerts[0]);
      Assert.equal(
        storedCertAsString,
        getLongString(i, CERT_LENGTH),
        "certificate should be as expected (lots of data test)"
      );
    }
  }
);

add_task(
  {
    skip_if: () => !AppConstants.MOZ_NEW_CERT_STORAGE,
  },
  async function test_removal() {
    // As long as cert_storage is given valid base64, attempting to delete some nonexistent
    // certificate will "succeed" (it'll do nothing).
    await removeCertsByHashes([btoa("thishashisthewrongsize")]);

    let removalCert1 = new CertInfo(
      "removal certificate bytes 1",
      "common subject to remove"
    );
    let removalCert2 = new CertInfo(
      "removal certificate bytes 2",
      "common subject to remove"
    );
    let removalCert3 = new CertInfo(
      "removal certificate bytes 3",
      "common subject to remove"
    );
    await addCerts([removalCert1, removalCert2, removalCert3]);

    let storedCerts = certStorage.findCertsBySubject(
      stringToArray("common subject to remove")
    );
    let storedCertsAsStrings = storedCerts.map(arrayToString);
    let expectedCerts = [
      "removal certificate bytes 1",
      "removal certificate bytes 2",
      "removal certificate bytes 3",
    ];
    Assert.deepEqual(
      storedCertsAsStrings.sort(),
      expectedCerts.sort(),
      "should find expected certs before removing them"
    );

    // echo -n "removal certificate bytes 2" | sha256sum | xxd -r -p | base64
    await removeCertsByHashes(["2nUPHwl5TVr1mAD1FU9FivLTlTb0BAdnVUhsYgBccN4="]);
    storedCerts = certStorage.findCertsBySubject(
      stringToArray("common subject to remove")
    );
    storedCertsAsStrings = storedCerts.map(arrayToString);
    expectedCerts = [
      "removal certificate bytes 1",
      "removal certificate bytes 3",
    ];
    Assert.deepEqual(
      storedCertsAsStrings.sort(),
      expectedCerts.sort(),
      "should only have first and third certificates now"
    );

    // echo -n "removal certificate bytes 1" | sha256sum | xxd -r -p | base64
    await removeCertsByHashes(["8zoRqHYrklr7Zx6UWpzrPuL+ol8KL1Ml6XHBQmXiaTY="]);
    storedCerts = certStorage.findCertsBySubject(
      stringToArray("common subject to remove")
    );
    storedCertsAsStrings = storedCerts.map(arrayToString);
    expectedCerts = ["removal certificate bytes 3"];
    Assert.deepEqual(
      storedCertsAsStrings.sort(),
      expectedCerts.sort(),
      "should only have third certificate now"
    );

    // echo -n "removal certificate bytes 3" | sha256sum | xxd -r -p | base64
    await removeCertsByHashes(["vZn7GwDSabB/AVo0T+N26nUsfSXIIx4NgQtSi7/0p/w="]);
    storedCerts = certStorage.findCertsBySubject(
      stringToArray("common subject to remove")
    );
    Assert.equal(storedCerts.length, 0, "shouldn't have any certificates now");

    // echo -n "removal certificate bytes 3" | sha256sum | xxd -r -p | base64
    // Again, removing a nonexistent certificate should "succeed".
    await removeCertsByHashes(["vZn7GwDSabB/AVo0T+N26nUsfSXIIx4NgQtSi7/0p/w="]);
  }
);

add_task(
  {
    skip_if: () => !AppConstants.MOZ_NEW_CERT_STORAGE,
  },
  async function test_batched_removal() {
    let removalCert1 = new CertInfo(
      "batch removal certificate bytes 1",
      "batch subject to remove"
    );
    let removalCert2 = new CertInfo(
      "batch removal certificate bytes 2",
      "batch subject to remove"
    );
    let removalCert3 = new CertInfo(
      "batch removal certificate bytes 3",
      "batch subject to remove"
    );
    await addCerts([removalCert1, removalCert2, removalCert3]);
    let storedCerts = certStorage.findCertsBySubject(
      stringToArray("batch subject to remove")
    );
    let storedCertsAsStrings = storedCerts.map(arrayToString);
    let expectedCerts = [
      "batch removal certificate bytes 1",
      "batch removal certificate bytes 2",
      "batch removal certificate bytes 3",
    ];
    Assert.deepEqual(
      storedCertsAsStrings.sort(),
      expectedCerts.sort(),
      "should find expected certs before removing them"
    );
    // echo -n "batch removal certificate bytes 1" | sha256sum | xxd -r -p | base64
    // echo -n "batch removal certificate bytes 2" | sha256sum | xxd -r -p | base64
    // echo -n "batch removal certificate bytes 3" | sha256sum | xxd -r -p | base64
    await removeCertsByHashes([
      "EOEEUTuanHZX9NFVCoMKVT22puIJC6g+ZuNPpJgvaa8=",
      "Xz6h/Kvn35cCLJEZXkjPqk1GG36b56sreLyAXpO+0zg=",
      "Jr7XdiTT8ZONUL+ogNNMW2oxKxanvYOLQPKBPgH/has=",
    ]);
    storedCerts = certStorage.findCertsBySubject(
      stringToArray("batch subject to remove")
    );
    Assert.equal(storedCerts.length, 0, "shouldn't have any certificates now");
  }
);

class CRLiteState {
  constructor(subject, spkiHash, state) {
    this.subject = btoa(subject);
    this.spkiHash = spkiHash;
    this.state = state;
  }
}
if (AppConstants.MOZ_NEW_CERT_STORAGE) {
  CRLiteState.prototype.QueryInterface = ChromeUtils.generateQI([
    Ci.nsICRLiteState,
  ]);
}

async function addCRLiteState(state) {
  let result = await new Promise(resolve => {
    certStorage.setCRLiteState(state, resolve);
  });
  Assert.equal(result, Cr.NS_OK, "setCRLiteState should succeed");
}

add_task(
  {
    skip_if: () => !AppConstants.MOZ_NEW_CERT_STORAGE,
  },
  async function test_crlite_state() {
    // echo -n "some spki 1" | sha256sum | xxd -r -p | base64
    let crliteState1 = new CRLiteState(
      "some subject 1",
      "bDlKlhR5ptlvuxclnZ3RQHznG8/3pgIybrRJ/Zvn9L8=",
      Ci.nsICertStorage.STATE_ENFORCE
    );
    // echo -n "some spki 2" | sha256sum | xxd -r -p | base64
    let crliteState2 = new CRLiteState(
      "some subject 2",
      "ZlXvlHhtdx4yKwkhZqg7Opv5T1ofwzorlsCoLf0wnlY=",
      Ci.nsICertStorage.STATE_UNSET
    );
    // echo -n "some spki 3" | sha256sum | xxd -r -p | base64
    let crliteState3 = new CRLiteState(
      "some subject 3",
      "pp1SRn6njaHX/c+b2uf82JPeBkWhPfTBp/Mxb3xkjRM=",
      Ci.nsICertStorage.STATE_ENFORCE
    );
    await addCRLiteState([crliteState1, crliteState2, crliteState3]);

    let state1 = certStorage.getCRLiteState(
      stringToArray("some subject 1"),
      stringToArray("some spki 1")
    );
    Assert.equal(state1, Ci.nsICertStorage.STATE_ENFORCE);
    let state2 = certStorage.getCRLiteState(
      stringToArray("some subject 2"),
      stringToArray("some spki 2")
    );
    Assert.equal(state2, Ci.nsICertStorage.STATE_UNSET);
    let state3 = certStorage.getCRLiteState(
      stringToArray("some subject 3"),
      stringToArray("some spki 3")
    );
    Assert.equal(state3, Ci.nsICertStorage.STATE_ENFORCE);

    // Check that if we never set the state of a particular subject/spki pair, we get "unset" when we
    // look it up.
    let stateNeverSet = certStorage.getCRLiteState(
      stringToArray("some unknown subject"),
      stringToArray("some unknown spki")
    );
    Assert.equal(stateNeverSet, Ci.nsICertStorage.STATE_UNSET);

    // "some subject 1"/"some spki 1" and "some subject 3"/"some spki 3" both have their CRLite state
    // set. However, the combination of "some subject 3"/"some spki1" should not.
    let stateDifferentSubjectSPKI = certStorage.getCRLiteState(
      stringToArray("some subject 3"),
      stringToArray("some spki 1")
    );
    Assert.equal(stateDifferentSubjectSPKI, Ci.nsICertStorage.STATE_UNSET);

    let anotherStateDifferentSubjectSPKI = certStorage.getCRLiteState(
      stringToArray("some subject 1"),
      stringToArray("some spki 2")
    );
    Assert.equal(
      anotherStateDifferentSubjectSPKI,
      Ci.nsICertStorage.STATE_UNSET
    );
    let yetAnotherStateDifferentSubjectSPKI = certStorage.getCRLiteState(
      stringToArray("some subject 2"),
      stringToArray("some spki 1")
    );
    Assert.equal(
      yetAnotherStateDifferentSubjectSPKI,
      Ci.nsICertStorage.STATE_UNSET
    );

    crliteState3 = new CRLiteState(
      "some subject 3",
      "pp1SRn6njaHX/c+b2uf82JPeBkWhPfTBp/Mxb3xkjRM=",
      Ci.nsICertStorage.STATE_UNSET
    );
    await addCRLiteState([crliteState3]);
    state3 = certStorage.getCRLiteState(
      stringToArray("some subject 3"),
      stringToArray("some spki 3")
    );
    Assert.equal(state3, Ci.nsICertStorage.STATE_UNSET);

    crliteState2 = new CRLiteState(
      "some subject 2",
      "ZlXvlHhtdx4yKwkhZqg7Opv5T1ofwzorlsCoLf0wnlY=",
      Ci.nsICertStorage.STATE_ENFORCE
    );
    await addCRLiteState([crliteState2]);
    state2 = certStorage.getCRLiteState(
      stringToArray("some subject 2"),
      stringToArray("some spki 2")
    );
    Assert.equal(state2, Ci.nsICertStorage.STATE_ENFORCE);

    // Inserting a subject/spki pair with a state value outside of our expected
    // values will succeed. However, since our data type is a signed 16-bit value,
    // values outside that range will be truncated. The least significant 16 bits
    // of 2013003773 are FFFD, which when interpreted as a signed 16-bit integer
    // comes out to -3.
    // echo -n "some spki 4" | sha256sum | xxd -r -p | base64
    let bogusValueState = new CRLiteState(
      "some subject 4",
      "1eA0++hCqzt8vpzREYSqHAqpEOLchZca1Gx8viCVYzc=",
      2013003773
    );
    await addCRLiteState([bogusValueState]);
    let bogusValueStateValue = certStorage.getCRLiteState(
      stringToArray("some subject 4"),
      stringToArray("some spki 4")
    );
    Assert.equal(bogusValueStateValue, -3);
  }
);

async function enrollCertForCRLite(nsCert) {
  let { subjectString, spkiHashString } = getSubjectAndSPKIHash(nsCert);
  let crliteState = new CRLiteState(
    subjectString,
    spkiHashString,
    Ci.nsICertStorage.STATE_ENFORCE
  );
  await addCRLiteState([crliteState]);
}

add_task(
  {
    skip_if: () => !AppConstants.MOZ_NEW_CERT_STORAGE,
  },
  async function test_crlite_filter() {
    let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
      Ci.nsIX509CertDB
    );
    let validCertIssuer = constructCertFromFile(
      "test_cert_storage_direct/valid-cert-issuer.pem"
    );
    await enrollCertForCRLite(validCertIssuer);
    let validCert = constructCertFromFile(
      "test_cert_storage_direct/valid-cert.pem"
    );
    let revokedCertIssuer = constructCertFromFile(
      "test_cert_storage_direct/revoked-cert-issuer.pem"
    );
    await enrollCertForCRLite(revokedCertIssuer);
    let revokedCert = constructCertFromFile(
      "test_cert_storage_direct/revoked-cert.pem"
    );

    let filterFile = do_get_file(
      "test_cert_storage_direct/test-filter.crlite",
      false
    );
    ok(filterFile.exists(), "test filter file should exist");
    let filterBytes = stringToArray(readFile(filterFile));
    // First simulate the filter being from before the certificates being tested are valid. With
    // CRLite enabled, none of the certificates should appear to be revoked.
    let setFullCRLiteFilterResult = await new Promise(resolve => {
      certStorage.setFullCRLiteFilter(
        filterBytes,
        new Date("2017-10-28T00:00:00Z").getTime() / 1000,
        resolve
      );
    });
    Assert.equal(
      setFullCRLiteFilterResult,
      Cr.NS_OK,
      "setFullCRLiteFilter should succeed"
    );

    Services.prefs.setIntPref(
      "security.pki.crlite_mode",
      CRLiteModeEnforcePrefValue
    );
    await checkCertErrorGenericAtTime(
      certdb,
      validCert,
      PRErrorCodeSuccess,
      certificateUsageSSLServer,
      new Date("2019-11-04T00:00:00Z").getTime() / 1000,
      false,
      "skynew.jp",
      Ci.nsIX509CertDB.FLAG_LOCAL_ONLY
    );
    await checkCertErrorGenericAtTime(
      certdb,
      revokedCert,
      PRErrorCodeSuccess,
      certificateUsageSSLServer,
      new Date("2019-11-04T00:00:00Z").getTime() / 1000,
      false,
      "schunk-group.com",
      Ci.nsIX509CertDB.FLAG_LOCAL_ONLY
    );

    // Now "replace" the filter with a more recent one. The revoked certificate should be revoked.
    setFullCRLiteFilterResult = await new Promise(resolve => {
      certStorage.setFullCRLiteFilter(
        filterBytes,
        new Date("2019-10-28T00:00:00Z").getTime() / 1000,
        resolve
      );
    });
    Assert.equal(
      setFullCRLiteFilterResult,
      Cr.NS_OK,
      "setFullCRLiteFilter should succeed"
    );
    await checkCertErrorGenericAtTime(
      certdb,
      validCert,
      PRErrorCodeSuccess,
      certificateUsageSSLServer,
      new Date("2019-11-04T00:00:00Z").getTime() / 1000,
      false,
      "skynew.jp",
      Ci.nsIX509CertDB.FLAG_LOCAL_ONLY
    );
    await checkCertErrorGenericAtTime(
      certdb,
      revokedCert,
      SEC_ERROR_REVOKED_CERTIFICATE,
      certificateUsageSSLServer,
      new Date("2019-11-04T00:00:00Z").getTime() / 1000,
      false,
      "schunk-group.com",
      Ci.nsIX509CertDB.FLAG_LOCAL_ONLY
    );

    // If we're only collecting telemetry, none of the certificates should appear to be revoked.
    Services.prefs.setIntPref(
      "security.pki.crlite_mode",
      CRLiteModeTelemetryOnlyPrefValue
    );
    await checkCertErrorGenericAtTime(
      certdb,
      validCert,
      PRErrorCodeSuccess,
      certificateUsageSSLServer,
      new Date("2019-11-04T00:00:00Z").getTime() / 1000,
      false,
      "skynew.jp",
      Ci.nsIX509CertDB.FLAG_LOCAL_ONLY
    );
    await checkCertErrorGenericAtTime(
      certdb,
      revokedCert,
      PRErrorCodeSuccess,
      certificateUsageSSLServer,
      new Date("2019-11-04T00:00:00Z").getTime() / 1000,
      false,
      "schunk-group.com",
      Ci.nsIX509CertDB.FLAG_LOCAL_ONLY
    );

    // If CRLite is disabled, none of the certificates should appear to be revoked.
    Services.prefs.setIntPref(
      "security.pki.crlite_mode",
      CRLiteModeDisabledPrefValue
    );
    await checkCertErrorGenericAtTime(
      certdb,
      validCert,
      PRErrorCodeSuccess,
      certificateUsageSSLServer,
      new Date("2019-11-04T00:00:00Z").getTime() / 1000,
      false,
      "skynew.jp",
      Ci.nsIX509CertDB.FLAG_LOCAL_ONLY
    );
    await checkCertErrorGenericAtTime(
      certdb,
      revokedCert,
      PRErrorCodeSuccess,
      certificateUsageSSLServer,
      new Date("2019-11-04T00:00:00Z").getTime() / 1000,
      false,
      "schunk-group.com",
      Ci.nsIX509CertDB.FLAG_LOCAL_ONLY
    );
  }
);
