// Copyright 2014 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/cert_policy_enforcer.h" #include #include "base/bind.h" #include "base/build_time.h" #include "base/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/values.h" #include "base/version.h" #include "net/cert/ct_ev_whitelist.h" #include "net/cert/ct_verify_result.h" #include "net/cert/signed_certificate_timestamp.h" #include "net/cert/x509_certificate.h" #include "net/cert/x509_certificate_net_log_param.h" #include "net/log/net_log.h" namespace net { namespace { bool IsEmbeddedSCT(const scoped_refptr& sct) { return sct->origin == ct::SignedCertificateTimestamp::SCT_EMBEDDED; } // Returns true if the current build is recent enough to ensure that // built-in security information (e.g. CT Logs) is fresh enough. // TODO(eranm): Move to base or net/base bool IsBuildTimely() { #if defined(DONT_EMBED_BUILD_METADATA) && !defined(OFFICIAL_BUILD) return true; #else const base::Time build_time = base::GetBuildTime(); // We consider built-in information to be timely for 10 weeks. return (base::Time::Now() - build_time).InDays() < 70 /* 10 weeks */; #endif } // Returns a rounded-down months difference of |start| and |end|, // together with an indication of whether the last month was // a full month, because the range starts specified in the policy // are not consistent in terms of including the range start value. void RoundedDownMonthDifference(const base::Time& start, const base::Time& end, size_t* rounded_months_difference, bool* has_partial_month) { DCHECK(rounded_months_difference); DCHECK(has_partial_month); base::Time::Exploded exploded_start; base::Time::Exploded exploded_expiry; start.UTCExplode(&exploded_start); end.UTCExplode(&exploded_expiry); if (end < start) { *rounded_months_difference = 0; *has_partial_month = false; } *has_partial_month = true; uint32_t month_diff = (exploded_expiry.year - exploded_start.year) * 12 + (exploded_expiry.month - exploded_start.month); if (exploded_expiry.day_of_month < exploded_start.day_of_month) --month_diff; else if (exploded_expiry.day_of_month == exploded_start.day_of_month) *has_partial_month = false; *rounded_months_difference = month_diff; } bool HasRequiredNumberOfSCTs(const X509Certificate& cert, const ct::CTVerifyResult& ct_result) { // TODO(eranm): Count the number of *independent* SCTs once the information // about log operators is available, crbug.com/425174 size_t num_valid_scts = ct_result.verified_scts.size(); size_t num_embedded_scts = std::count_if(ct_result.verified_scts.begin(), ct_result.verified_scts.end(), IsEmbeddedSCT); size_t num_non_embedded_scts = num_valid_scts - num_embedded_scts; // If at least two valid SCTs were delivered by means other than embedding // (i.e. in a TLS extension or OCSP), then the certificate conforms to bullet // number 3 of the "Qualifying Certificate" section of the CT/EV policy. if (num_non_embedded_scts >= 2) return true; if (cert.valid_start().is_null() || cert.valid_expiry().is_null() || cert.valid_start().is_max() || cert.valid_expiry().is_max()) { // Will not be able to calculate the certificate's validity period. return false; } size_t lifetime; bool has_partial_month; RoundedDownMonthDifference(cert.valid_start(), cert.valid_expiry(), &lifetime, &has_partial_month); // For embedded SCTs, if the certificate has the number of SCTs specified in // table 1 of the "Qualifying Certificate" section of the CT/EV policy, then // it qualifies. size_t num_required_embedded_scts; if (lifetime > 39 || (lifetime == 39 && has_partial_month)) { num_required_embedded_scts = 5; } else if (lifetime > 27 || (lifetime == 27 && has_partial_month)) { num_required_embedded_scts = 4; } else if (lifetime >= 15) { num_required_embedded_scts = 3; } else { num_required_embedded_scts = 2; } return num_embedded_scts >= num_required_embedded_scts; } enum CTComplianceStatus { CT_NOT_COMPLIANT = 0, CT_IN_WHITELIST = 1, CT_ENOUGH_SCTS = 2, CT_COMPLIANCE_MAX, }; const char* ComplianceStatusToString(CTComplianceStatus status) { switch (status) { case CT_NOT_COMPLIANT: return "NOT_COMPLIANT"; break; case CT_IN_WHITELIST: return "WHITELISTED"; break; case CT_ENOUGH_SCTS: return "ENOUGH_SCTS"; break; case CT_COMPLIANCE_MAX: break; } return "unknown"; } enum EVWhitelistStatus { EV_WHITELIST_NOT_PRESENT = 0, EV_WHITELIST_INVALID = 1, EV_WHITELIST_VALID = 2, EV_WHITELIST_MAX, }; void LogCTComplianceStatusToUMA(CTComplianceStatus status, const ct::EVCertsWhitelist* ev_whitelist) { UMA_HISTOGRAM_ENUMERATION("Net.SSL_EVCertificateCTCompliance", status, CT_COMPLIANCE_MAX); if (status == CT_NOT_COMPLIANT) { EVWhitelistStatus ev_whitelist_status = EV_WHITELIST_NOT_PRESENT; if (ev_whitelist != NULL) { if (ev_whitelist->IsValid()) ev_whitelist_status = EV_WHITELIST_VALID; else ev_whitelist_status = EV_WHITELIST_INVALID; } UMA_HISTOGRAM_ENUMERATION("Net.SSL_EVWhitelistValidityForNonCompliantCert", ev_whitelist_status, EV_WHITELIST_MAX); } } struct ComplianceDetails { ComplianceDetails() : ct_presence_required(false), build_timely(false), status(CT_NOT_COMPLIANT) {} // Whether enforcement of the policy was required or not. bool ct_presence_required; // Whether the build is not older than 10 weeks. The value is meaningful only // if |ct_presence_required| is true. bool build_timely; // Compliance status - meaningful only if |ct_presence_required| and // |build_timely| are true. CTComplianceStatus status; // EV whitelist version. base::Version whitelist_version; }; scoped_ptr NetLogComplianceCheckResultCallback( X509Certificate* cert, ComplianceDetails* details, NetLogCaptureMode capture_mode) { scoped_ptr dict(new base::DictionaryValue()); dict->Set("certificate", NetLogX509CertificateCallback(cert, capture_mode)); dict->SetBoolean("policy_enforcement_required", details->ct_presence_required); if (details->ct_presence_required) { dict->SetBoolean("build_timely", details->build_timely); if (details->build_timely) { dict->SetString("ct_compliance_status", ComplianceStatusToString(details->status)); if (details->whitelist_version.IsValid()) dict->SetString("ev_whitelist_version", details->whitelist_version.GetString()); } } return dict.Pass(); } bool IsCertificateInWhitelist(const X509Certificate& cert, const ct::EVCertsWhitelist* ev_whitelist) { bool cert_in_ev_whitelist = false; if (ev_whitelist && ev_whitelist->IsValid()) { const SHA256HashValue fingerprint( X509Certificate::CalculateFingerprint256(cert.os_cert_handle())); std::string truncated_fp = std::string(reinterpret_cast(fingerprint.data), 8); cert_in_ev_whitelist = ev_whitelist->ContainsCertificateHash(truncated_fp); UMA_HISTOGRAM_BOOLEAN("Net.SSL_EVCertificateInWhitelist", cert_in_ev_whitelist); } return cert_in_ev_whitelist; } void CheckCTEVPolicyCompliance(X509Certificate* cert, const ct::EVCertsWhitelist* ev_whitelist, const ct::CTVerifyResult& ct_result, ComplianceDetails* result) { result->ct_presence_required = true; if (!IsBuildTimely()) return; result->build_timely = true; if (ev_whitelist && ev_whitelist->IsValid()) result->whitelist_version = ev_whitelist->Version(); if (IsCertificateInWhitelist(*cert, ev_whitelist)) { result->status = CT_IN_WHITELIST; return; } if (HasRequiredNumberOfSCTs(*cert, ct_result)) { result->status = CT_ENOUGH_SCTS; return; } result->status = CT_NOT_COMPLIANT; } } // namespace bool CertPolicyEnforcer::DoesConformToCTEVPolicy( X509Certificate* cert, const ct::EVCertsWhitelist* ev_whitelist, const ct::CTVerifyResult& ct_result, const BoundNetLog& net_log) { ComplianceDetails details; CheckCTEVPolicyCompliance(cert, ev_whitelist, ct_result, &details); NetLog::ParametersCallback net_log_callback = base::Bind(&NetLogComplianceCheckResultCallback, base::Unretained(cert), base::Unretained(&details)); net_log.AddEvent(NetLog::TYPE_EV_CERT_CT_COMPLIANCE_CHECKED, net_log_callback); if (!details.ct_presence_required) return true; if (!details.build_timely) return false; LogCTComplianceStatusToUMA(details.status, ev_whitelist); if (details.status == CT_IN_WHITELIST || details.status == CT_ENOUGH_SCTS) return true; return false; } } // namespace net