blob: c5050115e523fa68dba790f96849e57ec1366588 [file] [log] [blame]
// 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/location.h"
#include "base/memory/ref_counted.h"
#include "base/sequenced_task_runner.h"
#include "base/stl_util.h"
#include "base/strings/string_util.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/internal/name_constraints.h"
#include "net/cert/internal/parse_name.h"
#include "net/cert/internal/parsed_certificate.h"
#include "net/cert/known_roots.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::TypeOrganizationNameOid())
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::TypeOrganizationNameOid())
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::TimeDelta::FromSeconds(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<url_matcher::URLMatcherConditionSet::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