// Copyright 2016 The Chromium Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "ocsp.h"

#include <gtest/gtest.h>

#include <openssl/base64.h>
#include <openssl/pool.h>
#include <openssl/span.h>

#include "encode_values.h"
#include "string_util.h"
#include "test_helpers.h"

BSSL_NAMESPACE_BEGIN

namespace {

constexpr int64_t kOCSPAgeOneWeek = 7 * 24 * 60 * 60;

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

std::shared_ptr<const ParsedCertificate> ParseCertificate(
    std::string_view data) {
  CertErrors errors;
  auto bytes = StringAsBytes(data);
  return ParsedCertificate::Create(
      bssl::UniquePtr<CRYPTO_BUFFER>(
          CRYPTO_BUFFER_new(bytes.data(), bytes.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},
    {"has_critical_single_extension.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::UNHANDLED_CRITICAL_EXTENSION},
    {"has_critical_response_extension.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::UNHANDLED_CRITICAL_EXTENSION},
    {"has_critical_ct_extension.pem", OCSPRevocationStatus::GOOD,
     OCSPVerifyResult::PROVIDED},
    {"missing_response.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::NO_MATCHING_RESPONSE},
    {"stale_response.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::INVALID_DATE},
    {"future_response.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::INVALID_DATE},
    {"old_response.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::INVALID_DATE},
    {"produced_early_response.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::BAD_PRODUCED_AT},
    {"produced_late_response.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::BAD_PRODUCED_AT},
    {"invalid_response.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::PARSE_RESPONSE_ERROR},
    {"invalid_response_data.pem", OCSPRevocationStatus::UNKNOWN,
     OCSPVerifyResult::PARSE_RESPONSE_DATA_ERROR},
    {"multiple_response_good_revoked.pem", OCSPRevocationStatus::REVOKED,
     OCSPVerifyResult::PROVIDED},
};

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

INSTANTIATE_TEST_SUITE_P(All, 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
  int64_t kVerifyTime = 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.
  std::shared_ptr<const ParsedCertificate> cert = ParseCertificate(cert_data);
  ASSERT_TRUE(cert);

  std::shared_ptr<const 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),
            der::Input(StringAsBytes(request_data)));
}

std::string_view 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<std::string_view> {
};

INSTANTIATE_TEST_SUITE_P(All, 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));

  std::shared_ptr<const ParsedCertificate> cert = ParseCertificate(cert_data);
  ASSERT_TRUE(cert);

  std::shared_ptr<const ParsedCertificate> issuer = ParseCertificate(ca_data);
  ASSERT_TRUE(issuer);

  std::optional<std::string> url =
      CreateOCSPGetURL(cert.get(), issuer.get(), GetParam());
  ASSERT_TRUE(url);

  // 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 implementation file.
  std::string b64 = url->substr(GetParam().size() + 1);

  // Hex un-escape the data.
  b64 = bssl::string_util::FindAndReplace(b64, "%2B", "+");
  b64 = bssl::string_util::FindAndReplace(b64, "%2F", "/");
  b64 = bssl::string_util::FindAndReplace(b64, "%3D", "=");

  // Base64 decode the data.
  size_t len;
  EXPECT_TRUE(EVP_DecodedLength(&len, b64.size()));
  std::vector<uint8_t> decoded(len);
  EXPECT_TRUE(EVP_DecodeBase64(decoded.data(), &len, len,
                               reinterpret_cast<const uint8_t *>(b64.data()),
                               b64.size()));
  std::string decoded_string(decoded.begin(), decoded.begin() + len);

  EXPECT_EQ(request_data, decoded_string);
}

}  // namespace

BSSL_NAMESPACE_END
