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

#include "components/certificate_transparency/chrome_ct_policy_enforcer.h"

#include <stdint.h>

#include <algorithm>
#include <memory>
#include <utility>

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/field_trial.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "base/values.h"
#include "base/version.h"
#include "components/certificate_transparency/ct_known_logs.h"
#include "crypto/sha2.h"
#include "net/cert/ct_policy_status.h"
#include "net/cert/signed_certificate_timestamp.h"
#include "net/cert/x509_certificate.h"
#include "net/log/net_log_capture_mode.h"
#include "net/log/net_log_event_type.h"
#include "net/log/net_log_values.h"
#include "net/log/net_log_with_source.h"
#include "third_party/boringssl/src/include/openssl/bytestring.h"

using net::ct::CTPolicyCompliance;

namespace certificate_transparency {

namespace {

// Type of a leaf index extension in an SCT from a Static CT API log.
const uint8_t kExtensionTypeLeafIndex = 0;

base::DictValue NetLogCertComplianceCheckResultParams(
    net::X509Certificate* cert,
    bool build_timely,
    base::Time log_list_timestamp,
    CTPolicyCompliance compliance) {
  base::DictValue dict;
  dict.Set("build_timely", build_timely);
  dict.Set("log_list_timestamp",
           net::NetLogNumberValue(
               log_list_timestamp.InMillisecondsSinceUnixEpoch()));
  dict.Set("ct_compliance_status", CTPolicyComplianceToString(compliance));
  return dict;
}

// Returns true if the extension is a leaf index extension from a Static CT API
// log. See
// https://github.com/C2SP/C2SP/blob/main/static-ct-api.md#sct-extension
bool IsValidLeafIndexExtension(CBS* in) {
  uint8_t bytes[5];
  if (!CBS_copy_bytes(in, bytes, 5)) {
    return false;
  }
  // Any value is a valid leaf index.
  return true;
}

// Returns true if the SCT has only one valid leaf index extension.
bool HasValidLeafIndex(
    const scoped_refptr<net::ct::SignedCertificateTimestamp> sct) {
  CBS extension_cbs;
  CBS_init(&extension_cbs, reinterpret_cast<uint8_t*>(sct->extensions.data()),
           sct->extensions.size());
  enum class LeafIndexStatus {
    kFoundValid,
    kFoundInvalid,
    kNotFound,
  };
  LeafIndexStatus status = LeafIndexStatus::kNotFound;

  // Look for a valid leaf index extension. The extension can be anywhere, so
  // keep looking until we can find one. There must not be more than one leaf
  // index extension.
  while (CBS_len(&extension_cbs) != 0) {
    uint8_t extension_type;
    if (!CBS_get_u8(&extension_cbs, &extension_type)) {
      return false;
    }

    CBS extension_data;
    if (!CBS_get_u16_length_prefixed(&extension_cbs, &extension_data)) {
      return false;
    }

    if (extension_type == kExtensionTypeLeafIndex) {
      if (status != LeafIndexStatus::kNotFound) {
        // Must not have multiple leaf index extensions.
        return false;
      }
      status = IsValidLeafIndexExtension(&extension_data)
                   ? LeafIndexStatus::kFoundValid
                   : LeafIndexStatus::kFoundInvalid;
    }
  }
  return status == LeafIndexStatus::kFoundValid;
}

}  // namespace

OperatorHistoryEntry::OperatorHistoryEntry() = default;
OperatorHistoryEntry::~OperatorHistoryEntry() = default;
OperatorHistoryEntry::OperatorHistoryEntry(const OperatorHistoryEntry& other) =
    default;

ChromeCTPolicyEnforcer::ChromeCTPolicyEnforcer(
    base::Time log_list_date,
    std::vector<std::pair<std::string, base::Time>> disqualified_logs,
    std::map<std::string, LogInfo> log_info,
    bool enforce_one_rfc6962_ct_policy)
    : disqualified_logs_(std::move(disqualified_logs)),
      log_info_(std::move(log_info)),
      log_list_date_(log_list_date),
      enforce_one_rfc6962_ct_policy_(enforce_one_rfc6962_ct_policy) {}

ChromeCTPolicyEnforcer::~ChromeCTPolicyEnforcer() = default;

CTPolicyCompliance ChromeCTPolicyEnforcer::CheckCompliance(
    net::X509Certificate* cert,
    const net::ct::SCTList& verified_scts,
    base::Time current_time,
    const net::NetLogWithSource& net_log) const {
  // If the build is not timely, no certificate is considered compliant
  // with CT policy. The reasoning is that, for example, a log might
  // have been pulled and is no longer considered valid; thus, a client
  // needs up-to-date information about logs to consider certificates to
  // be compliant with policy.
  bool build_timely = IsLogDataTimely(current_time);
  CTPolicyCompliance compliance;
  if (!build_timely) {
    compliance = CTPolicyCompliance::CT_POLICY_BUILD_NOT_TIMELY;
  } else {
    compliance = CheckCTPolicyCompliance(*cert, verified_scts, current_time);
  }

  net_log.AddEvent(net::NetLogEventType::CERT_CT_COMPLIANCE_CHECKED, [&] {
    return NetLogCertComplianceCheckResultParams(cert, build_timely,
                                                 log_list_date_, compliance);
  });

  return compliance;
}

std::optional<base::Time> ChromeCTPolicyEnforcer::GetLogDisqualificationTime(
    std::string_view log_id) const {
  CHECK_EQ(log_id.size(), crypto::kSHA256Length);

  auto p = std::lower_bound(
      std::begin(disqualified_logs_), std::end(disqualified_logs_), log_id,
      [](const auto& a, std::string_view b) { return a.first < b; });
  if (p == std::end(disqualified_logs_) || p->first != log_id) {
    return std::nullopt;
  }
  return p->second;
}

bool ChromeCTPolicyEnforcer::IsCtEnabled() const {
  return true;
}

bool ChromeCTPolicyEnforcer::IsLogDisqualified(
    std::string_view log_id,
    base::Time current_time,
    base::Time* out_disqualification_date) const {
  std::optional<base::Time> disqualification_date =
      GetLogDisqualificationTime(log_id);
  if (!disqualification_date.has_value()) {
    return false;
  }
  *out_disqualification_date = disqualification_date.value();
  return current_time >= disqualification_date.value();
}

bool ChromeCTPolicyEnforcer::IsLogDataTimely(base::Time current_time) const {
  // We consider built-in information to be timely for 10 weeks.
  return (current_time - log_list_date_).InDays() < 70 /* 10 weeks */;
}

// Evaluates against the "on-or-after 15 April 2022" policy specified at
// https://googlechrome.github.io/CertificateTransparency/ct_policy.html
// (No certificate issued before that date could still be valid.)
CTPolicyCompliance ChromeCTPolicyEnforcer::CheckCTPolicyCompliance(
    const net::X509Certificate& cert,
    const net::ct::SCTList& verified_scts,
    base::Time current_time) const {
  // Cert is outside the bounds of parsable; reject it.
  if (cert.valid_start().is_null() || cert.valid_expiry().is_null() ||
      cert.valid_start().is_max() || cert.valid_expiry().is_max()) {
    return CTPolicyCompliance::CT_POLICY_NOT_ENOUGH_SCTS;
  }

  // Scan for the earliest SCT. This is used to determine whether to enforce
  // log diversity requirements, as well as whether to enforce whether or not
  // a log was qualified or pending qualification at time of issuance (in the
  // case of embedded SCTs). It's acceptable to ignore the origin of the SCT,
  // because SCTs delivered via OCSP/TLS extension will cover the full
  // certificate, which necessarily will exist only after the precertificate
  // has been logged and the actual certificate issued.
  // Note: Here, issuance date is defined as the earliest of all SCTs, rather
  // than the latest of embedded SCTs, in order to give CAs the benefit of
  // the doubt in the event a log is revoked in the midst of processing
  // a precertificate and issuing the certificate.
  base::Time issuance_date = base::Time::Max();
  for (const auto& sct : verified_scts) {
    base::Time unused;
    if (IsLogDisqualified(sct->log_id, current_time, &unused)) {
      continue;
    }
    issuance_date = std::min(sct->timestamp, issuance_date);
  }

  bool has_valid_embedded_sct = false;
  bool has_valid_nonembedded_sct = false;
  bool has_diverse_log_operators = false;
  bool has_rfc6962_log = false;
  std::vector<std::string_view> embedded_log_ids;
  std::string first_seen_operator;
  for (const auto& sct : verified_scts) {
    base::Time disqualification_date;
    bool is_disqualified =
        IsLogDisqualified(sct->log_id, current_time, &disqualification_date);
    if (is_disqualified &&
        sct->origin != net::ct::SignedCertificateTimestamp::SCT_EMBEDDED) {
      // For OCSP and TLS delivered SCTs, only SCTs that are valid at the
      // time of check are accepted.
      continue;
    }

    auto log_type = GetLogType(sct->log_id);
    if (log_type == network::mojom::CTLogInfo::LogType::kStaticCTAPI &&
        !HasValidLeafIndex(sct)) {
      continue;
    }

    if (sct->origin != net::ct::SignedCertificateTimestamp::SCT_EMBEDDED) {
      has_valid_nonembedded_sct = true;
    } else {
      has_valid_embedded_sct |= !is_disqualified;
      // If the log is disqualified, it only counts towards quorum if
      // the certificate was issued before the log was disqualified, and the
      // SCT was obtained before the log was disqualified.
      if (!is_disqualified || (issuance_date < disqualification_date &&
                               sct->timestamp < disqualification_date)) {
        embedded_log_ids.push_back(sct->log_id);
      }
    }

    if (!has_diverse_log_operators) {
      std::string sct_operator = GetOperatorForLog(sct->log_id, sct->timestamp);
      if (first_seen_operator.empty()) {
        first_seen_operator = sct_operator;
      } else {
        has_diverse_log_operators |= first_seen_operator != sct_operator;
      }
    }

    if (enforce_one_rfc6962_ct_policy_) {
      // TODO(crbug.com/370724580): Disallow kUnspecified once all logs in the
      // hardcoded and component updater protos have proper log types.
      has_rfc6962_log |=
          (log_type == network::mojom::CTLogInfo::LogType::kRFC6962 ||
           log_type == network::mojom::CTLogInfo::LogType::kUnspecified);
    }
  }

  // Option 1:
  // An SCT presented via the TLS extension OR embedded within a stapled OCSP
  //   response is from a log qualified at time of check;
  // AND there are at least two SCTs from logs with different operators,
  //   presented by any method.
  //
  // Note: Because SCTs embedded via TLS or OCSP can be updated on the fly,
  // the issuance date is irrelevant, as any policy changes can be
  // accommodated.
  if (has_valid_nonembedded_sct && has_diverse_log_operators &&
      (!enforce_one_rfc6962_ct_policy_ || has_rfc6962_log)) {
    return CTPolicyCompliance::CT_POLICY_COMPLIES_VIA_SCTS;
  }
  // Note: If has_valid_nonembedded_sct was true, but Option 2 isn't met,
  // then the result will be that there weren't diverse enough SCTs, as that
  // the only other way for the conditional above to fail). Because Option 1
  // has the diversity requirement, it's implicitly a minimum number of SCTs
  // (specifically, 2), but that's not explicitly specified in the policy.

  // Option 2:
  // There is at least one embedded SCT from a log qualified at the time of
  //   check ...
  if (!has_valid_embedded_sct) {
    // Under Option 2, there weren't enough SCTs, and potentially under
    // Option 1, there weren't diverse enough SCTs. Try to signal the error
    // that is most easily fixed.
    return has_valid_nonembedded_sct
               ? CTPolicyCompliance::CT_POLICY_NOT_DIVERSE_SCTS
               : CTPolicyCompliance::CT_POLICY_NOT_ENOUGH_SCTS;
  }

  size_t num_required_embedded_scts = 5;
  // ... AND there are at least two SCTs from logs with different
  // operators ...
  if (!has_diverse_log_operators) {
    return CTPolicyCompliance::CT_POLICY_NOT_DIVERSE_SCTS;
  }

  // ... AND at least one of the SCTs must come from an RFC6962 log.
  if (enforce_one_rfc6962_ct_policy_ && !has_rfc6962_log) {
    return CTPolicyCompliance::CT_POLICY_NOT_DIVERSE_SCTS;
  }

  // ... AND the certificate embeds SCTs from AT LEAST the number of logs
  //   once or currently qualified shown in Table 1 of the CT Policy.
  base::TimeDelta lifetime = cert.valid_expiry() - cert.valid_start();
  if (lifetime > base::Days(180)) {
    num_required_embedded_scts = 3;
  } else {
    num_required_embedded_scts = 2;
  }

  // Sort the embedded log IDs and remove duplicates, so that only a single
  // SCT from each log is accepted. This is to handle the case where a given
  // log returns different SCTs for the same precertificate (which is
  // permitted, but advised against).
  std::sort(embedded_log_ids.begin(), embedded_log_ids.end());
  auto sorted_end =
      std::unique(embedded_log_ids.begin(), embedded_log_ids.end());
  size_t num_embedded_scts =
      std::distance(embedded_log_ids.begin(), sorted_end);

  if (num_embedded_scts >= num_required_embedded_scts) {
    return CTPolicyCompliance::CT_POLICY_COMPLIES_VIA_SCTS;
  }

  // Under Option 2, there weren't enough SCTs, and potentially under Option
  // 1, there weren't diverse enough SCTs. Try to signal the error that is
  // most easily fixed.
  return has_valid_nonembedded_sct
             ? CTPolicyCompliance::CT_POLICY_NOT_DIVERSE_SCTS
             : CTPolicyCompliance::CT_POLICY_NOT_ENOUGH_SCTS;
}

std::string ChromeCTPolicyEnforcer::GetOperatorForLog(
    const std::string& log_id,
    base::Time timestamp) const {
  DCHECK(log_info_.find(log_id) != log_info_.end());
  const OperatorHistoryEntry& log_history =
      log_info_.at(log_id).operator_history;
  for (const auto& operator_entry : log_history.previous_operators) {
    if (timestamp < operator_entry.second) {
      return operator_entry.first;
    }
  }
  // Either the log has only ever had one operator, or the timestamp is after
  // the last operator change.
  return log_history.current_operator;
}

network::mojom::CTLogInfo::LogType ChromeCTPolicyEnforcer::GetLogType(
    const std::string& log_id) const {
  DCHECK(log_info_.find(log_id) != log_info_.end());
  return log_info_.at(log_id).log_type;
}

}  // namespace certificate_transparency
