// -*- 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 that end-entity certificates that should successfully verify as EV
// (Extended Validation) do so and that end-entity certificates that should not
// successfully verify as EV do not. Also tests related situations (e.g. that
// failure to fetch an OCSP response results in no EV treatment).
//
// A quick note about the certificates in these tests: generally, an EV
// certificate chain will have an end-entity with a specific policy OID followed
// by an intermediate with the anyPolicy OID chaining to a root with no policy
// OID (since it's a trust anchor, it can be omitted). In these tests, the
// specific policy OID is 1.3.6.1.4.1.13769.666.666.666.1.500.9.1 and is
// referred to as the test OID. In order to reflect what will commonly be
// encountered, the end-entity of any given test path will have the test OID
// unless otherwise specified in the name of the test path. Similarly, the
// intermediate will have the anyPolicy OID, again unless otherwise specified.
// For example, for the path where the end-entity does not have an OCSP URI
// (referred to as "no-ocsp-ee-path-{ee,int}", the end-entity has the test OID
// whereas the intermediate has the anyPolicy OID.
// For another example, for the test OID path ("test-oid-path-{ee,int}"), both
// the end-entity and the intermediate have the test OID.

do_get_profile(); // must be called before getting nsIX509CertDB
const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
  Ci.nsIX509CertDB
);

registerCleanupFunction(() => {
  Services.prefs.clearUserPref("network.dns.localDomains");
  Services.prefs.clearUserPref("security.OCSP.enabled");
});

Services.prefs.setCharPref("network.dns.localDomains", "www.example.com");
Services.prefs.setIntPref("security.OCSP.enabled", 1);
const evroot = addCertFromFile(certdb, "test_ev_certs/evroot.pem", "CTu,,");
addCertFromFile(certdb, "test_ev_certs/non-evroot-ca.pem", "CTu,,");

const SERVER_PORT = 8888;

function failingOCSPResponder() {
  return getFailingHttpServer(SERVER_PORT, ["www.example.com"]);
}

class EVCertVerificationResult {
  constructor(
    testcase,
    expectedPRErrorCode,
    expectedEV,
    resolve,
    ocspResponder
  ) {
    this.testcase = testcase;
    this.expectedPRErrorCode = expectedPRErrorCode;
    this.expectedEV = expectedEV;
    this.resolve = resolve;
    this.ocspResponder = ocspResponder;
  }

  verifyCertFinished(prErrorCode, verifiedChain, hasEVPolicy) {
    equal(
      prErrorCode,
      this.expectedPRErrorCode,
      `${this.testcase} should have expected error code`
    );
    equal(
      hasEVPolicy,
      this.expectedEV,
      `${this.testcase} should result in expected EV status`
    );
    this.ocspResponder.stop(this.resolve);
  }
}

function asyncTestEV(
  cert,
  expectedPRErrorCode,
  expectedEV,
  expectedOCSPRequestPaths,
  ocspResponseTypes = undefined
) {
  let now = Date.now() / 1000;
  return new Promise(resolve => {
    let ocspResponder = expectedOCSPRequestPaths.length
      ? startOCSPResponder(
          SERVER_PORT,
          "www.example.com",
          "test_ev_certs",
          expectedOCSPRequestPaths,
          expectedOCSPRequestPaths.slice(),
          null,
          ocspResponseTypes
        )
      : failingOCSPResponder();
    let result = new EVCertVerificationResult(
      cert.subjectName,
      expectedPRErrorCode,
      expectedEV,
      resolve,
      ocspResponder
    );
    certdb.asyncVerifyCertAtTime(
      cert,
      certificateUsageSSLServer,
      0,
      "ev-test.example.com",
      now,
      result
    );
  });
}

function ensureVerifiesAsEV(testcase) {
  let cert = constructCertFromFile(`test_ev_certs/${testcase}-ee.pem`);
  addCertFromFile(certdb, `test_ev_certs/${testcase}-int.pem`, ",,");
  let expectedOCSPRequestPaths = [`${testcase}-ee`];
  return asyncTestEV(
    cert,
    PRErrorCodeSuccess,
    gEVExpected,
    expectedOCSPRequestPaths
  );
}

function ensureVerifiesAsEVWithNoOCSPRequests(testcase) {
  let cert = constructCertFromFile(`test_ev_certs/${testcase}-ee.pem`);
  addCertFromFile(certdb, `test_ev_certs/${testcase}-int.pem`, ",,");
  return asyncTestEV(cert, PRErrorCodeSuccess, gEVExpected, []);
}

function ensureVerifiesAsDV(testcase, expectedOCSPRequestPaths = undefined) {
  let cert = constructCertFromFile(`test_ev_certs/${testcase}-ee.pem`);
  addCertFromFile(certdb, `test_ev_certs/${testcase}-int.pem`, ",,");
  return asyncTestEV(
    cert,
    PRErrorCodeSuccess,
    false,
    expectedOCSPRequestPaths ? expectedOCSPRequestPaths : [`${testcase}-ee`]
  );
}

function ensureVerificationFails(testcase, expectedPRErrorCode) {
  let cert = constructCertFromFile(`test_ev_certs/${testcase}-ee.pem`);
  addCertFromFile(certdb, `test_ev_certs/${testcase}-int.pem`, ",,");
  return asyncTestEV(cert, expectedPRErrorCode, false, []);
}

function verifyWithFlags_LOCAL_ONLY_and_MUST_BE_EV(testcase, expectSuccess) {
  let cert = constructCertFromFile(`test_ev_certs/${testcase}-ee.pem`);
  addCertFromFile(certdb, `test_ev_certs/${testcase}-int.pem`, ",,");
  let now = Date.now() / 1000;
  let expectedErrorCode = SEC_ERROR_POLICY_VALIDATION_FAILED;
  if (expectSuccess && gEVExpected) {
    expectedErrorCode = PRErrorCodeSuccess;
  }
  return new Promise(resolve => {
    let ocspResponder = failingOCSPResponder();
    let result = new EVCertVerificationResult(
      cert.subjectName,
      expectedErrorCode,
      expectSuccess && gEVExpected,
      resolve,
      ocspResponder
    );
    let flags =
      Ci.nsIX509CertDB.FLAG_LOCAL_ONLY | Ci.nsIX509CertDB.FLAG_MUST_BE_EV;
    certdb.asyncVerifyCertAtTime(
      cert,
      certificateUsageSSLServer,
      flags,
      "ev-test.example.com",
      now,
      result
    );
  });
}

function ensureNoOCSPMeansNoEV(testcase) {
  return verifyWithFlags_LOCAL_ONLY_and_MUST_BE_EV(testcase, false);
}

function ensureVerifiesAsEVWithFLAG_LOCAL_ONLY(testcase) {
  return verifyWithFlags_LOCAL_ONLY_and_MUST_BE_EV(testcase, true);
}

function verifyWithOCSPResponseType(testcase, response, expectEV) {
  let cert = constructCertFromFile(`test_ev_certs/${testcase}-ee.pem`);
  addCertFromFile(certdb, `test_ev_certs/${testcase}-int.pem`, ",,");
  let expectedOCSPRequestPaths = [`${testcase}-ee`];
  let ocspResponseTypes = [response];
  return asyncTestEV(
    cert,
    PRErrorCodeSuccess,
    gEVExpected && expectEV,
    expectedOCSPRequestPaths,
    ocspResponseTypes
  );
}

function ensureVerifiesAsDVWithOldEndEntityOCSPResponse(testcase) {
  return verifyWithOCSPResponseType(testcase, "longvalidityalmostold", false);
}

function ensureVerifiesAsDVWithVeryOldEndEntityOCSPResponse(testcase) {
  return verifyWithOCSPResponseType(testcase, "ancientstillvalid", false);
}

// These should all verify as EV.
add_task(async function plainExpectSuccessEVTests() {
  await ensureVerifiesAsEV("anyPolicy-int-path");
  await ensureVerifiesAsEV("test-oid-path");
  await ensureVerifiesAsEV("cabforum-oid-path");
  await ensureVerifiesAsEV("cabforum-and-test-oid-ee-path");
  await ensureVerifiesAsEV("test-and-cabforum-oid-ee-path");
  await ensureVerifiesAsEV("reverse-order-oids-path");
  // In this case, the end-entity has both the CA/B Forum OID and the test OID
  // (in that order). The intermediate has the CA/B Forum OID. Since the
  // implementation tries all EV policies it encounters, this successfully
  // verifies as EV.
  await ensureVerifiesAsEV("cabforum-and-test-oid-ee-cabforum-oid-int-path");
  // In this case, the end-entity has both the test OID and the CA/B Forum OID
  // (in that order). The intermediate has only the CA/B Forum OID. Since the
  // implementation tries all EV policies it encounters, this successfully
  // verifies as EV.
  await ensureVerifiesAsEV("test-and-cabforum-oid-ee-cabforum-oid-int-path");
});

// These fail for various reasons to verify as EV, but fallback to DV should
// succeed.
add_task(async function expectDVFallbackTests() {
  await ensureVerifiesAsDV("anyPolicy-ee-path");
  await ensureVerifiesAsDV("non-ev-root-path");
  await ensureVerifiesAsDV("no-ocsp-ee-path", []);
  await ensureVerifiesAsEV("no-ocsp-int-path");
  // In this case, the end-entity has the test OID and the intermediate has the
  // CA/B Forum OID. Since the CA/B Forum OID is not treated the same as the
  // anyPolicy OID, this will not verify as EV.
  await ensureVerifiesAsDV("test-oid-ee-cabforum-oid-int-path");
});

// Test that removing the trust bits from an EV root causes verifications
// relying on that root to fail (and then test that adding back the trust bits
// causes the verifications to succeed again).
add_task(async function evRootTrustTests() {
  clearOCSPCache();
  info("untrusting evroot");
  certdb.setCertTrust(
    evroot,
    Ci.nsIX509Cert.CA_CERT,
    Ci.nsIX509CertDB.UNTRUSTED
  );
  await ensureVerificationFails("test-oid-path", SEC_ERROR_UNKNOWN_ISSUER);
  info("re-trusting evroot");
  certdb.setCertTrust(
    evroot,
    Ci.nsIX509Cert.CA_CERT,
    Ci.nsIX509CertDB.TRUSTED_SSL
  );
  await ensureVerifiesAsEV("test-oid-path");
});

// Test that if FLAG_LOCAL_ONLY and FLAG_MUST_BE_EV are specified, that no OCSP
// requests are made (this also means that nothing will verify as EV).
add_task(async function localOnlyMustBeEVTests() {
  clearOCSPCache();
  await ensureNoOCSPMeansNoEV("anyPolicy-ee-path");
  await ensureNoOCSPMeansNoEV("anyPolicy-int-path");
  await ensureNoOCSPMeansNoEV("non-ev-root-path");
  await ensureNoOCSPMeansNoEV("no-ocsp-ee-path");
  await ensureNoOCSPMeansNoEV("no-ocsp-int-path");
  await ensureNoOCSPMeansNoEV("test-oid-path");
});

// Prime the OCSP cache and then ensure that we can validate certificates as EV
// without hitting the network. There's two cases here: one where we simply
// validate like normal and then check that the network was never accessed and
// another where we use flags to mandate that the network not be used.
add_task(async function ocspCachingTests() {
  clearOCSPCache();

  await ensureVerifiesAsEV("anyPolicy-int-path");
  await ensureVerifiesAsEV("test-oid-path");

  await ensureVerifiesAsEVWithNoOCSPRequests("anyPolicy-int-path");
  await ensureVerifiesAsEVWithNoOCSPRequests("test-oid-path");

  await ensureVerifiesAsEVWithFLAG_LOCAL_ONLY("anyPolicy-int-path");
  await ensureVerifiesAsEVWithFLAG_LOCAL_ONLY("test-oid-path");
});

// Old-but-still-valid OCSP responses are accepted for intermediates but not
// end-entity certificates (because of OCSP soft-fail this results in DV
// fallback).
add_task(async function oldOCSPResponseTests() {
  clearOCSPCache();

  clearOCSPCache();
  await ensureVerifiesAsDVWithOldEndEntityOCSPResponse("anyPolicy-int-path");
  await ensureVerifiesAsDVWithOldEndEntityOCSPResponse("test-oid-path");

  clearOCSPCache();
  await ensureVerifiesAsDVWithVeryOldEndEntityOCSPResponse(
    "anyPolicy-int-path"
  );
  await ensureVerifiesAsDVWithVeryOldEndEntityOCSPResponse("test-oid-path");
});
