// -*- 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";

// Tests the certificate overrides we allow.
// add_cert_override_test will queue a test that does the following:
// 1. Attempt to connect to the given host. This should fail with the
//    given error.
// 2. Add an override for that host/port/certificate.
// 3. Connect again. This should succeed.

do_get_profile();

// Enable the collection (during test) for all products so even products
// that don't collect the data will be able to run the test without failure.
Services.prefs.setBoolPref(
  "toolkit.telemetry.testing.overrideProductsCheck",
  true
);

function check_telemetry() {
  let histogram = Services.telemetry
    .getHistogramById("SSL_CERT_ERROR_OVERRIDES")
    .snapshot();
  equal(histogram.values[0], 0, "Should have 0 unclassified values");
  equal(
    histogram.values[2],
    9,
    "Actual and expected SEC_ERROR_UNKNOWN_ISSUER values should match"
  );
  equal(
    histogram.values[3],
    1,
    "Actual and expected SEC_ERROR_CA_CERT_INVALID values should match"
  );
  equal(
    histogram.values[4] || 0,
    0,
    "Actual and expected SEC_ERROR_UNTRUSTED_ISSUER values should match"
  );
  equal(
    histogram.values[5],
    1,
    "Actual and expected SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE values should match"
  );
  equal(
    histogram.values[6] || 0,
    0,
    "Actual and expected SEC_ERROR_UNTRUSTED_CERT values should match"
  );
  equal(
    histogram.values[7] || 0,
    0,
    "Actual and expected SEC_ERROR_INADEQUATE_KEY_USAGE values should match"
  );
  equal(
    histogram.values[8],
    2,
    "Actual and expected SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED values should match"
  );
  equal(
    histogram.values[9],
    gIsDebugBuild ? 9 : 8,
    "Actual and expected SSL_ERROR_BAD_CERT_DOMAIN values should match"
  );
  equal(
    histogram.values[10],
    1,
    "Actual and expected SEC_ERROR_EXPIRED_CERTIFICATE values should match"
  );
  equal(
    histogram.values[11],
    2,
    "Actual and expected MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY values should match"
  );
  equal(
    histogram.values[12],
    1,
    "Actual and expected MOZILLA_PKIX_ERROR_V1_CERT_USED_AS_CA values should match"
  );
  equal(
    histogram.values[13],
    1,
    "Actual and expected MOZILLA_PKIX_ERROR_INADEQUATE_KEY_SIZE values should match"
  );
  equal(
    histogram.values[14],
    1,
    "Actual and expected MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE values should match"
  );
  equal(
    histogram.values[15],
    1,
    "Actual and expected MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE values should match"
  );
  equal(
    histogram.values[16],
    2,
    "Actual and expected SEC_ERROR_INVALID_TIME values should match"
  );
  equal(
    histogram.values[17],
    1,
    "Actual and expected MOZILLA_PKIX_ERROR_EMPTY_ISSUER_NAME values should match"
  );
  equal(
    histogram.values[19],
    4,
    "Actual and expected MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT values should match"
  );
  equal(
    histogram.values[20],
    1,
    "Actual and expected MOZILLA_PKIX_ERROR_MITM_DETECTED values should match"
  );

  let keySizeHistogram = Services.telemetry
    .getHistogramById("CERT_CHAIN_KEY_SIZE_STATUS")
    .snapshot();
  equal(
    keySizeHistogram.values[0],
    0,
    "Actual and expected unchecked key size values should match"
  );
  equal(
    keySizeHistogram.values[1],
    gIsDebugBuild ? 17 : 15,
    "Actual and expected successful verifications of 2048-bit keys should match"
  );
  equal(
    keySizeHistogram.values[2] || 0,
    0,
    "Actual and expected successful verifications of 1024-bit keys should match"
  );
  equal(
    keySizeHistogram.values[3],
    70,
    "Actual and expected verification failures unrelated to key size should match"
  );

  run_next_test();
}

// Internally, specifying "port" -1 is the same as port 443. This tests that.
function run_port_equivalency_test(inPort, outPort) {
  Assert.ok(
    (inPort == 443 && outPort == -1) || (inPort == -1 && outPort == 443),
    "The two specified ports must be -1 and 443 (in any order)"
  );
  let certOverrideService = Cc[
    "@mozilla.org/security/certoverride;1"
  ].getService(Ci.nsICertOverrideService);
  let cert = constructCertFromFile("bad_certs/default-ee.pem");
  let expectedTemporary = true;
  certOverrideService.rememberValidityOverride(
    "example.com",
    inPort,
    {},
    cert,
    expectedTemporary
  );
  let actualTemporary = {};
  Assert.ok(
    certOverrideService.hasMatchingOverride(
      "example.com",
      outPort,
      {},
      cert,
      actualTemporary
    ),
    `override set on port ${inPort} should match port ${outPort}`
  );
  equal(
    actualTemporary.value,
    expectedTemporary,
    "input override temporary value should match output temporary value"
  );
  Assert.ok(
    !certOverrideService.hasMatchingOverride("example.com", 563, {}, cert, {}),
    `override set on port ${inPort} should not match port 563`
  );
  certOverrideService.clearValidityOverride("example.com", inPort, {});
  Assert.ok(
    !certOverrideService.hasMatchingOverride(
      "example.com",
      outPort,
      {},
      cert,
      {}
    ),
    `override cleared on port ${inPort} should match port ${outPort}`
  );
}

function run_test() {
  run_port_equivalency_test(-1, 443);
  run_port_equivalency_test(443, -1);

  Services.prefs.setIntPref("security.OCSP.enabled", 1);
  add_tls_server_setup("BadCertAndPinningServer", "bad_certs");

  let fakeOCSPResponder = new HttpServer();
  fakeOCSPResponder.registerPrefixHandler("/", function (request, response) {
    response.setStatusLine(request.httpVersion, 500, "Internal Server Error");
  });
  fakeOCSPResponder.start(8888);

  add_simple_tests();
  add_localhost_tests();
  add_combo_tests();
  add_distrust_tests();

  add_test(function () {
    fakeOCSPResponder.stop(check_telemetry);
  });

  run_next_test();
}

function add_simple_tests() {
  add_cert_override_test("expired.example.com", SEC_ERROR_EXPIRED_CERTIFICATE);
  add_cert_override_test(
    "notyetvalid.example.com",
    MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE
  );
  add_cert_override_test("before-epoch.example.com", SEC_ERROR_INVALID_TIME);
  add_cert_override_test(
    "before-epoch-self-signed.example.com",
    MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT
  );
  add_cert_override_test(
    "selfsigned.example.com",
    MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT
  );
  add_cert_override_test("unknownissuer.example.com", SEC_ERROR_UNKNOWN_ISSUER);
  add_cert_override_test(
    "expiredissuer.example.com",
    SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE
  );
  add_cert_override_test(
    "notyetvalidissuer.example.com",
    MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE
  );
  add_cert_override_test(
    "before-epoch-issuer.example.com",
    SEC_ERROR_INVALID_TIME
  );
  add_cert_override_test(
    "md5signature.example.com",
    SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED
  );
  add_cert_override_test(
    "emptyissuername.example.com",
    MOZILLA_PKIX_ERROR_EMPTY_ISSUER_NAME
  );
  // This has name information in the subject alternative names extension,
  // but not the subject common name.
  add_cert_override_test("mismatch.example.com", SSL_ERROR_BAD_CERT_DOMAIN);
  // This has name information in the subject common name but not the subject
  // alternative names extension.
  add_cert_override_test("mismatch-CN.example.com", SSL_ERROR_BAD_CERT_DOMAIN);

  // A Microsoft IIS utility generates self-signed certificates with
  // properties similar to the one this "host" will present.
  add_cert_override_test(
    "selfsigned-inadequateEKU.example.com",
    MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT
  );

  add_prevented_cert_override_test(
    "inadequatekeyusage.example.com",
    SEC_ERROR_INADEQUATE_KEY_USAGE
  );

  // Test triggering the MitM detection. We don't set-up a proxy here. Just
  // set the pref. Without the pref set we expect an unkown issuer error.
  add_cert_override_test("mitm.example.com", SEC_ERROR_UNKNOWN_ISSUER);
  add_test(function () {
    Services.prefs.setStringPref(
      "security.pki.mitm_canary_issuer",
      "CN=Test MITM Root"
    );
    let certOverrideService = Cc[
      "@mozilla.org/security/certoverride;1"
    ].getService(Ci.nsICertOverrideService);
    certOverrideService.clearValidityOverride("mitm.example.com", 8443, {});
    run_next_test();
  });
  add_cert_override_test("mitm.example.com", MOZILLA_PKIX_ERROR_MITM_DETECTED);
  add_test(function () {
    Services.prefs.setStringPref(
      "security.pki.mitm_canary_issuer",
      "CN=Other MITM Root"
    );
    let certOverrideService = Cc[
      "@mozilla.org/security/certoverride;1"
    ].getService(Ci.nsICertOverrideService);
    certOverrideService.clearValidityOverride("mitm.example.com", 8443, {});
    run_next_test();
  });
  // If the canary issuer doesn't match the one we see, we exepct and unknown
  // issuer error.
  add_cert_override_test("mitm.example.com", SEC_ERROR_UNKNOWN_ISSUER);
  // If security.pki.mitm_canary_issuer.enabled is false, there should always
  // be an unknown issuer error.
  add_test(function () {
    Services.prefs.setBoolPref(
      "security.pki.mitm_canary_issuer.enabled",
      false
    );
    let certOverrideService = Cc[
      "@mozilla.org/security/certoverride;1"
    ].getService(Ci.nsICertOverrideService);
    certOverrideService.clearValidityOverride("mitm.example.com", 8443, {});
    run_next_test();
  });
  add_cert_override_test("mitm.example.com", SEC_ERROR_UNKNOWN_ISSUER);
  add_test(function () {
    Services.prefs.clearUserPref("security.pki.mitm_canary_issuer");
    run_next_test();
  });

  // This is intended to test the case where a verification has failed for one
  // overridable reason (e.g. unknown issuer) but then, in the process of
  // reporting that error, a non-overridable error is encountered. The
  // non-overridable error should be prioritized.
  add_test(function () {
    let rootCert = constructCertFromFile("bad_certs/test-ca.pem");
    setCertTrust(rootCert, ",,");
    run_next_test();
  });
  add_prevented_cert_override_test(
    "nsCertTypeCritical.example.com",
    SEC_ERROR_UNKNOWN_CRITICAL_EXTENSION
  );
  add_test(function () {
    let rootCert = constructCertFromFile("bad_certs/test-ca.pem");
    setCertTrust(rootCert, "CTu,,");
    run_next_test();
  });

  // Bug 990603: Apache documentation has recommended generating a self-signed
  // test certificate with basic constraints: CA:true. For compatibility, this
  // is a scenario in which an override is allowed.
  add_cert_override_test(
    "self-signed-end-entity-with-cA-true.example.com",
    MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT
  );

  add_cert_override_test(
    "ca-used-as-end-entity.example.com",
    MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY
  );

  // If an X.509 version 1 certificate is not a trust anchor, we will
  // encounter an overridable error.
  add_cert_override_test(
    "end-entity-issued-by-v1-cert.example.com",
    MOZILLA_PKIX_ERROR_V1_CERT_USED_AS_CA
  );
  // If we make that certificate a trust anchor, the connection will succeed.
  add_test(function () {
    let certOverrideService = Cc[
      "@mozilla.org/security/certoverride;1"
    ].getService(Ci.nsICertOverrideService);
    certOverrideService.clearValidityOverride(
      "end-entity-issued-by-v1-cert.example.com",
      8443,
      {}
    );
    let v1Cert = constructCertFromFile("bad_certs/v1Cert.pem");
    setCertTrust(v1Cert, "CTu,,");
    clearSessionCache();
    run_next_test();
  });
  add_connection_test(
    "end-entity-issued-by-v1-cert.example.com",
    PRErrorCodeSuccess
  );
  // Reset the trust for that certificate.
  add_test(function () {
    let v1Cert = constructCertFromFile("bad_certs/v1Cert.pem");
    setCertTrust(v1Cert, ",,");
    clearSessionCache();
    run_next_test();
  });

  // Due to compatibility issues, we allow overrides for certificates issued by
  // certificates that are not valid CAs.
  add_cert_override_test(
    "end-entity-issued-by-non-CA.example.com",
    SEC_ERROR_CA_CERT_INVALID
  );

  // This host presents a 1016-bit RSA key.
  add_cert_override_test(
    "inadequate-key-size-ee.example.com",
    MOZILLA_PKIX_ERROR_INADEQUATE_KEY_SIZE
  );

  // The test root is not a built-in (by default), so the invalid dNSName entry
  // in the subject alternative name extension is skipped.
  add_connection_test(
    "ipAddressAsDNSNameInSAN.example.com",
    PRErrorCodeSuccess
  );

  if (gIsDebugBuild) {
    // Treat the test root like a built-in.
    add_test(function () {
      let rootCert = constructCertFromFile("bad_certs/test-ca.pem");
      Services.prefs.setCharPref(
        "security.test.built_in_root_hash",
        rootCert.sha256Fingerprint
      );
      run_next_test();
    });
    // If the root is a built-in, the invalid dNSName entry in the subject
    // alternative name extension is not skipped, and this result in an error.
    add_cert_override_test(
      "ipAddressAsDNSNameInSAN.example.com",
      SSL_ERROR_BAD_CERT_DOMAIN
    );
    // Reset the test root's built-in status.
    add_test(function () {
      Services.prefs.clearUserPref("security.test.built_in_root_hash");
      run_next_test();
    });
  }

  add_cert_override_test("noValidNames.example.com", SSL_ERROR_BAD_CERT_DOMAIN);
  add_cert_override_test(
    "badSubjectAltNames.example.com",
    SSL_ERROR_BAD_CERT_DOMAIN
  );

  add_cert_override_test(
    "bug413909.xn--hxajbheg2az3al.xn--jxalpdlp",
    SEC_ERROR_UNKNOWN_ISSUER
  );
  add_test(function () {
    // At this point, the override for bug413909.xn--hxajbheg2az3al.xn--jxalpdlp
    // is still valid. Do some additional tests relating to IDN handling.
    let certOverrideService = Cc[
      "@mozilla.org/security/certoverride;1"
    ].getService(Ci.nsICertOverrideService);
    let uri = Services.io.newURI(
      "https://bug413909.xn--hxajbheg2az3al.xn--jxalpdlp"
    );
    let cert = constructCertFromFile("bad_certs/idn-certificate.pem");
    Assert.ok(
      certOverrideService.hasMatchingOverride(
        uri.asciiHost,
        8443,
        {},
        cert,
        {}
      ),
      "IDN certificate should have matching override using ascii host"
    );
    Assert.throws(
      () =>
        !certOverrideService.hasMatchingOverride(
          uri.displayHost,
          8443,
          {},
          cert,
          {}
        ),
      /NS_ERROR_ILLEGAL_VALUE/,
      "IDN certificate should not have matching override using (non-ascii) host"
    );
    let invalidHost = uri.asciiHost.replace(/./g, c =>
      String.fromCharCode(c.charCodeAt(0) | 0x100)
    );
    Assert.throws(
      () =>
        !certOverrideService.hasMatchingOverride(
          invalidHost,
          8443,
          {},
          cert,
          {}
        ),
      /NS_ERROR_ILLEGAL_VALUE/,
      "hasMatchingOverride should not truncate high-bytes"
    );
    run_next_test();
  });

  add_test(function () {
    // Add a bunch of overrides...
    let certOverrideService = Cc[
      "@mozilla.org/security/certoverride;1"
    ].getService(Ci.nsICertOverrideService);
    let cert = constructCertFromFile("bad_certs/default-ee.pem");
    certOverrideService.rememberValidityOverride(
      "example.com",
      443,
      {},
      cert,
      false
    );
    Assert.ok(
      certOverrideService.hasMatchingOverride("example.com", 443, {}, cert, {}),
      "Should have added override for example.com:443"
    );
    certOverrideService.rememberValidityOverride(
      "example.com",
      80,
      {},
      cert,
      false
    );
    certOverrideService.rememberValidityOverride("::1", 80, {}, cert, false);
    Assert.ok(
      certOverrideService.hasMatchingOverride("example.com", 80, {}, cert, {}),
      "Should have added override for example.com:80"
    );
    certOverrideService.rememberValidityOverride(
      "example.org",
      443,
      {},
      cert,
      false
    );
    Assert.ok(
      certOverrideService.hasMatchingOverride("example.org", 443, {}, cert, {}),
      "Should have added override for example.org:443"
    );
    Assert.ok(
      certOverrideService.hasMatchingOverride("::1", 80, {}, cert, {}),
      "Should have added override for [::1]:80"
    );
    // When in a private browsing context, overrides added in non-private
    // contexts should match (but not vice-versa).
    Assert.ok(
      certOverrideService.hasMatchingOverride(
        "example.org",
        443,
        { privateBrowsingId: 1 },
        cert,
        {}
      ),
      "Should have override for example.org:443 with privateBrowsingId 1"
    );
    Assert.ok(
      certOverrideService.hasMatchingOverride(
        "example.org",
        443,
        { privateBrowsingId: 2 },
        cert,
        {}
      ),
      "Should have override for example.org:443 with privateBrowsingId 2"
    );
    Assert.ok(
      certOverrideService.hasMatchingOverride(
        "example.org",
        443,
        { firstPartyDomain: "example.org", userContextId: 1 },
        cert,
        {}
      ),
      "Should ignore firstPartyDomain and userContextId when checking overrides"
    );
    certOverrideService.rememberValidityOverride(
      "example.org",
      80,
      {},
      cert,
      true
    );
    Assert.ok(
      certOverrideService.hasMatchingOverride("example.org", 80, {}, cert, {}),
      "Should have added override for example.org:80"
    );
    certOverrideService.rememberValidityOverride(
      "test.example.org",
      443,
      { firstPartyDomain: "example.org", userContextId: 1 },
      cert,
      false
    );
    Assert.ok(
      certOverrideService.hasMatchingOverride(
        "test.example.org",
        443,
        {},
        cert,
        {}
      ),
      "Should ignore firstPartyDomain and userContextId when adding overrides"
    );
    Assert.ok(
      certOverrideService.hasMatchingOverride(
        "test.example.org",
        443,
        { firstPartyDomain: "example.com", userContextId: 2 },
        cert,
        {}
      ),
      "Should ignore firstPartyDomain and userContextId when checking overrides"
    );
    certOverrideService.rememberValidityOverride(
      "example.test",
      443,
      { privateBrowsingId: 1 },
      cert,
      false
    );
    Assert.ok(
      certOverrideService.hasMatchingOverride(
        "example.test",
        443,
        { privateBrowsingId: 1 },
        cert,
        {}
      ),
      "Should have added override for example.test:443 with privateBrowsingId 1"
    );
    Assert.ok(
      !certOverrideService.hasMatchingOverride(
        "example.test",
        443,
        { privateBrowsingId: 2 },
        cert,
        {}
      ),
      "Should not have override for example.test:443 with privateBrowsingId 2"
    );
    Assert.ok(
      !certOverrideService.hasMatchingOverride(
        "example.test",
        443,
        {},
        cert,
        {}
      ),
      "Should not have override for example.test:443 with non-private OriginAttributes"
    );
    // Clear them all...
    certOverrideService.clearAllOverrides();

    // And ensure they're all gone.
    Assert.ok(
      !certOverrideService.hasMatchingOverride(
        "example.com",
        443,
        {},
        cert,
        {}
      ),
      "Should have removed override for example.com:443"
    );
    Assert.ok(
      !certOverrideService.hasMatchingOverride("example.com", 80, {}, cert, {}),
      "Should have removed override for example.com:80"
    );
    Assert.ok(
      !certOverrideService.hasMatchingOverride(
        "example.org",
        443,
        {},
        cert,
        {}
      ),
      "Should have removed override for example.org:443"
    );
    Assert.ok(
      !certOverrideService.hasMatchingOverride("example.org", 80, {}, cert, {}),
      "Should have removed override for example.org:80"
    );
    Assert.ok(
      !certOverrideService.hasMatchingOverride(
        "example.org",
        443,
        { privateBrowsingId: 1 },
        cert,
        {}
      ),
      "Should have removed override for example.org:443 with privateBrowsingId 1"
    );

    run_next_test();
  });
}

function add_localhost_tests() {
  add_cert_override_test("localhost", SEC_ERROR_UNKNOWN_ISSUER);
  add_cert_override_test("127.0.0.1", SSL_ERROR_BAD_CERT_DOMAIN);
  add_cert_override_test("::1", SSL_ERROR_BAD_CERT_DOMAIN);
}

function add_combo_tests() {
  add_cert_override_test(
    "mismatch-expired.example.com",
    SSL_ERROR_BAD_CERT_DOMAIN
  );
  add_cert_override_test(
    "mismatch-notYetValid.example.com",
    SSL_ERROR_BAD_CERT_DOMAIN
  );
  add_cert_override_test(
    "mismatch-untrusted.example.com",
    SEC_ERROR_UNKNOWN_ISSUER
  );
  add_cert_override_test(
    "untrusted-expired.example.com",
    SEC_ERROR_UNKNOWN_ISSUER
  );
  add_cert_override_test(
    "mismatch-untrusted-expired.example.com",
    SEC_ERROR_UNKNOWN_ISSUER
  );

  add_cert_override_test(
    "md5signature-expired.example.com",
    SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED
  );

  add_cert_override_test(
    "ca-used-as-end-entity-name-mismatch.example.com",
    MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY
  );
}

function add_distrust_tests() {
  // Before we specifically distrust this certificate, it should be trusted.
  add_connection_test("untrusted.example.com", PRErrorCodeSuccess);

  add_distrust_test(
    "bad_certs/default-ee.pem",
    "untrusted.example.com",
    SEC_ERROR_UNTRUSTED_CERT
  );

  add_distrust_test(
    "bad_certs/other-test-ca.pem",
    "untrustedissuer.example.com",
    SEC_ERROR_UNTRUSTED_ISSUER
  );

  add_distrust_test(
    "bad_certs/test-ca.pem",
    "ca-used-as-end-entity.example.com",
    SEC_ERROR_UNTRUSTED_ISSUER
  );
}

function add_distrust_test(certFileName, hostName, expectedResult) {
  let certToDistrust = constructCertFromFile(certFileName);

  add_test(function () {
    // Add an entry to the NSS certDB that says to distrust the cert
    setCertTrust(certToDistrust, "pu,,");
    clearSessionCache();
    run_next_test();
  });
  add_prevented_cert_override_test(hostName, expectedResult);
  add_test(function () {
    setCertTrust(certToDistrust, "u,,");
    run_next_test();
  });
}
