1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
|
// 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/metrics/histogram_macros.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::Value::Dict NetLogCertComplianceCheckResultParams(
net::X509Certificate* cert,
bool build_timely,
base::Time log_list_timestamp,
CTPolicyCompliance compliance) {
base::Value::Dict 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 enable_static_ct_api_enforcement)
: disqualified_logs_(std::move(disqualified_logs)),
log_info_(std::move(log_info)),
log_list_date_(log_list_date),
enable_static_ct_api_enforcement_(enable_static_ct_api_enforcement) {}
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 (enable_static_ct_api_enforcement_ &&
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 (enable_static_ct_api_enforcement_) {
// 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 &&
(!enable_static_ct_api_enforcement_ || 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 (enable_static_ct_api_enforcement_ && !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
|