#include "components/certificate_transparency/chrome_require_ct_delegate.h"
#include <algorithm>
#include <iterator>
#include <map>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/types/zip.h"
#include "base/values.h"
#include "components/url_formatter/url_fixer.h"
#include "components/url_matcher/url_matcher.h"
#include "crypto/sha2.h"
#include "net/base/hash_value.h"
#include "net/base/host_port_pair.h"
#include "net/cert/asn1_util.h"
#include "net/cert/known_roots.h"
#include "net/cert/x509_certificate.h"
#include "net/cert/x509_util.h"
#include "third_party/boringssl/src/pki/name_constraints.h"
#include "third_party/boringssl/src/pki/parse_name.h"
#include "third_party/boringssl/src/pki/parsed_certificate.h"
namespace certificate_transparency {
namespace {
class OrgAttributeFilter {
public:
explicit OrgAttributeFilter(const bssl::RDNSequence& sequence)
: sequence_head_(sequence.begin()), sequence_end_(sequence.end()) {
if (sequence_head_ != sequence_end_) {
rdn_it_ = sequence_head_->begin();
AdvanceIfNecessary();
}
}
bool IsValid() const { return sequence_head_ != sequence_end_; }
const bssl::X509NameAttribute& GetAttribute() const {
DCHECK(IsValid());
return *rdn_it_;
}
void Advance() {
DCHECK(IsValid());
++rdn_it_;
AdvanceIfNecessary();
}
private:
void AdvanceIfNecessary() {
while (sequence_head_ != sequence_end_) {
while (rdn_it_ != sequence_head_->end()) {
if (rdn_it_->type == bssl::der::Input(bssl::kTypeOrganizationNameOid)) {
return;
}
++rdn_it_;
}
++sequence_head_;
if (sequence_head_ != sequence_end_) {
rdn_it_ = sequence_head_->begin();
}
}
}
bssl::RDNSequence::const_iterator sequence_head_;
bssl::RDNSequence::const_iterator sequence_end_;
bssl::RelativeDistinguishedName::const_iterator rdn_it_;
};
bool ParseOrganizationBoundName(bssl::der::Input dn_without_sequence,
bssl::RDNSequence* out) {
if (!bssl::ParseNameValue(dn_without_sequence, out)) {
return false;
}
for (const auto& rdn : *out) {
for (const auto& attribute_type_and_value : rdn) {
if (attribute_type_and_value.type ==
bssl::der::Input(bssl::kTypeOrganizationNameOid)) {
return true;
}
}
}
return false;
}
bool AreCertsSameOrganization(const bssl::RDNSequence& leaf_rdn_sequence,
CRYPTO_BUFFER* org_cert) {
std::shared_ptr<const bssl::ParsedCertificate> parsed_org =
bssl::ParsedCertificate::Create(bssl::UpRef(org_cert),
bssl::ParseCertificateOptions(), nullptr);
if (!parsed_org)
return false;
if (parsed_org->has_name_constraints()) {
const bssl::NameConstraints& nc = parsed_org->name_constraints();
for (const auto& permitted_name : nc.permitted_subtrees().directory_names) {
bssl::RDNSequence tmp;
if (ParseOrganizationBoundName(permitted_name, &tmp))
return true;
}
}
bssl::RDNSequence org_rdn_sequence;
if (!bssl::ParseNameValue(parsed_org->normalized_subject(),
&org_rdn_sequence)) {
return false;
}
OrgAttributeFilter leaf_filter(leaf_rdn_sequence);
OrgAttributeFilter org_filter(org_rdn_sequence);
while (leaf_filter.IsValid() && org_filter.IsValid()) {
if (leaf_filter.GetAttribute().type != org_filter.GetAttribute().type ||
leaf_filter.GetAttribute().value_tag !=
org_filter.GetAttribute().value_tag ||
leaf_filter.GetAttribute().value != org_filter.GetAttribute().value) {
return false;
}
leaf_filter.Advance();
org_filter.Advance();
}
return !leaf_filter.IsValid() && !org_filter.IsValid();
}
}
ChromeRequireCTDelegate::ChromeRequireCTDelegate()
: url_matcher_(std::make_unique<url_matcher::URLMatcher>()), next_id_(0) {}
ChromeRequireCTDelegate::~ChromeRequireCTDelegate() = default;
net::RequireCTDelegate::CTRequirementLevel
ChromeRequireCTDelegate::IsCTRequiredForHost(
std::string_view hostname,
const net::X509Certificate* chain,
const std::vector<net::SHA256HashValue>& spki_hashes) const {
if (MatchHostname(hostname) || MatchSPKI(chain, spki_hashes)) {
return CTRequirementLevel::NOT_REQUIRED;
}
return CTRequirementLevel::REQUIRED;
}
void ChromeRequireCTDelegate::UpdateCTPolicies(
const std::vector<std::string>& excluded_hosts,
const std::vector<std::string>& excluded_spkis) {
url_matcher_ = std::make_unique<url_matcher::URLMatcher>();
filters_.clear();
next_id_ = 0;
url_matcher::URLMatcherConditionSet::Vector all_conditions;
AddFilters(excluded_hosts, &all_conditions);
url_matcher_->AddConditionSets(all_conditions);
ParseSpkiHashes(excluded_spkis, &spkis_);
}
bool ChromeRequireCTDelegate::MatchHostname(std::string_view hostname) const {
if (url_matcher_->IsEmpty())
return false;
std::set<base::MatcherStringPattern::ID> matching_ids =
url_matcher_->MatchURL(
GURL("https://" + net::HostPortPair(hostname, 443).HostForURL()));
if (matching_ids.empty())
return false;
return true;
}
bool ChromeRequireCTDelegate::MatchSPKI(
const net::X509Certificate* chain,
const std::vector<net::SHA256HashValue>& hashes) const {
if (spkis_.empty())
return false;
if (spkis_.contains(hashes.front())) {
return true;
}
std::vector<CRYPTO_BUFFER*> candidates;
auto intermediate_hashes = base::span(hashes).subspan(1u);
auto intermediate_buffers = chain->intermediate_buffers();
for (auto [hash, cert_buffer] :
base::zip(intermediate_hashes, intermediate_buffers)) {
if (spkis_.contains(hash)) {
candidates.push_back(cert_buffer.get());
}
}
if (candidates.empty()) {
return false;
}
CRYPTO_BUFFER* leaf_cert = chain->cert_buffer();
std::shared_ptr<const bssl::ParsedCertificate> parsed_leaf =
bssl::ParsedCertificate::Create(bssl::UpRef(leaf_cert),
bssl::ParseCertificateOptions(), nullptr);
if (!parsed_leaf)
return false;
bssl::RDNSequence leaf_rdn_sequence;
if (!ParseOrganizationBoundName(parsed_leaf->normalized_subject(),
&leaf_rdn_sequence)) {
return false;
}
for (auto* cert : candidates) {
if (AreCertsSameOrganization(leaf_rdn_sequence, cert)) {
return true;
}
}
return false;
}
void ChromeRequireCTDelegate::AddFilters(
const std::vector<std::string>& hosts,
url_matcher::URLMatcherConditionSet::Vector* conditions) {
for (const auto& pattern : hosts) {
Filter filter;
url::Parsed parsed;
std::string ignored_scheme = url_formatter::SegmentURL(pattern, &parsed);
if (parsed.host.is_empty())
continue;
std::string lc_host = base::ToLowerASCII(
std::string_view(pattern).substr(parsed.host.begin, parsed.host.len));
if (lc_host == "*") {
continue;
} else if (lc_host[0] == '.') {
lc_host.erase(0, 1);
filter.match_subdomains = false;
} else {
url::RawCanonOutputT<char> output;
url::CanonHostInfo host_info;
url::CanonicalizeHostVerbose(pattern, parsed.host, &output, &host_info);
if (host_info.family == url::CanonHostInfo::NEUTRAL) {
lc_host.insert(lc_host.begin(), '.');
filter.match_subdomains = true;
} else {
filter.match_subdomains = false;
}
}
filter.host_length = lc_host.size();
url_matcher::URLMatcherConditionFactory* condition_factory =
url_matcher_->condition_factory();
std::set<url_matcher::URLMatcherCondition> condition_set;
condition_set.insert(
filter.match_subdomains
? condition_factory->CreateHostSuffixCondition(lc_host)
: condition_factory->CreateHostEqualsCondition(lc_host));
conditions->push_back(
new url_matcher::URLMatcherConditionSet(next_id_, condition_set));
filters_[next_id_] = filter;
++next_id_;
}
}
void ChromeRequireCTDelegate::ParseSpkiHashes(
const std::vector<std::string> spki_list,
absl::flat_hash_set<net::SHA256HashValue>* hashes) const {
hashes->clear();
for (const auto& value : spki_list) {
net::HashValue hash;
if (!hash.FromString(value)) {
continue;
}
if (hash.tag() != net::HASH_VALUE_SHA256) {
continue;
}
hashes->insert(hash.sha256hashvalue());
}
}
}