// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/cert/internal/ocsp.h"

#include "base/base64.h"
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "build/build_config.h"
#include "net/cert/internal/test_helpers.h"
#include "net/der/encode_values.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/boringssl/src/include/openssl/pool.h"
#include "url/gurl.h"

namespace net {

namespace {

const base::TimeDelta kOCSPAgeOneWeek = base::TimeDelta::FromDays(7);

std::string GetFilePath(const std::string& file_name) {
  return std::string("net/data/ocsp_unittest/") + file_name;
}

scoped_refptr<ParsedCertificate> ParseCertificate(base::StringPiece data) {
  CertErrors errors;
  return ParsedCertificate::Create(
      bssl::UniquePtr<CRYPTO_BUFFER>(CRYPTO_BUFFER_new(
          reinterpret_cast<const uint8_t*>(data.data()), data.size(), nullptr)),
      {}, &errors);
}

struct TestParams {
  const char* file_name;
  OCSPRevocationStatus expected_revocation_status;
  OCSPVerifyResult::ResponseStatus expected_response_status;
};

class CheckOCSPTest : public ::testing::TestWithParam<TestParams> {};

const TestParams kTestParams[] = {
    {"good_response.pem", OCSPRevocationStatus::GOOD,
     OCSPVerifyResult::PROVIDED},

    {"good_response_sha256.pem", OCSPRevocationStatus::GOOD,
     OCSPVerifyResult::PROVIDED},

    {"no_response.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::NO_MATCHING_RESPONSE},

    {"malformed_request.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::ERROR_RESPONSE},

    {"bad_status.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::PARSE_RESPONSE_ERROR},

    {"bad_ocsp_type.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::PARSE_RESPONSE_ERROR},

    {"bad_signature.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::PROVIDED},

    {"ocsp_sign_direct.pem", OCSPRevocationStatus::GOOD,
     OCSPVerifyResult::PROVIDED},

    {"ocsp_sign_indirect.pem", OCSPRevocationStatus::GOOD,
     OCSPVerifyResult::PROVIDED},

    {"ocsp_sign_indirect_missing.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::PROVIDED},

    {"ocsp_sign_bad_indirect.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::PROVIDED},

    {"ocsp_extra_certs.pem", OCSPRevocationStatus::GOOD,
     OCSPVerifyResult::PROVIDED},

    {"has_version.pem", OCSPRevocationStatus::GOOD, OCSPVerifyResult::PROVIDED},

    {"responder_name.pem", OCSPRevocationStatus::GOOD,
     OCSPVerifyResult::PROVIDED},

    {"responder_id.pem", OCSPRevocationStatus::GOOD,
     OCSPVerifyResult::PROVIDED},

    {"has_extension.pem", OCSPRevocationStatus::GOOD,
     OCSPVerifyResult::PROVIDED},

    {"good_response_next_update.pem", OCSPRevocationStatus::GOOD,
     OCSPVerifyResult::PROVIDED},

    {"revoke_response.pem", OCSPRevocationStatus::REVOKED,
     OCSPVerifyResult::PROVIDED},

    {"revoke_response_reason.pem", OCSPRevocationStatus::REVOKED,
     OCSPVerifyResult::PROVIDED},

    {"unknown_response.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::PROVIDED},

    {"multiple_response.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::PROVIDED},

    {"other_response.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::NO_MATCHING_RESPONSE},

    {"has_single_extension.pem", OCSPRevocationStatus::GOOD,
     OCSPVerifyResult::PROVIDED},

    {"missing_response.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::NO_MATCHING_RESPONSE},
};

// Parameterised test name generator for tests depending on RenderTextBackend.
struct PrintTestName {
  std::string operator()(const testing::TestParamInfo<TestParams>& info) const {
    base::StringPiece name(info.param.file_name);
    // Strip ".pem" from the end as GTest names cannot contain period.
    name.remove_suffix(4);
    return name.as_string();
  }
};

INSTANTIATE_TEST_CASE_P(,
                        CheckOCSPTest,
                        ::testing::ValuesIn(kTestParams),
                        PrintTestName());

TEST_P(CheckOCSPTest, FromFile) {
  const TestParams& params = GetParam();

  std::string ocsp_data;
  std::string ca_data;
  std::string cert_data;
  std::string request_data;
  const PemBlockMapping mappings[] = {
      {"OCSP RESPONSE", &ocsp_data},
      {"CA CERTIFICATE", &ca_data},
      {"CERTIFICATE", &cert_data},
      {"OCSP REQUEST", &request_data},
  };

  ASSERT_TRUE(ReadTestDataFromPemFile(GetFilePath(params.file_name), mappings));

  // Mar 5 00:00:00 2017 GMT
  base::Time kVerifyTime =
      base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(1488672000);

  // Test that CheckOCSP() works.
  OCSPVerifyResult::ResponseStatus response_status;
  OCSPRevocationStatus revocation_status =
      CheckOCSP(ocsp_data, cert_data, ca_data, kVerifyTime, kOCSPAgeOneWeek,
                &response_status);

  EXPECT_EQ(params.expected_revocation_status, revocation_status);
  EXPECT_EQ(params.expected_response_status, response_status);

  // Check that CreateOCSPRequest() works.
  scoped_refptr<ParsedCertificate> cert = ParseCertificate(cert_data);
  ASSERT_TRUE(cert);

  scoped_refptr<ParsedCertificate> issuer = ParseCertificate(ca_data);
  ASSERT_TRUE(issuer);

  std::vector<uint8_t> encoded_request;
  ASSERT_TRUE(CreateOCSPRequest(cert.get(), issuer.get(), &encoded_request));

  EXPECT_EQ(der::Input(encoded_request.data(), encoded_request.size()),
            der::Input(&request_data));
}

TEST(OCSPDateTest, Valid) {
  OCSPSingleResponse response;

  base::Time now = base::Time::Now();
  base::Time this_update = now - base::TimeDelta::FromHours(1);
  ASSERT_TRUE(
      der::EncodeTimeAsGeneralizedTime(this_update, &response.this_update));
  response.has_next_update = false;
  EXPECT_TRUE(CheckOCSPDateValid(response, now, kOCSPAgeOneWeek));

  base::Time next_update = this_update + base::TimeDelta::FromDays(7);
  ASSERT_TRUE(
      der::EncodeTimeAsGeneralizedTime(next_update, &response.next_update));
  response.has_next_update = true;
  EXPECT_TRUE(CheckOCSPDateValid(response, now, kOCSPAgeOneWeek));
}

TEST(OCSPDateTest, ThisUpdateInTheFuture) {
  OCSPSingleResponse response;

  base::Time now = base::Time::Now();
  base::Time this_update = now + base::TimeDelta::FromHours(1);
  ASSERT_TRUE(
      der::EncodeTimeAsGeneralizedTime(this_update, &response.this_update));
  response.has_next_update = false;
  EXPECT_FALSE(CheckOCSPDateValid(response, now, kOCSPAgeOneWeek));

  base::Time next_update = this_update + base::TimeDelta::FromDays(7);
  ASSERT_TRUE(
      der::EncodeTimeAsGeneralizedTime(next_update, &response.next_update));
  response.has_next_update = true;
  EXPECT_FALSE(CheckOCSPDateValid(response, now, kOCSPAgeOneWeek));
}

TEST(OCSPDateTest, NextUpdatePassed) {
  OCSPSingleResponse response;

  base::Time now = base::Time::Now();
  base::Time this_update = now - base::TimeDelta::FromDays(6);
  ASSERT_TRUE(
      der::EncodeTimeAsGeneralizedTime(this_update, &response.this_update));
  response.has_next_update = false;
  EXPECT_TRUE(CheckOCSPDateValid(response, now, kOCSPAgeOneWeek));

  base::Time next_update = now - base::TimeDelta::FromHours(1);
  ASSERT_TRUE(
      der::EncodeTimeAsGeneralizedTime(next_update, &response.next_update));
  response.has_next_update = true;
  EXPECT_FALSE(CheckOCSPDateValid(response, now, kOCSPAgeOneWeek));
}

TEST(OCSPDateTest, NextUpdateBeforeThisUpdate) {
  OCSPSingleResponse response;

  base::Time now = base::Time::Now();
  base::Time this_update = now - base::TimeDelta::FromDays(1);
  ASSERT_TRUE(
      der::EncodeTimeAsGeneralizedTime(this_update, &response.this_update));
  response.has_next_update = false;
  EXPECT_TRUE(CheckOCSPDateValid(response, now, kOCSPAgeOneWeek));

  base::Time next_update = this_update - base::TimeDelta::FromDays(1);
  ASSERT_TRUE(
      der::EncodeTimeAsGeneralizedTime(next_update, &response.next_update));
  response.has_next_update = true;
  EXPECT_FALSE(CheckOCSPDateValid(response, now, kOCSPAgeOneWeek));
}

TEST(OCSPDateTest, ThisUpdateOlderThanMaxAge) {
  OCSPSingleResponse response;

  base::Time now = base::Time::Now();
  base::Time this_update = now - kOCSPAgeOneWeek;
  ASSERT_TRUE(
      der::EncodeTimeAsGeneralizedTime(this_update, &response.this_update));
  response.has_next_update = false;
  EXPECT_TRUE(CheckOCSPDateValid(response, now, kOCSPAgeOneWeek));

  base::Time next_update = now + base::TimeDelta::FromHours(1);
  ASSERT_TRUE(
      der::EncodeTimeAsGeneralizedTime(next_update, &response.next_update));
  response.has_next_update = true;
  EXPECT_TRUE(CheckOCSPDateValid(response, now, kOCSPAgeOneWeek));

  ASSERT_TRUE(der::EncodeTimeAsGeneralizedTime(
      this_update - base::TimeDelta::FromSeconds(1), &response.this_update));
  response.has_next_update = false;
  EXPECT_FALSE(CheckOCSPDateValid(response, now, kOCSPAgeOneWeek));
  response.has_next_update = true;
  EXPECT_FALSE(CheckOCSPDateValid(response, now, kOCSPAgeOneWeek));
}

TEST(OCSPDateTest, VerifyTimeFromBeforeWindowsEpoch) {
  OCSPSingleResponse response;
  base::Time windows_epoch;
  base::Time verify_time = windows_epoch - base::TimeDelta::FromDays(1);

  base::Time now = base::Time::Now();
  base::Time this_update = now - base::TimeDelta::FromHours(1);
  ASSERT_TRUE(
      der::EncodeTimeAsGeneralizedTime(this_update, &response.this_update));
  response.has_next_update = false;
  EXPECT_FALSE(CheckOCSPDateValid(response, verify_time, kOCSPAgeOneWeek));

  base::Time next_update = this_update + kOCSPAgeOneWeek;
  ASSERT_TRUE(
      der::EncodeTimeAsGeneralizedTime(next_update, &response.next_update));
  response.has_next_update = true;
  EXPECT_FALSE(CheckOCSPDateValid(response, verify_time, kOCSPAgeOneWeek));
}

TEST(OCSPDateTest, VerifyTimeMinusAgeFromBeforeWindowsEpoch) {
  OCSPSingleResponse response;
  base::Time windows_epoch;
  base::Time verify_time = windows_epoch + base::TimeDelta::FromDays(1);

  base::Time this_update = windows_epoch;
  ASSERT_TRUE(
      der::EncodeTimeAsGeneralizedTime(this_update, &response.this_update));
  response.has_next_update = false;
#if defined(OS_WIN)
  EXPECT_FALSE(CheckOCSPDateValid(response, verify_time, kOCSPAgeOneWeek));
#else
  EXPECT_TRUE(CheckOCSPDateValid(response, verify_time, kOCSPAgeOneWeek));
#endif
}

base::StringPiece kGetURLTestParams[] = {
    "http://www.example.com/", "http://www.example.com/path/",
    "http://www.example.com/path",
    "http://www.example.com/path?query"
    "http://user:pass@www.example.com/path?query",
};

class CreateOCSPGetURLTest
    : public ::testing::TestWithParam<base::StringPiece> {};

INSTANTIATE_TEST_CASE_P(,
                        CreateOCSPGetURLTest,
                        ::testing::ValuesIn(kGetURLTestParams));

TEST_P(CreateOCSPGetURLTest, Basic) {
  std::string ca_data;
  std::string cert_data;
  std::string request_data;
  const PemBlockMapping mappings[] = {
      {"CA CERTIFICATE", &ca_data},
      {"CERTIFICATE", &cert_data},
      {"OCSP REQUEST", &request_data},
  };

  // Load one of the test files. (Doesn't really matter which one as
  // constructing the DER is tested elsewhere).
  ASSERT_TRUE(
      ReadTestDataFromPemFile(GetFilePath("good_response.pem"), mappings));

  scoped_refptr<ParsedCertificate> cert = ParseCertificate(cert_data);
  ASSERT_TRUE(cert);

  scoped_refptr<ParsedCertificate> issuer = ParseCertificate(ca_data);
  ASSERT_TRUE(issuer);

  GURL url = CreateOCSPGetURL(cert.get(), issuer.get(), GetParam());

  // Try to extract the encoded data and compare against |request_data|.
  //
  // A known answer output test would be better as this just reverses the logic
  // from the implementaiton file.
  std::string b64 = url.spec().substr(GetParam().size() + 1);

  // Hex un-escape the data.
  base::ReplaceSubstringsAfterOffset(&b64, 0, "%2B", "+");
  base::ReplaceSubstringsAfterOffset(&b64, 0, "%2F", "/");
  base::ReplaceSubstringsAfterOffset(&b64, 0, "%3D", "=");

  // Base64 decode the data.
  std::string decoded;
  ASSERT_TRUE(base::Base64Decode(b64, &decoded));

  EXPECT_EQ(request_data, decoded);
}

}  // namespace

}  // namespace net
