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

// In which we connect to a number of domains (as faked by a server running
// locally) with OCSP stapling enabled to determine that good things happen
// and bad things don't, specifically with respect to various expired OCSP
// responses (stapled and otherwise).
// According to RFC 6066, if a stapled OCSP response can't be satisfactorilly
// verified, the client should terminate the connection. Unfortunately, due to
// some bugs where servers will staple any old garbage without verifying it, we
// can't be this strict in practice. Originally this caveat only applied to
// expired responses, but recent high-profile failures have caused us to expand
// this to "try later" responses and responses where the signing certificate
// doesn't verify successfully.

var gCurrentOCSPResponse = null;
var gOCSPRequestCount = 0;

function add_ocsp_test(
  aHost,
  aExpectedResult,
  aOCSPResponseToServe,
  aExpectedRequestCount
) {
  add_connection_test(
    aHost,
    aExpectedResult,
    function () {
      clearOCSPCache();
      clearSessionCache();
      gCurrentOCSPResponse = aOCSPResponseToServe;
      gOCSPRequestCount = 0;
    },
    function () {
      equal(
        gOCSPRequestCount,
        aExpectedRequestCount,
        "Should have made " +
          aExpectedRequestCount +
          " fallback OCSP request" +
          (aExpectedRequestCount == 1 ? "" : "s")
      );
    }
  );
}

do_get_profile();
Services.prefs.setBoolPref("security.ssl.enable_ocsp_stapling", true);
Services.prefs.setIntPref("security.OCSP.enabled", 1);
// Sometimes this test will fail on android due to an OCSP request timing out.
// That aspect of OCSP requests is not what we're testing here, so we can just
// bump the timeout and hopefully avoid these failures.
Services.prefs.setIntPref("security.OCSP.timeoutMilliseconds.soft", 5000);
var args = [
  ["good", "default-ee", "unused", 0],
  ["expiredresponse", "default-ee", "unused", 0],
  ["oldvalidperiod", "default-ee", "unused", 0],
  ["revoked", "default-ee", "unused", 0],
  ["unknown", "default-ee", "unused", 0],
  ["good", "must-staple-ee", "unused", 0],
];
var ocspResponses = generateOCSPResponses(args, "ocsp_certs");
// Fresh response, certificate is good.
var ocspResponseGood = ocspResponses[0];
// Expired response, certificate is good.
var expiredOCSPResponseGood = ocspResponses[1];
// Fresh signature, old validity period, certificate is good.
var oldValidityPeriodOCSPResponseGood = ocspResponses[2];
// Fresh signature, certificate is revoked.
var ocspResponseRevoked = ocspResponses[3];
// Fresh signature, certificate is unknown.
var ocspResponseUnknown = ocspResponses[4];
var ocspResponseGoodMustStaple = ocspResponses[5];

// sometimes we expect a result without re-fetch
var willNotRetry = 1;
// but sometimes, since a bad response is in the cache, OCSP fetch will be
// attempted for each validation - in practice, for these test certs, this
// means 2 requests because various key sizes are tried.
var willRetry = 2;

function run_test() {
  let ocspResponder = new HttpServer();
  ocspResponder.registerPrefixHandler("/", function (request, response) {
    if (gCurrentOCSPResponse) {
      response.setStatusLine(request.httpVersion, 200, "OK");
      response.setHeader("Content-Type", "application/ocsp-response");
      response.write(gCurrentOCSPResponse);
    } else {
      response.setStatusLine(request.httpVersion, 500, "Internal Server Error");
      response.write("Internal Server Error");
    }
    gOCSPRequestCount++;
  });
  ocspResponder.start(8888);
  add_tls_server_setup("OCSPStaplingServer", "ocsp_certs");

  // In these tests, the OCSP stapling server gives us a stapled
  // response based on the host name ("ocsp-stapling-expired" or
  // "ocsp-stapling-expired-fresh-ca"). We then ensure that we're
  // properly falling back to fetching revocation information.
  // For ocsp-stapling-expired.example.com, the OCSP stapling server
  // staples an expired OCSP response. The certificate has not expired.
  // For ocsp-stapling-expired-fresh-ca.example.com, the OCSP stapling
  // server staples an OCSP response with a recent signature but with an
  // out-of-date validity period. The certificate has not expired.
  add_ocsp_test(
    "ocsp-stapling-expired.example.com",
    PRErrorCodeSuccess,
    ocspResponseGood,
    willNotRetry
  );
  add_ocsp_test(
    "ocsp-stapling-expired-fresh-ca.example.com",
    PRErrorCodeSuccess,
    ocspResponseGood,
    willNotRetry
  );
  // if we can't fetch a more recent response when
  // given an expired stapled response, we terminate the connection.
  add_ocsp_test(
    "ocsp-stapling-expired.example.com",
    SEC_ERROR_OCSP_OLD_RESPONSE,
    expiredOCSPResponseGood,
    willRetry
  );
  add_ocsp_test(
    "ocsp-stapling-expired-fresh-ca.example.com",
    SEC_ERROR_OCSP_OLD_RESPONSE,
    expiredOCSPResponseGood,
    willRetry
  );
  add_ocsp_test(
    "ocsp-stapling-expired.example.com",
    SEC_ERROR_OCSP_OLD_RESPONSE,
    oldValidityPeriodOCSPResponseGood,
    willRetry
  );
  add_ocsp_test(
    "ocsp-stapling-expired-fresh-ca.example.com",
    SEC_ERROR_OCSP_OLD_RESPONSE,
    oldValidityPeriodOCSPResponseGood,
    willRetry
  );
  add_ocsp_test(
    "ocsp-stapling-expired.example.com",
    SEC_ERROR_OCSP_OLD_RESPONSE,
    null,
    willNotRetry
  );
  add_ocsp_test(
    "ocsp-stapling-expired.example.com",
    SEC_ERROR_OCSP_OLD_RESPONSE,
    null,
    willNotRetry
  );
  // Of course, if the newer response indicates Revoked or Unknown,
  // that status must be returned.
  add_ocsp_test(
    "ocsp-stapling-expired.example.com",
    SEC_ERROR_REVOKED_CERTIFICATE,
    ocspResponseRevoked,
    willNotRetry
  );
  add_ocsp_test(
    "ocsp-stapling-expired-fresh-ca.example.com",
    SEC_ERROR_REVOKED_CERTIFICATE,
    ocspResponseRevoked,
    willNotRetry
  );
  add_ocsp_test(
    "ocsp-stapling-expired.example.com",
    SEC_ERROR_OCSP_UNKNOWN_CERT,
    ocspResponseUnknown,
    willRetry
  );
  add_ocsp_test(
    "ocsp-stapling-expired-fresh-ca.example.com",
    SEC_ERROR_OCSP_UNKNOWN_CERT,
    ocspResponseUnknown,
    willRetry
  );

  // If the response is expired but indicates Revoked or Unknown and a
  // newer status can't be fetched, the Revoked or Unknown status will
  // be returned.
  add_ocsp_test(
    "ocsp-stapling-revoked-old.example.com",
    SEC_ERROR_REVOKED_CERTIFICATE,
    null,
    willNotRetry
  );
  add_ocsp_test(
    "ocsp-stapling-unknown-old.example.com",
    SEC_ERROR_OCSP_UNKNOWN_CERT,
    null,
    willNotRetry
  );
  // If the response is expired but indicates Revoked or Unknown and
  // a newer status can be fetched and successfully verified, this
  // should result in a successful certificate verification.
  add_ocsp_test(
    "ocsp-stapling-revoked-old.example.com",
    PRErrorCodeSuccess,
    ocspResponseGood,
    willNotRetry
  );
  add_ocsp_test(
    "ocsp-stapling-unknown-old.example.com",
    PRErrorCodeSuccess,
    ocspResponseGood,
    willNotRetry
  );
  // If a newer status can be fetched but it fails to verify, the
  // Revoked or Unknown status of the expired stapled response
  // should be returned.
  add_ocsp_test(
    "ocsp-stapling-revoked-old.example.com",
    SEC_ERROR_REVOKED_CERTIFICATE,
    expiredOCSPResponseGood,
    willRetry
  );
  add_ocsp_test(
    "ocsp-stapling-unknown-old.example.com",
    SEC_ERROR_OCSP_UNKNOWN_CERT,
    expiredOCSPResponseGood,
    willRetry
  );

  // These tests are verifying that an valid but very old response
  // is rejected as a valid stapled response, requiring a fetch
  // from the ocsp responder.
  add_ocsp_test(
    "ocsp-stapling-ancient-valid.example.com",
    PRErrorCodeSuccess,
    ocspResponseGood,
    willNotRetry
  );
  add_ocsp_test(
    "ocsp-stapling-ancient-valid.example.com",
    SEC_ERROR_REVOKED_CERTIFICATE,
    ocspResponseRevoked,
    willNotRetry
  );
  add_ocsp_test(
    "ocsp-stapling-ancient-valid.example.com",
    SEC_ERROR_OCSP_UNKNOWN_CERT,
    ocspResponseUnknown,
    willRetry
  );

  // Test how OCSP-must-staple (i.e. TLS feature) interacts with stapled OCSP
  // responses that don't successfully verify.
  // A strict reading of the relevant RFCs might say that these connections
  // should all fail because a satisfactory stapled OCSP response is not
  // present, but for compatibility reasons we fall back to active OCSP fetching
  // in these situations. If the fetch succeeds, then connection succeeds.
  add_ocsp_test(
    "ocsp-stapling-must-staple-expired.example.com",
    PRErrorCodeSuccess,
    ocspResponseGoodMustStaple,
    willNotRetry
  );
  add_ocsp_test(
    "ocsp-stapling-must-staple-try-later.example.com",
    PRErrorCodeSuccess,
    ocspResponseGoodMustStaple,
    willNotRetry
  );
  add_ocsp_test(
    "ocsp-stapling-must-staple-invalid-signer.example.com",
    PRErrorCodeSuccess,
    ocspResponseGoodMustStaple,
    willNotRetry
  );

  add_test(function () {
    ocspResponder.stop(run_next_test);
  });
  add_test(check_ocsp_stapling_telemetry);
  run_next_test();
}

function check_ocsp_stapling_telemetry() {
  let histogram = Services.telemetry
    .getHistogramById("SSL_OCSP_STAPLING")
    .snapshot();
  equal(
    histogram.values[0] || 0,
    0,
    "Should have 0 connections for unused histogram bucket 0"
  );
  equal(
    histogram.values[1] || 0,
    0,
    "Actual and expected connections with a good response should match"
  );
  equal(
    histogram.values[2] || 0,
    0,
    "Actual and expected connections with no stapled response should match"
  );
  equal(
    histogram.values[3],
    22,
    "Actual and expected connections with an expired response should match"
  );
  equal(
    histogram.values[4],
    2,
    "Actual and expected connections with bad responses should match"
  );
  run_next_test();
}
