blob: c5050115e523fa68dba790f96849e57ec1366588 [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"
Hwanseung Lee231c90d2018-06-28 15:21:1220#include "base/stl_util.h"
rsleevi96356f82016-06-30 09:01:2021#include "base/strings/string_util.h"
22#include "base/threading/sequenced_task_runner_handle.h"
23#include "base/values.h"
rsleevi96356f82016-06-30 09:01:2024#include "components/url_formatter/url_fixer.h"
25#include "components/url_matcher/url_matcher.h"
Ryan Sleevi3dabe0b2018-04-05 03:59:0126#include "crypto/sha2.h"
27#include "net/base/hash_value.h"
rsleevi96356f82016-06-30 09:01:2028#include "net/base/host_port_pair.h"
Ryan Sleevi3dabe0b2018-04-05 03:59:0129#include "net/cert/asn1_util.h"
30#include "net/cert/internal/name_constraints.h"
31#include "net/cert/internal/parse_name.h"
32#include "net/cert/internal/parsed_certificate.h"
33#include "net/cert/known_roots.h"
34#include "net/cert/x509_certificate.h"
35#include "net/cert/x509_util.h"
rsleevi96356f82016-06-30 09:01:2036
37namespace certificate_transparency {
38
Ryan Sleevi3dabe0b2018-04-05 03:59:0139namespace {
40
41// Helper that takes a given net::RDNSequence and returns only the
42// organizationName net::X509NameAttributes.
43class OrgAttributeFilter {
44 public:
45 // Creates a new OrgAttributeFilter for |sequence| that begins iterating at
46 // |head|. Note that |head| can be equal to |sequence.end()|, in which case,
47 // there are no organizationName attributes.
48 explicit OrgAttributeFilter(const net::RDNSequence& sequence)
49 : sequence_head_(sequence.begin()), sequence_end_(sequence.end()) {
50 if (sequence_head_ != sequence_end_) {
51 rdn_it_ = sequence_head_->begin();
52 AdvanceIfNecessary();
53 }
54 }
55
56 bool IsValid() const { return sequence_head_ != sequence_end_; }
57
58 const net::X509NameAttribute& GetAttribute() const {
59 DCHECK(IsValid());
60 return *rdn_it_;
61 }
62
63 void Advance() {
64 DCHECK(IsValid());
65 ++rdn_it_;
66 AdvanceIfNecessary();
67 }
68
69 private:
70 // If the current field is an organization field, does nothing, otherwise,
71 // advances the state to the next organization field, or, if no more are
72 // present, the end of the sequence.
73 void AdvanceIfNecessary() {
74 while (sequence_head_ != sequence_end_) {
75 while (rdn_it_ != sequence_head_->end()) {
76 if (rdn_it_->type == net::TypeOrganizationNameOid())
77 return;
78 ++rdn_it_;
79 }
80 ++sequence_head_;
81 if (sequence_head_ != sequence_end_) {
82 rdn_it_ = sequence_head_->begin();
83 }
84 }
85 }
86
87 net::RDNSequence::const_iterator sequence_head_;
88 net::RDNSequence::const_iterator sequence_end_;
89 net::RelativeDistinguishedName::const_iterator rdn_it_;
90};
91
92// Returns true if |dn_without_sequence| identifies an
93// organizationally-validated certificate, per the CA/Browser Forum's Baseline
94// Requirements, storing the parsed RDNSequence in |*out|.
95bool ParseOrganizationBoundName(net::der::Input dn_without_sequence,
96 net::RDNSequence* out) {
97 if (!net::ParseNameValue(dn_without_sequence, out))
98 return false;
99 for (const auto& rdn : *out) {
100 for (const auto& attribute_type_and_value : rdn) {
101 if (attribute_type_and_value.type == net::TypeOrganizationNameOid())
102 return true;
103 }
104 }
105 return false;
106}
107
108// Returns true if the certificate identified by |leaf_rdn_sequence| is
109// considered to be issued under the same organizational authority as
110// |org_cert|.
111bool AreCertsSameOrganization(const net::RDNSequence& leaf_rdn_sequence,
112 CRYPTO_BUFFER* org_cert) {
113 scoped_refptr<net::ParsedCertificate> parsed_org =
David Benjamin4db85cf2018-07-10 16:10:04114 net::ParsedCertificate::Create(bssl::UpRef(org_cert),
Ryan Sleevi3dabe0b2018-04-05 03:59:01115 net::ParseCertificateOptions(), nullptr);
116 if (!parsed_org)
117 return false;
118
119 // If the candidate cert has nameConstraints, see if it has a
120 // permittedSubtrees nameConstraint over a DirectoryName that is
121 // organizationally-bound. If so, the enforcement of nameConstraints is
122 // sufficient to consider |org_cert| a match.
123 if (parsed_org->has_name_constraints()) {
124 const net::NameConstraints& nc = parsed_org->name_constraints();
125 for (const auto& permitted_name : nc.permitted_subtrees().directory_names) {
126 net::RDNSequence tmp;
127 if (ParseOrganizationBoundName(permitted_name, &tmp))
128 return true;
129 }
130 }
131
132 net::RDNSequence org_rdn_sequence;
133 if (!net::ParseNameValue(parsed_org->normalized_subject(), &org_rdn_sequence))
134 return false;
135
136 // Finally, try to match the organization fields within |leaf_rdn_sequence|
137 // to |org_rdn_sequence|. As |leaf_rdn_sequence| has already been checked
138 // for all the necessary fields, it's not necessary to check
139 // |org_rdn_sequence|. Iterate through all of the organization fields in
140 // each, doing a byte-for-byte equality check.
141 // Note that this does permit differences in the SET encapsulations between
142 // RelativeDistinguishedNames, although it does still require that the same
143 // number of organization fields appear, and with the same overall ordering.
144 // This is simply as an implementation simplification, and not done for
145 // semantic or technical reasons.
146 OrgAttributeFilter leaf_filter(leaf_rdn_sequence);
147 OrgAttributeFilter org_filter(org_rdn_sequence);
148 while (leaf_filter.IsValid() && org_filter.IsValid()) {
149 if (leaf_filter.GetAttribute().type != org_filter.GetAttribute().type ||
150 leaf_filter.GetAttribute().value_tag !=
151 org_filter.GetAttribute().value_tag ||
152 leaf_filter.GetAttribute().value != org_filter.GetAttribute().value) {
153 return false;
154 }
155 leaf_filter.Advance();
156 org_filter.Advance();
157 }
158
159 // Ensure all attributes were fully consumed.
160 return !leaf_filter.IsValid() && !org_filter.IsValid();
161}
162
163} // namespace
164
Ryan Sleevie5574e02018-05-15 04:37:23165ChromeRequireCTDelegate::ChromeRequireCTDelegate()
166 : url_matcher_(std::make_unique<url_matcher::URLMatcher>()), next_id_(0) {}
rsleevi96356f82016-06-30 09:01:20167
Ryan Sleevie5574e02018-05-15 04:37:23168ChromeRequireCTDelegate::~ChromeRequireCTDelegate() {}
rsleevi96356f82016-06-30 09:01:20169
170net::TransportSecurityState::RequireCTDelegate::CTRequirementLevel
Ryan Sleevie5574e02018-05-15 04:37:23171ChromeRequireCTDelegate::IsCTRequiredForHost(
Ryan Sleevi3dabe0b2018-04-05 03:59:01172 const std::string& hostname,
173 const net::X509Certificate* chain,
Ryan Sleevie5574e02018-05-15 04:37:23174 const net::HashValueVector& spki_hashes) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01175 bool ct_required = false;
176 if (MatchHostname(hostname, &ct_required) ||
Ryan Sleevie5574e02018-05-15 04:37:23177 MatchSPKI(chain, spki_hashes, &ct_required)) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01178 return ct_required ? CTRequirementLevel::REQUIRED
179 : CTRequirementLevel::NOT_REQUIRED;
180 }
181
Ryan Sleevie5574e02018-05-15 04:37:23182 // Compute >= 2018-05-01, rather than deal with possible fractional
183 // seconds.
184 const base::Time kMay_1_2018 =
185 base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(1525132800);
186 if (chain->valid_start() >= kMay_1_2018)
187 return CTRequirementLevel::REQUIRED;
188
Ryan Sleevi3dabe0b2018-04-05 03:59:01189 return CTRequirementLevel::DEFAULT;
190}
191
Ryan Sleevie5574e02018-05-15 04:37:23192void ChromeRequireCTDelegate::UpdateCTPolicies(
193 const std::vector<std::string>& required_hosts,
194 const std::vector<std::string>& excluded_hosts,
195 const std::vector<std::string>& excluded_spkis,
196 const std::vector<std::string>& excluded_legacy_spkis) {
197 url_matcher_ = std::make_unique<url_matcher::URLMatcher>();
198 filters_.clear();
199 next_id_ = 0;
200
201 url_matcher::URLMatcherConditionSet::Vector all_conditions;
202 AddFilters(true, required_hosts, &all_conditions);
203 AddFilters(false, excluded_hosts, &all_conditions);
204
205 url_matcher_->AddConditionSets(all_conditions);
206
207 ParseSpkiHashes(excluded_spkis, &spkis_);
208 ParseSpkiHashes(excluded_legacy_spkis, &legacy_spkis_);
209
210 // Filter out SPKIs that aren't for legacy CAs.
Jdragona248d5c2018-08-24 12:46:42211 base::EraseIf(legacy_spkis_, [](const net::HashValue& hash) {
212 if (!net::IsLegacyPubliclyTrustedCA(hash)) {
213 LOG(ERROR) << "Non-legacy SPKI configured " << hash.ToString();
214 return true;
215 }
216 return false;
217 });
Ryan Sleevie5574e02018-05-15 04:37:23218}
219
220bool ChromeRequireCTDelegate::MatchHostname(const std::string& hostname,
221 bool* ct_required) const {
Ryan Sleevi3dabe0b2018-04-05 03:59:01222 if (url_matcher_->IsEmpty())
223 return false;
224
rsleevi96356f82016-06-30 09:01:20225 // Scheme and port are ignored by the policy, so it's OK to construct a
226 // new GURL here. However, |hostname| is in network form, not URL form,
227 // so it's necessary to wrap IPv6 addresses in brackets.
228 std::set<url_matcher::URLMatcherConditionSet::ID> matching_ids =
229 url_matcher_->MatchURL(
230 GURL("https://" + net::HostPortPair(hostname, 443).HostForURL()));
231 if (matching_ids.empty())
Ryan Sleevi3dabe0b2018-04-05 03:59:01232 return false;
rsleevi96356f82016-06-30 09:01:20233
234 // Determine the overall policy by determining the most specific policy.
jdoerrie3feb1852018-10-05 12:16:44235 auto it = filters_.begin();
rsleevi96356f82016-06-30 09:01:20236 const Filter* active_filter = nullptr;
237 for (const auto& match : matching_ids) {
238 // Because both |filters_| and |matching_ids| are sorted on the ID,
239 // treat both as forward-only iterators.
240 while (it != filters_.end() && it->first < match)
241 ++it;
242 if (it == filters_.end()) {
243 NOTREACHED();
244 break;
245 }
246
247 if (!active_filter || FilterTakesPrecedence(it->second, *active_filter))
248 active_filter = &it->second;
249 }
250 CHECK(active_filter);
251
Ryan Sleevi3dabe0b2018-04-05 03:59:01252 *ct_required = active_filter->ct_required;
253 return true;
rsleevi96356f82016-06-30 09:01:20254}
255
Ryan Sleevie5574e02018-05-15 04:37:23256bool ChromeRequireCTDelegate::MatchSPKI(const net::X509Certificate* chain,
257 const net::HashValueVector& hashes,
258 bool* ct_required) const {
Ryan Sleevi3dabe0b2018-04-05 03:59:01259 // Try to scan legacy SPKIs first, if any, since they will only require
260 // comparing hash values.
261 if (!legacy_spkis_.empty()) {
262 for (const auto& hash : hashes) {
263 if (std::binary_search(legacy_spkis_.begin(), legacy_spkis_.end(),
264 hash)) {
265 *ct_required = false;
266 return true;
267 }
268 }
269 }
270
271 if (spkis_.empty())
272 return false;
273
274 // Scan the constrained SPKIs via |hashes| first, as an optimization. If
275 // there are matches, the SPKI hash will have to be recomputed anyways to
276 // find the matching certificate, but avoid recomputing all the hashes for
277 // the case where there is no match.
278 net::HashValueVector matches;
279 for (const auto& hash : hashes) {
280 if (std::binary_search(spkis_.begin(), spkis_.end(), hash)) {
281 matches.push_back(hash);
282 }
283 }
284 if (matches.empty())
285 return false;
286
287 CRYPTO_BUFFER* leaf_cert = chain->cert_buffer();
288
289 // As an optimization, since the leaf is allowed to be listed as an SPKI,
290 // a match on the leaf's SPKI hash can return early, without comparing
291 // the organization information to itself.
292 net::HashValue hash;
293 if (net::x509_util::CalculateSha256SpkiHash(leaf_cert, &hash) &&
Jan Wilken Dörrie45d34f42019-06-08 09:40:54294 base::Contains(matches, hash)) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01295 *ct_required = false;
296 return true;
297 }
298
299 // If there was a match (or multiple matches), it's necessary to recompute
300 // the hashes to find the associated certificate.
301 std::vector<CRYPTO_BUFFER*> candidates;
302 for (const auto& buffer : chain->intermediate_buffers()) {
303 if (net::x509_util::CalculateSha256SpkiHash(buffer.get(), &hash) &&
Jan Wilken Dörrie45d34f42019-06-08 09:40:54304 base::Contains(matches, hash)) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01305 candidates.push_back(buffer.get());
306 }
307 }
308
309 if (candidates.empty())
310 return false;
311
312 scoped_refptr<net::ParsedCertificate> parsed_leaf =
David Benjamin4db85cf2018-07-10 16:10:04313 net::ParsedCertificate::Create(bssl::UpRef(leaf_cert),
Ryan Sleevi3dabe0b2018-04-05 03:59:01314 net::ParseCertificateOptions(), nullptr);
315 if (!parsed_leaf)
316 return false;
317 // If the leaf is not organizationally-bound, it's not a match.
318 net::RDNSequence leaf_rdn_sequence;
319 if (!ParseOrganizationBoundName(parsed_leaf->normalized_subject(),
320 &leaf_rdn_sequence)) {
321 return false;
322 }
323
324 for (auto* cert : candidates) {
325 if (AreCertsSameOrganization(leaf_rdn_sequence, cert)) {
326 *ct_required = false;
327 return true;
328 }
329 }
330
331 return false;
332}
333
Ryan Sleevie5574e02018-05-15 04:37:23334void ChromeRequireCTDelegate::AddFilters(
rsleevi96356f82016-06-30 09:01:20335 bool ct_required,
Doug Turner9e79cf0c2018-04-05 21:32:30336 const std::vector<std::string>& hosts,
rsleevi96356f82016-06-30 09:01:20337 url_matcher::URLMatcherConditionSet::Vector* conditions) {
Doug Turner9e79cf0c2018-04-05 21:32:30338 for (const auto& pattern : hosts) {
rsleevi96356f82016-06-30 09:01:20339 Filter filter;
340 filter.ct_required = ct_required;
341
342 // Parse the pattern just to the hostname, ignoring all other portions of
343 // the URL.
344 url::Parsed parsed;
345 std::string ignored_scheme = url_formatter::SegmentURL(pattern, &parsed);
346 if (!parsed.host.is_nonempty())
347 continue; // If there is no host to match, can't apply the filter.
348
349 std::string lc_host = base::ToLowerASCII(
350 base::StringPiece(pattern).substr(parsed.host.begin, parsed.host.len));
351 if (lc_host == "*") {
352 // Wildcard hosts are not allowed and ignored.
353 continue;
354 } else if (lc_host[0] == '.') {
355 // A leading dot means exact match and to not match subdomains.
356 lc_host.erase(0, 1);
357 filter.match_subdomains = false;
358 } else {
359 // Canonicalize the host to make sure it's an actual hostname, not an
360 // IP address or a BROKEN canonical host, as matching subdomains is
361 // not desirable for those.
362 url::RawCanonOutputT<char> output;
363 url::CanonHostInfo host_info;
364 url::CanonicalizeHostVerbose(pattern.c_str(), parsed.host, &output,
365 &host_info);
366 // TODO(rsleevi): Use canonicalized form?
367 if (host_info.family == url::CanonHostInfo::NEUTRAL) {
368 // Match subdomains (implicit by the omission of '.'). Add in a
369 // leading dot to make sure matches only happen at the domain
370 // component boundary.
371 lc_host.insert(lc_host.begin(), '.');
372 filter.match_subdomains = true;
373 } else {
374 filter.match_subdomains = false;
375 }
376 }
377 filter.host_length = lc_host.size();
378
379 // Create a condition for the URLMatcher that matches the hostname (and/or
380 // subdomains).
381 url_matcher::URLMatcherConditionFactory* condition_factory =
382 url_matcher_->condition_factory();
383 std::set<url_matcher::URLMatcherCondition> condition_set;
384 condition_set.insert(
385 filter.match_subdomains
386 ? condition_factory->CreateHostSuffixCondition(lc_host)
387 : condition_factory->CreateHostEqualsCondition(lc_host));
388 conditions->push_back(
389 new url_matcher::URLMatcherConditionSet(next_id_, condition_set));
390 filters_[next_id_] = filter;
391 ++next_id_;
392 }
393}
394
Ryan Sleevie5574e02018-05-15 04:37:23395void ChromeRequireCTDelegate::ParseSpkiHashes(
396 const std::vector<std::string> spki_list,
Ryan Sleevi3dabe0b2018-04-05 03:59:01397 net::HashValueVector* hashes) const {
398 hashes->clear();
Ryan Sleevie5574e02018-05-15 04:37:23399 for (const auto& value : spki_list) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01400 net::HashValue hash;
Doug Turner9e79cf0c2018-04-05 21:32:30401 if (!hash.FromString(value)) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01402 continue;
403 }
404 hashes->push_back(std::move(hash));
405 }
406 std::sort(hashes->begin(), hashes->end());
407}
408
Ryan Sleevie5574e02018-05-15 04:37:23409bool ChromeRequireCTDelegate::FilterTakesPrecedence(const Filter& lhs,
410 const Filter& rhs) const {
rsleevi96356f82016-06-30 09:01:20411 if (lhs.match_subdomains != rhs.match_subdomains)
412 return !lhs.match_subdomains; // Prefer the more explicit policy.
413
414 if (lhs.host_length != rhs.host_length)
415 return lhs.host_length > rhs.host_length; // Prefer the longer host match.
416
417 if (lhs.ct_required != rhs.ct_required)
418 return lhs.ct_required; // Prefer the policy that requires CT.
419
420 return false;
421}
422
rsleevi96356f82016-06-30 09:01:20423} // namespace certificate_transparency