123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- // Copyright 2016 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 "components/certificate_transparency/chrome_require_ct_delegate.h"
- #include <algorithm>
- #include <iterator>
- #include <map>
- #include <set>
- #include <string>
- #include <utility>
- #include <vector>
- #include "base/bind.h"
- #include "base/callback.h"
- #include "base/containers/contains.h"
- #include "base/containers/cxx20_erase.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/threading/sequenced_task_runner_handle.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/pki/name_constraints.h"
- #include "net/cert/pki/parse_name.h"
- #include "net/cert/pki/parsed_certificate.h"
- #include "net/cert/x509_certificate.h"
- #include "net/cert/x509_util.h"
- namespace certificate_transparency {
- namespace {
- // Helper that takes a given net::RDNSequence and returns only the
- // organizationName net::X509NameAttributes.
- class OrgAttributeFilter {
- public:
- // Creates a new OrgAttributeFilter for |sequence| that begins iterating at
- // |head|. Note that |head| can be equal to |sequence.end()|, in which case,
- // there are no organizationName attributes.
- explicit OrgAttributeFilter(const net::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 net::X509NameAttribute& GetAttribute() const {
- DCHECK(IsValid());
- return *rdn_it_;
- }
- void Advance() {
- DCHECK(IsValid());
- ++rdn_it_;
- AdvanceIfNecessary();
- }
- private:
- // If the current field is an organization field, does nothing, otherwise,
- // advances the state to the next organization field, or, if no more are
- // present, the end of the sequence.
- void AdvanceIfNecessary() {
- while (sequence_head_ != sequence_end_) {
- while (rdn_it_ != sequence_head_->end()) {
- if (rdn_it_->type == net::der::Input(net::kTypeOrganizationNameOid))
- return;
- ++rdn_it_;
- }
- ++sequence_head_;
- if (sequence_head_ != sequence_end_) {
- rdn_it_ = sequence_head_->begin();
- }
- }
- }
- net::RDNSequence::const_iterator sequence_head_;
- net::RDNSequence::const_iterator sequence_end_;
- net::RelativeDistinguishedName::const_iterator rdn_it_;
- };
- // Returns true if |dn_without_sequence| identifies an
- // organizationally-validated certificate, per the CA/Browser Forum's Baseline
- // Requirements, storing the parsed RDNSequence in |*out|.
- bool ParseOrganizationBoundName(net::der::Input dn_without_sequence,
- net::RDNSequence* out) {
- if (!net::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 ==
- net::der::Input(net::kTypeOrganizationNameOid)) {
- return true;
- }
- }
- }
- return false;
- }
- // Returns true if the certificate identified by |leaf_rdn_sequence| is
- // considered to be issued under the same organizational authority as
- // |org_cert|.
- bool AreCertsSameOrganization(const net::RDNSequence& leaf_rdn_sequence,
- CRYPTO_BUFFER* org_cert) {
- scoped_refptr<net::ParsedCertificate> parsed_org =
- net::ParsedCertificate::Create(bssl::UpRef(org_cert),
- net::ParseCertificateOptions(), nullptr);
- if (!parsed_org)
- return false;
- // If the candidate cert has nameConstraints, see if it has a
- // permittedSubtrees nameConstraint over a DirectoryName that is
- // organizationally-bound. If so, the enforcement of nameConstraints is
- // sufficient to consider |org_cert| a match.
- if (parsed_org->has_name_constraints()) {
- const net::NameConstraints& nc = parsed_org->name_constraints();
- for (const auto& permitted_name : nc.permitted_subtrees().directory_names) {
- net::RDNSequence tmp;
- if (ParseOrganizationBoundName(permitted_name, &tmp))
- return true;
- }
- }
- net::RDNSequence org_rdn_sequence;
- if (!net::ParseNameValue(parsed_org->normalized_subject(), &org_rdn_sequence))
- return false;
- // Finally, try to match the organization fields within |leaf_rdn_sequence|
- // to |org_rdn_sequence|. As |leaf_rdn_sequence| has already been checked
- // for all the necessary fields, it's not necessary to check
- // |org_rdn_sequence|. Iterate through all of the organization fields in
- // each, doing a byte-for-byte equality check.
- // Note that this does permit differences in the SET encapsulations between
- // RelativeDistinguishedNames, although it does still require that the same
- // number of organization fields appear, and with the same overall ordering.
- // This is simply as an implementation simplification, and not done for
- // semantic or technical reasons.
- 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();
- }
- // Ensure all attributes were fully consumed.
- return !leaf_filter.IsValid() && !org_filter.IsValid();
- }
- } // namespace
- ChromeRequireCTDelegate::ChromeRequireCTDelegate()
- : url_matcher_(std::make_unique<url_matcher::URLMatcher>()), next_id_(0) {}
- ChromeRequireCTDelegate::~ChromeRequireCTDelegate() {}
- net::TransportSecurityState::RequireCTDelegate::CTRequirementLevel
- ChromeRequireCTDelegate::IsCTRequiredForHost(
- const std::string& hostname,
- const net::X509Certificate* chain,
- const net::HashValueVector& spki_hashes) {
- bool ct_required = false;
- if (MatchHostname(hostname, &ct_required) ||
- MatchSPKI(chain, spki_hashes, &ct_required)) {
- return ct_required ? CTRequirementLevel::REQUIRED
- : CTRequirementLevel::NOT_REQUIRED;
- }
- // Compute >= 2018-05-01, rather than deal with possible fractional
- // seconds.
- const base::Time kMay_1_2018 =
- base::Time::UnixEpoch() + base::Seconds(1525132800);
- if (chain->valid_start() >= kMay_1_2018)
- return CTRequirementLevel::REQUIRED;
- return CTRequirementLevel::DEFAULT;
- }
- void ChromeRequireCTDelegate::UpdateCTPolicies(
- const std::vector<std::string>& required_hosts,
- const std::vector<std::string>& excluded_hosts,
- const std::vector<std::string>& excluded_spkis,
- const std::vector<std::string>& excluded_legacy_spkis) {
- url_matcher_ = std::make_unique<url_matcher::URLMatcher>();
- filters_.clear();
- next_id_ = 0;
- url_matcher::URLMatcherConditionSet::Vector all_conditions;
- AddFilters(true, required_hosts, &all_conditions);
- AddFilters(false, excluded_hosts, &all_conditions);
- url_matcher_->AddConditionSets(all_conditions);
- ParseSpkiHashes(excluded_spkis, &spkis_);
- ParseSpkiHashes(excluded_legacy_spkis, &legacy_spkis_);
- // Filter out SPKIs that aren't for legacy CAs.
- base::EraseIf(legacy_spkis_, [](const net::HashValue& hash) {
- if (!net::IsLegacyPubliclyTrustedCA(hash)) {
- LOG(ERROR) << "Non-legacy SPKI configured " << hash.ToString();
- return true;
- }
- return false;
- });
- }
- bool ChromeRequireCTDelegate::MatchHostname(const std::string& hostname,
- bool* ct_required) const {
- if (url_matcher_->IsEmpty())
- return false;
- // Scheme and port are ignored by the policy, so it's OK to construct a
- // new GURL here. However, |hostname| is in network form, not URL form,
- // so it's necessary to wrap IPv6 addresses in brackets.
- std::set<base::MatcherStringPattern::ID> matching_ids =
- url_matcher_->MatchURL(
- GURL("https://" + net::HostPortPair(hostname, 443).HostForURL()));
- if (matching_ids.empty())
- return false;
- // Determine the overall policy by determining the most specific policy.
- auto it = filters_.begin();
- const Filter* active_filter = nullptr;
- for (const auto& match : matching_ids) {
- // Because both |filters_| and |matching_ids| are sorted on the ID,
- // treat both as forward-only iterators.
- while (it != filters_.end() && it->first < match)
- ++it;
- if (it == filters_.end()) {
- NOTREACHED();
- break;
- }
- if (!active_filter || FilterTakesPrecedence(it->second, *active_filter))
- active_filter = &it->second;
- }
- CHECK(active_filter);
- *ct_required = active_filter->ct_required;
- return true;
- }
- bool ChromeRequireCTDelegate::MatchSPKI(const net::X509Certificate* chain,
- const net::HashValueVector& hashes,
- bool* ct_required) const {
- // Try to scan legacy SPKIs first, if any, since they will only require
- // comparing hash values.
- if (!legacy_spkis_.empty()) {
- for (const auto& hash : hashes) {
- if (std::binary_search(legacy_spkis_.begin(), legacy_spkis_.end(),
- hash)) {
- *ct_required = false;
- return true;
- }
- }
- }
- if (spkis_.empty())
- return false;
- // Scan the constrained SPKIs via |hashes| first, as an optimization. If
- // there are matches, the SPKI hash will have to be recomputed anyways to
- // find the matching certificate, but avoid recomputing all the hashes for
- // the case where there is no match.
- net::HashValueVector matches;
- for (const auto& hash : hashes) {
- if (std::binary_search(spkis_.begin(), spkis_.end(), hash)) {
- matches.push_back(hash);
- }
- }
- if (matches.empty())
- return false;
- CRYPTO_BUFFER* leaf_cert = chain->cert_buffer();
- // As an optimization, since the leaf is allowed to be listed as an SPKI,
- // a match on the leaf's SPKI hash can return early, without comparing
- // the organization information to itself.
- net::HashValue hash;
- if (net::x509_util::CalculateSha256SpkiHash(leaf_cert, &hash) &&
- base::Contains(matches, hash)) {
- *ct_required = false;
- return true;
- }
- // If there was a match (or multiple matches), it's necessary to recompute
- // the hashes to find the associated certificate.
- std::vector<CRYPTO_BUFFER*> candidates;
- for (const auto& buffer : chain->intermediate_buffers()) {
- if (net::x509_util::CalculateSha256SpkiHash(buffer.get(), &hash) &&
- base::Contains(matches, hash)) {
- candidates.push_back(buffer.get());
- }
- }
- if (candidates.empty())
- return false;
- scoped_refptr<net::ParsedCertificate> parsed_leaf =
- net::ParsedCertificate::Create(bssl::UpRef(leaf_cert),
- net::ParseCertificateOptions(), nullptr);
- if (!parsed_leaf)
- return false;
- // If the leaf is not organizationally-bound, it's not a match.
- net::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)) {
- *ct_required = false;
- return true;
- }
- }
- return false;
- }
- void ChromeRequireCTDelegate::AddFilters(
- bool ct_required,
- const std::vector<std::string>& hosts,
- url_matcher::URLMatcherConditionSet::Vector* conditions) {
- for (const auto& pattern : hosts) {
- Filter filter;
- filter.ct_required = ct_required;
- // Parse the pattern just to the hostname, ignoring all other portions of
- // the URL.
- url::Parsed parsed;
- std::string ignored_scheme = url_formatter::SegmentURL(pattern, &parsed);
- if (!parsed.host.is_nonempty())
- continue; // If there is no host to match, can't apply the filter.
- std::string lc_host = base::ToLowerASCII(
- base::StringPiece(pattern).substr(parsed.host.begin, parsed.host.len));
- if (lc_host == "*") {
- // Wildcard hosts are not allowed and ignored.
- continue;
- } else if (lc_host[0] == '.') {
- // A leading dot means exact match and to not match subdomains.
- lc_host.erase(0, 1);
- filter.match_subdomains = false;
- } else {
- // Canonicalize the host to make sure it's an actual hostname, not an
- // IP address or a BROKEN canonical host, as matching subdomains is
- // not desirable for those.
- url::RawCanonOutputT<char> output;
- url::CanonHostInfo host_info;
- url::CanonicalizeHostVerbose(pattern.c_str(), parsed.host, &output,
- &host_info);
- // TODO(rsleevi): Use canonicalized form?
- if (host_info.family == url::CanonHostInfo::NEUTRAL) {
- // Match subdomains (implicit by the omission of '.'). Add in a
- // leading dot to make sure matches only happen at the domain
- // component boundary.
- lc_host.insert(lc_host.begin(), '.');
- filter.match_subdomains = true;
- } else {
- filter.match_subdomains = false;
- }
- }
- filter.host_length = lc_host.size();
- // Create a condition for the URLMatcher that matches the hostname (and/or
- // subdomains).
- 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,
- net::HashValueVector* hashes) const {
- hashes->clear();
- for (const auto& value : spki_list) {
- net::HashValue hash;
- if (!hash.FromString(value)) {
- continue;
- }
- hashes->push_back(std::move(hash));
- }
- std::sort(hashes->begin(), hashes->end());
- }
- bool ChromeRequireCTDelegate::FilterTakesPrecedence(const Filter& lhs,
- const Filter& rhs) const {
- if (lhs.match_subdomains != rhs.match_subdomains)
- return !lhs.match_subdomains; // Prefer the more explicit policy.
- if (lhs.host_length != rhs.host_length)
- return lhs.host_length > rhs.host_length; // Prefer the longer host match.
- if (lhs.ct_required != rhs.ct_required)
- return lhs.ct_required; // Prefer the policy that requires CT.
- return false;
- }
- } // namespace certificate_transparency
|