blob: c75aec77af0434200181110f64677ed40ef94d5a [file] [log] [blame]
rsleevi96356f82016-06-30 09:01:201// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Ryan Sleevie5574e02018-05-15 04:37:235#include "components/certificate_transparency/chrome_require_ct_delegate.h"
rsleevi96356f82016-06-30 09:01:206
Ryan Sleevi3dabe0b2018-04-05 03:59:017#include <algorithm>
8#include <iterator>
rsleevi96356f82016-06-30 09:01:209#include <map>
10#include <set>
11#include <string>
Ryan Sleevi3dabe0b2018-04-05 03:59:0112#include <utility>
Doug Turner9e79cf0c2018-04-05 21:32:3013#include <vector>
rsleevi96356f82016-06-30 09:01:2014
15#include "base/bind.h"
16#include "base/callback.h"
17#include "base/location.h"
Ryan Sleevi3dabe0b2018-04-05 03:59:0118#include "base/memory/ref_counted.h"
rsleevi96356f82016-06-30 09:01:2019#include "base/sequenced_task_runner.h"
20#include "base/strings/string_util.h"
21#include "base/threading/sequenced_task_runner_handle.h"
22#include "base/values.h"
rsleevi96356f82016-06-30 09:01:2023#include "components/url_formatter/url_fixer.h"
24#include "components/url_matcher/url_matcher.h"
Ryan Sleevi3dabe0b2018-04-05 03:59:0125#include "crypto/sha2.h"
26#include "net/base/hash_value.h"
rsleevi96356f82016-06-30 09:01:2027#include "net/base/host_port_pair.h"
Ryan Sleevi3dabe0b2018-04-05 03:59:0128#include "net/cert/asn1_util.h"
29#include "net/cert/internal/name_constraints.h"
30#include "net/cert/internal/parse_name.h"
31#include "net/cert/internal/parsed_certificate.h"
32#include "net/cert/known_roots.h"
33#include "net/cert/x509_certificate.h"
34#include "net/cert/x509_util.h"
rsleevi96356f82016-06-30 09:01:2035
36namespace certificate_transparency {
37
Ryan Sleevi3dabe0b2018-04-05 03:59:0138namespace {
39
40// Helper that takes a given net::RDNSequence and returns only the
41// organizationName net::X509NameAttributes.
42class OrgAttributeFilter {
43 public:
44 // Creates a new OrgAttributeFilter for |sequence| that begins iterating at
45 // |head|. Note that |head| can be equal to |sequence.end()|, in which case,
46 // there are no organizationName attributes.
47 explicit OrgAttributeFilter(const net::RDNSequence& sequence)
48 : sequence_head_(sequence.begin()), sequence_end_(sequence.end()) {
49 if (sequence_head_ != sequence_end_) {
50 rdn_it_ = sequence_head_->begin();
51 AdvanceIfNecessary();
52 }
53 }
54
55 bool IsValid() const { return sequence_head_ != sequence_end_; }
56
57 const net::X509NameAttribute& GetAttribute() const {
58 DCHECK(IsValid());
59 return *rdn_it_;
60 }
61
62 void Advance() {
63 DCHECK(IsValid());
64 ++rdn_it_;
65 AdvanceIfNecessary();
66 }
67
68 private:
69 // If the current field is an organization field, does nothing, otherwise,
70 // advances the state to the next organization field, or, if no more are
71 // present, the end of the sequence.
72 void AdvanceIfNecessary() {
73 while (sequence_head_ != sequence_end_) {
74 while (rdn_it_ != sequence_head_->end()) {
75 if (rdn_it_->type == net::TypeOrganizationNameOid())
76 return;
77 ++rdn_it_;
78 }
79 ++sequence_head_;
80 if (sequence_head_ != sequence_end_) {
81 rdn_it_ = sequence_head_->begin();
82 }
83 }
84 }
85
86 net::RDNSequence::const_iterator sequence_head_;
87 net::RDNSequence::const_iterator sequence_end_;
88 net::RelativeDistinguishedName::const_iterator rdn_it_;
89};
90
91// Returns true if |dn_without_sequence| identifies an
92// organizationally-validated certificate, per the CA/Browser Forum's Baseline
93// Requirements, storing the parsed RDNSequence in |*out|.
94bool ParseOrganizationBoundName(net::der::Input dn_without_sequence,
95 net::RDNSequence* out) {
96 if (!net::ParseNameValue(dn_without_sequence, out))
97 return false;
98 for (const auto& rdn : *out) {
99 for (const auto& attribute_type_and_value : rdn) {
100 if (attribute_type_and_value.type == net::TypeOrganizationNameOid())
101 return true;
102 }
103 }
104 return false;
105}
106
107// Returns true if the certificate identified by |leaf_rdn_sequence| is
108// considered to be issued under the same organizational authority as
109// |org_cert|.
110bool AreCertsSameOrganization(const net::RDNSequence& leaf_rdn_sequence,
111 CRYPTO_BUFFER* org_cert) {
112 scoped_refptr<net::ParsedCertificate> parsed_org =
113 net::ParsedCertificate::Create(net::x509_util::DupCryptoBuffer(org_cert),
114 net::ParseCertificateOptions(), nullptr);
115 if (!parsed_org)
116 return false;
117
118 // If the candidate cert has nameConstraints, see if it has a
119 // permittedSubtrees nameConstraint over a DirectoryName that is
120 // organizationally-bound. If so, the enforcement of nameConstraints is
121 // sufficient to consider |org_cert| a match.
122 if (parsed_org->has_name_constraints()) {
123 const net::NameConstraints& nc = parsed_org->name_constraints();
124 for (const auto& permitted_name : nc.permitted_subtrees().directory_names) {
125 net::RDNSequence tmp;
126 if (ParseOrganizationBoundName(permitted_name, &tmp))
127 return true;
128 }
129 }
130
131 net::RDNSequence org_rdn_sequence;
132 if (!net::ParseNameValue(parsed_org->normalized_subject(), &org_rdn_sequence))
133 return false;
134
135 // Finally, try to match the organization fields within |leaf_rdn_sequence|
136 // to |org_rdn_sequence|. As |leaf_rdn_sequence| has already been checked
137 // for all the necessary fields, it's not necessary to check
138 // |org_rdn_sequence|. Iterate through all of the organization fields in
139 // each, doing a byte-for-byte equality check.
140 // Note that this does permit differences in the SET encapsulations between
141 // RelativeDistinguishedNames, although it does still require that the same
142 // number of organization fields appear, and with the same overall ordering.
143 // This is simply as an implementation simplification, and not done for
144 // semantic or technical reasons.
145 OrgAttributeFilter leaf_filter(leaf_rdn_sequence);
146 OrgAttributeFilter org_filter(org_rdn_sequence);
147 while (leaf_filter.IsValid() && org_filter.IsValid()) {
148 if (leaf_filter.GetAttribute().type != org_filter.GetAttribute().type ||
149 leaf_filter.GetAttribute().value_tag !=
150 org_filter.GetAttribute().value_tag ||
151 leaf_filter.GetAttribute().value != org_filter.GetAttribute().value) {
152 return false;
153 }
154 leaf_filter.Advance();
155 org_filter.Advance();
156 }
157
158 // Ensure all attributes were fully consumed.
159 return !leaf_filter.IsValid() && !org_filter.IsValid();
160}
161
162} // namespace
163
Ryan Sleevie5574e02018-05-15 04:37:23164ChromeRequireCTDelegate::ChromeRequireCTDelegate()
165 : url_matcher_(std::make_unique<url_matcher::URLMatcher>()), next_id_(0) {}
rsleevi96356f82016-06-30 09:01:20166
Ryan Sleevie5574e02018-05-15 04:37:23167ChromeRequireCTDelegate::~ChromeRequireCTDelegate() {}
rsleevi96356f82016-06-30 09:01:20168
169net::TransportSecurityState::RequireCTDelegate::CTRequirementLevel
Ryan Sleevie5574e02018-05-15 04:37:23170ChromeRequireCTDelegate::IsCTRequiredForHost(
Ryan Sleevi3dabe0b2018-04-05 03:59:01171 const std::string& hostname,
172 const net::X509Certificate* chain,
Ryan Sleevie5574e02018-05-15 04:37:23173 const net::HashValueVector& spki_hashes) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01174 bool ct_required = false;
175 if (MatchHostname(hostname, &ct_required) ||
Ryan Sleevie5574e02018-05-15 04:37:23176 MatchSPKI(chain, spki_hashes, &ct_required)) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01177 return ct_required ? CTRequirementLevel::REQUIRED
178 : CTRequirementLevel::NOT_REQUIRED;
179 }
180
Ryan Sleevie5574e02018-05-15 04:37:23181 // Compute >= 2018-05-01, rather than deal with possible fractional
182 // seconds.
183 const base::Time kMay_1_2018 =
184 base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(1525132800);
185 if (chain->valid_start() >= kMay_1_2018)
186 return CTRequirementLevel::REQUIRED;
187
Ryan Sleevi3dabe0b2018-04-05 03:59:01188 return CTRequirementLevel::DEFAULT;
189}
190
Ryan Sleevie5574e02018-05-15 04:37:23191void ChromeRequireCTDelegate::UpdateCTPolicies(
192 const std::vector<std::string>& required_hosts,
193 const std::vector<std::string>& excluded_hosts,
194 const std::vector<std::string>& excluded_spkis,
195 const std::vector<std::string>& excluded_legacy_spkis) {
196 url_matcher_ = std::make_unique<url_matcher::URLMatcher>();
197 filters_.clear();
198 next_id_ = 0;
199
200 url_matcher::URLMatcherConditionSet::Vector all_conditions;
201 AddFilters(true, required_hosts, &all_conditions);
202 AddFilters(false, excluded_hosts, &all_conditions);
203
204 url_matcher_->AddConditionSets(all_conditions);
205
206 ParseSpkiHashes(excluded_spkis, &spkis_);
207 ParseSpkiHashes(excluded_legacy_spkis, &legacy_spkis_);
208
209 // Filter out SPKIs that aren't for legacy CAs.
210 legacy_spkis_.erase(
211 std::remove_if(legacy_spkis_.begin(), legacy_spkis_.end(),
212 [](const net::HashValue& hash) {
213 if (!net::IsLegacyPubliclyTrustedCA(hash)) {
214 LOG(ERROR) << "Non-legacy SPKI configured "
215 << hash.ToString();
216 return true;
217 }
218 return false;
219 }),
220 legacy_spkis_.end());
221}
222
223bool ChromeRequireCTDelegate::MatchHostname(const std::string& hostname,
224 bool* ct_required) const {
Ryan Sleevi3dabe0b2018-04-05 03:59:01225 if (url_matcher_->IsEmpty())
226 return false;
227
rsleevi96356f82016-06-30 09:01:20228 // Scheme and port are ignored by the policy, so it's OK to construct a
229 // new GURL here. However, |hostname| is in network form, not URL form,
230 // so it's necessary to wrap IPv6 addresses in brackets.
231 std::set<url_matcher::URLMatcherConditionSet::ID> matching_ids =
232 url_matcher_->MatchURL(
233 GURL("https://" + net::HostPortPair(hostname, 443).HostForURL()));
234 if (matching_ids.empty())
Ryan Sleevi3dabe0b2018-04-05 03:59:01235 return false;
rsleevi96356f82016-06-30 09:01:20236
237 // Determine the overall policy by determining the most specific policy.
238 std::map<url_matcher::URLMatcherConditionSet::ID, Filter>::const_iterator it =
239 filters_.begin();
240 const Filter* active_filter = nullptr;
241 for (const auto& match : matching_ids) {
242 // Because both |filters_| and |matching_ids| are sorted on the ID,
243 // treat both as forward-only iterators.
244 while (it != filters_.end() && it->first < match)
245 ++it;
246 if (it == filters_.end()) {
247 NOTREACHED();
248 break;
249 }
250
251 if (!active_filter || FilterTakesPrecedence(it->second, *active_filter))
252 active_filter = &it->second;
253 }
254 CHECK(active_filter);
255
Ryan Sleevi3dabe0b2018-04-05 03:59:01256 *ct_required = active_filter->ct_required;
257 return true;
rsleevi96356f82016-06-30 09:01:20258}
259
Ryan Sleevie5574e02018-05-15 04:37:23260bool ChromeRequireCTDelegate::MatchSPKI(const net::X509Certificate* chain,
261 const net::HashValueVector& hashes,
262 bool* ct_required) const {
Ryan Sleevi3dabe0b2018-04-05 03:59:01263 // Try to scan legacy SPKIs first, if any, since they will only require
264 // comparing hash values.
265 if (!legacy_spkis_.empty()) {
266 for (const auto& hash : hashes) {
267 if (std::binary_search(legacy_spkis_.begin(), legacy_spkis_.end(),
268 hash)) {
269 *ct_required = false;
270 return true;
271 }
272 }
273 }
274
275 if (spkis_.empty())
276 return false;
277
278 // Scan the constrained SPKIs via |hashes| first, as an optimization. If
279 // there are matches, the SPKI hash will have to be recomputed anyways to
280 // find the matching certificate, but avoid recomputing all the hashes for
281 // the case where there is no match.
282 net::HashValueVector matches;
283 for (const auto& hash : hashes) {
284 if (std::binary_search(spkis_.begin(), spkis_.end(), hash)) {
285 matches.push_back(hash);
286 }
287 }
288 if (matches.empty())
289 return false;
290
291 CRYPTO_BUFFER* leaf_cert = chain->cert_buffer();
292
293 // As an optimization, since the leaf is allowed to be listed as an SPKI,
294 // a match on the leaf's SPKI hash can return early, without comparing
295 // the organization information to itself.
296 net::HashValue hash;
297 if (net::x509_util::CalculateSha256SpkiHash(leaf_cert, &hash) &&
298 std::find(matches.begin(), matches.end(), hash) != matches.end()) {
299 *ct_required = false;
300 return true;
301 }
302
303 // If there was a match (or multiple matches), it's necessary to recompute
304 // the hashes to find the associated certificate.
305 std::vector<CRYPTO_BUFFER*> candidates;
306 for (const auto& buffer : chain->intermediate_buffers()) {
307 if (net::x509_util::CalculateSha256SpkiHash(buffer.get(), &hash) &&
308 std::find(matches.begin(), matches.end(), hash) != matches.end()) {
309 candidates.push_back(buffer.get());
310 }
311 }
312
313 if (candidates.empty())
314 return false;
315
316 scoped_refptr<net::ParsedCertificate> parsed_leaf =
317 net::ParsedCertificate::Create(net::x509_util::DupCryptoBuffer(leaf_cert),
318 net::ParseCertificateOptions(), nullptr);
319 if (!parsed_leaf)
320 return false;
321 // If the leaf is not organizationally-bound, it's not a match.
322 net::RDNSequence leaf_rdn_sequence;
323 if (!ParseOrganizationBoundName(parsed_leaf->normalized_subject(),
324 &leaf_rdn_sequence)) {
325 return false;
326 }
327
328 for (auto* cert : candidates) {
329 if (AreCertsSameOrganization(leaf_rdn_sequence, cert)) {
330 *ct_required = false;
331 return true;
332 }
333 }
334
335 return false;
336}
337
Ryan Sleevie5574e02018-05-15 04:37:23338void ChromeRequireCTDelegate::AddFilters(
rsleevi96356f82016-06-30 09:01:20339 bool ct_required,
Doug Turner9e79cf0c2018-04-05 21:32:30340 const std::vector<std::string>& hosts,
rsleevi96356f82016-06-30 09:01:20341 url_matcher::URLMatcherConditionSet::Vector* conditions) {
Doug Turner9e79cf0c2018-04-05 21:32:30342 for (const auto& pattern : hosts) {
rsleevi96356f82016-06-30 09:01:20343 Filter filter;
344 filter.ct_required = ct_required;
345
346 // Parse the pattern just to the hostname, ignoring all other portions of
347 // the URL.
348 url::Parsed parsed;
349 std::string ignored_scheme = url_formatter::SegmentURL(pattern, &parsed);
350 if (!parsed.host.is_nonempty())
351 continue; // If there is no host to match, can't apply the filter.
352
353 std::string lc_host = base::ToLowerASCII(
354 base::StringPiece(pattern).substr(parsed.host.begin, parsed.host.len));
355 if (lc_host == "*") {
356 // Wildcard hosts are not allowed and ignored.
357 continue;
358 } else if (lc_host[0] == '.') {
359 // A leading dot means exact match and to not match subdomains.
360 lc_host.erase(0, 1);
361 filter.match_subdomains = false;
362 } else {
363 // Canonicalize the host to make sure it's an actual hostname, not an
364 // IP address or a BROKEN canonical host, as matching subdomains is
365 // not desirable for those.
366 url::RawCanonOutputT<char> output;
367 url::CanonHostInfo host_info;
368 url::CanonicalizeHostVerbose(pattern.c_str(), parsed.host, &output,
369 &host_info);
370 // TODO(rsleevi): Use canonicalized form?
371 if (host_info.family == url::CanonHostInfo::NEUTRAL) {
372 // Match subdomains (implicit by the omission of '.'). Add in a
373 // leading dot to make sure matches only happen at the domain
374 // component boundary.
375 lc_host.insert(lc_host.begin(), '.');
376 filter.match_subdomains = true;
377 } else {
378 filter.match_subdomains = false;
379 }
380 }
381 filter.host_length = lc_host.size();
382
383 // Create a condition for the URLMatcher that matches the hostname (and/or
384 // subdomains).
385 url_matcher::URLMatcherConditionFactory* condition_factory =
386 url_matcher_->condition_factory();
387 std::set<url_matcher::URLMatcherCondition> condition_set;
388 condition_set.insert(
389 filter.match_subdomains
390 ? condition_factory->CreateHostSuffixCondition(lc_host)
391 : condition_factory->CreateHostEqualsCondition(lc_host));
392 conditions->push_back(
393 new url_matcher::URLMatcherConditionSet(next_id_, condition_set));
394 filters_[next_id_] = filter;
395 ++next_id_;
396 }
397}
398
Ryan Sleevie5574e02018-05-15 04:37:23399void ChromeRequireCTDelegate::ParseSpkiHashes(
400 const std::vector<std::string> spki_list,
Ryan Sleevi3dabe0b2018-04-05 03:59:01401 net::HashValueVector* hashes) const {
402 hashes->clear();
Ryan Sleevie5574e02018-05-15 04:37:23403 for (const auto& value : spki_list) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01404 net::HashValue hash;
Doug Turner9e79cf0c2018-04-05 21:32:30405 if (!hash.FromString(value)) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01406 continue;
407 }
408 hashes->push_back(std::move(hash));
409 }
410 std::sort(hashes->begin(), hashes->end());
411}
412
Ryan Sleevie5574e02018-05-15 04:37:23413bool ChromeRequireCTDelegate::FilterTakesPrecedence(const Filter& lhs,
414 const Filter& rhs) const {
rsleevi96356f82016-06-30 09:01:20415 if (lhs.match_subdomains != rhs.match_subdomains)
416 return !lhs.match_subdomains; // Prefer the more explicit policy.
417
418 if (lhs.host_length != rhs.host_length)
419 return lhs.host_length > rhs.host_length; // Prefer the longer host match.
420
421 if (lhs.ct_required != rhs.ct_required)
422 return lhs.ct_required; // Prefer the policy that requires CT.
423
424 return false;
425}
426
rsleevi96356f82016-06-30 09:01:20427} // namespace certificate_transparency