blob: 96b5f760e6c4c556eb482344cd876482c45bf424 [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 =
114 net::ParsedCertificate::Create(net::x509_util::DupCryptoBuffer(org_cert),
115 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.
211 legacy_spkis_.erase(
212 std::remove_if(legacy_spkis_.begin(), legacy_spkis_.end(),
213 [](const net::HashValue& hash) {
214 if (!net::IsLegacyPubliclyTrustedCA(hash)) {
215 LOG(ERROR) << "Non-legacy SPKI configured "
216 << hash.ToString();
217 return true;
218 }
219 return false;
220 }),
221 legacy_spkis_.end());
222}
223
224bool ChromeRequireCTDelegate::MatchHostname(const std::string& hostname,
225 bool* ct_required) const {
Ryan Sleevi3dabe0b2018-04-05 03:59:01226 if (url_matcher_->IsEmpty())
227 return false;
228
rsleevi96356f82016-06-30 09:01:20229 // Scheme and port are ignored by the policy, so it's OK to construct a
230 // new GURL here. However, |hostname| is in network form, not URL form,
231 // so it's necessary to wrap IPv6 addresses in brackets.
232 std::set<url_matcher::URLMatcherConditionSet::ID> matching_ids =
233 url_matcher_->MatchURL(
234 GURL("https://" + net::HostPortPair(hostname, 443).HostForURL()));
235 if (matching_ids.empty())
Ryan Sleevi3dabe0b2018-04-05 03:59:01236 return false;
rsleevi96356f82016-06-30 09:01:20237
238 // Determine the overall policy by determining the most specific policy.
239 std::map<url_matcher::URLMatcherConditionSet::ID, Filter>::const_iterator it =
240 filters_.begin();
241 const Filter* active_filter = nullptr;
242 for (const auto& match : matching_ids) {
243 // Because both |filters_| and |matching_ids| are sorted on the ID,
244 // treat both as forward-only iterators.
245 while (it != filters_.end() && it->first < match)
246 ++it;
247 if (it == filters_.end()) {
248 NOTREACHED();
249 break;
250 }
251
252 if (!active_filter || FilterTakesPrecedence(it->second, *active_filter))
253 active_filter = &it->second;
254 }
255 CHECK(active_filter);
256
Ryan Sleevi3dabe0b2018-04-05 03:59:01257 *ct_required = active_filter->ct_required;
258 return true;
rsleevi96356f82016-06-30 09:01:20259}
260
Ryan Sleevie5574e02018-05-15 04:37:23261bool ChromeRequireCTDelegate::MatchSPKI(const net::X509Certificate* chain,
262 const net::HashValueVector& hashes,
263 bool* ct_required) const {
Ryan Sleevi3dabe0b2018-04-05 03:59:01264 // Try to scan legacy SPKIs first, if any, since they will only require
265 // comparing hash values.
266 if (!legacy_spkis_.empty()) {
267 for (const auto& hash : hashes) {
268 if (std::binary_search(legacy_spkis_.begin(), legacy_spkis_.end(),
269 hash)) {
270 *ct_required = false;
271 return true;
272 }
273 }
274 }
275
276 if (spkis_.empty())
277 return false;
278
279 // Scan the constrained SPKIs via |hashes| first, as an optimization. If
280 // there are matches, the SPKI hash will have to be recomputed anyways to
281 // find the matching certificate, but avoid recomputing all the hashes for
282 // the case where there is no match.
283 net::HashValueVector matches;
284 for (const auto& hash : hashes) {
285 if (std::binary_search(spkis_.begin(), spkis_.end(), hash)) {
286 matches.push_back(hash);
287 }
288 }
289 if (matches.empty())
290 return false;
291
292 CRYPTO_BUFFER* leaf_cert = chain->cert_buffer();
293
294 // As an optimization, since the leaf is allowed to be listed as an SPKI,
295 // a match on the leaf's SPKI hash can return early, without comparing
296 // the organization information to itself.
297 net::HashValue hash;
298 if (net::x509_util::CalculateSha256SpkiHash(leaf_cert, &hash) &&
Hwanseung Lee231c90d2018-06-28 15:21:12299 base::ContainsValue(matches, hash)) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01300 *ct_required = false;
301 return true;
302 }
303
304 // If there was a match (or multiple matches), it's necessary to recompute
305 // the hashes to find the associated certificate.
306 std::vector<CRYPTO_BUFFER*> candidates;
307 for (const auto& buffer : chain->intermediate_buffers()) {
308 if (net::x509_util::CalculateSha256SpkiHash(buffer.get(), &hash) &&
Hwanseung Lee231c90d2018-06-28 15:21:12309 base::ContainsValue(matches, hash)) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01310 candidates.push_back(buffer.get());
311 }
312 }
313
314 if (candidates.empty())
315 return false;
316
317 scoped_refptr<net::ParsedCertificate> parsed_leaf =
318 net::ParsedCertificate::Create(net::x509_util::DupCryptoBuffer(leaf_cert),
319 net::ParseCertificateOptions(), nullptr);
320 if (!parsed_leaf)
321 return false;
322 // If the leaf is not organizationally-bound, it's not a match.
323 net::RDNSequence leaf_rdn_sequence;
324 if (!ParseOrganizationBoundName(parsed_leaf->normalized_subject(),
325 &leaf_rdn_sequence)) {
326 return false;
327 }
328
329 for (auto* cert : candidates) {
330 if (AreCertsSameOrganization(leaf_rdn_sequence, cert)) {
331 *ct_required = false;
332 return true;
333 }
334 }
335
336 return false;
337}
338
Ryan Sleevie5574e02018-05-15 04:37:23339void ChromeRequireCTDelegate::AddFilters(
rsleevi96356f82016-06-30 09:01:20340 bool ct_required,
Doug Turner9e79cf0c2018-04-05 21:32:30341 const std::vector<std::string>& hosts,
rsleevi96356f82016-06-30 09:01:20342 url_matcher::URLMatcherConditionSet::Vector* conditions) {
Doug Turner9e79cf0c2018-04-05 21:32:30343 for (const auto& pattern : hosts) {
rsleevi96356f82016-06-30 09:01:20344 Filter filter;
345 filter.ct_required = ct_required;
346
347 // Parse the pattern just to the hostname, ignoring all other portions of
348 // the URL.
349 url::Parsed parsed;
350 std::string ignored_scheme = url_formatter::SegmentURL(pattern, &parsed);
351 if (!parsed.host.is_nonempty())
352 continue; // If there is no host to match, can't apply the filter.
353
354 std::string lc_host = base::ToLowerASCII(
355 base::StringPiece(pattern).substr(parsed.host.begin, parsed.host.len));
356 if (lc_host == "*") {
357 // Wildcard hosts are not allowed and ignored.
358 continue;
359 } else if (lc_host[0] == '.') {
360 // A leading dot means exact match and to not match subdomains.
361 lc_host.erase(0, 1);
362 filter.match_subdomains = false;
363 } else {
364 // Canonicalize the host to make sure it's an actual hostname, not an
365 // IP address or a BROKEN canonical host, as matching subdomains is
366 // not desirable for those.
367 url::RawCanonOutputT<char> output;
368 url::CanonHostInfo host_info;
369 url::CanonicalizeHostVerbose(pattern.c_str(), parsed.host, &output,
370 &host_info);
371 // TODO(rsleevi): Use canonicalized form?
372 if (host_info.family == url::CanonHostInfo::NEUTRAL) {
373 // Match subdomains (implicit by the omission of '.'). Add in a
374 // leading dot to make sure matches only happen at the domain
375 // component boundary.
376 lc_host.insert(lc_host.begin(), '.');
377 filter.match_subdomains = true;
378 } else {
379 filter.match_subdomains = false;
380 }
381 }
382 filter.host_length = lc_host.size();
383
384 // Create a condition for the URLMatcher that matches the hostname (and/or
385 // subdomains).
386 url_matcher::URLMatcherConditionFactory* condition_factory =
387 url_matcher_->condition_factory();
388 std::set<url_matcher::URLMatcherCondition> condition_set;
389 condition_set.insert(
390 filter.match_subdomains
391 ? condition_factory->CreateHostSuffixCondition(lc_host)
392 : condition_factory->CreateHostEqualsCondition(lc_host));
393 conditions->push_back(
394 new url_matcher::URLMatcherConditionSet(next_id_, condition_set));
395 filters_[next_id_] = filter;
396 ++next_id_;
397 }
398}
399
Ryan Sleevie5574e02018-05-15 04:37:23400void ChromeRequireCTDelegate::ParseSpkiHashes(
401 const std::vector<std::string> spki_list,
Ryan Sleevi3dabe0b2018-04-05 03:59:01402 net::HashValueVector* hashes) const {
403 hashes->clear();
Ryan Sleevie5574e02018-05-15 04:37:23404 for (const auto& value : spki_list) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01405 net::HashValue hash;
Doug Turner9e79cf0c2018-04-05 21:32:30406 if (!hash.FromString(value)) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01407 continue;
408 }
409 hashes->push_back(std::move(hash));
410 }
411 std::sort(hashes->begin(), hashes->end());
412}
413
Ryan Sleevie5574e02018-05-15 04:37:23414bool ChromeRequireCTDelegate::FilterTakesPrecedence(const Filter& lhs,
415 const Filter& rhs) const {
rsleevi96356f82016-06-30 09:01:20416 if (lhs.match_subdomains != rhs.match_subdomains)
417 return !lhs.match_subdomains; // Prefer the more explicit policy.
418
419 if (lhs.host_length != rhs.host_length)
420 return lhs.host_length > rhs.host_length; // Prefer the longer host match.
421
422 if (lhs.ct_required != rhs.ct_required)
423 return lhs.ct_required; // Prefer the policy that requires CT.
424
425 return false;
426}
427
rsleevi96356f82016-06-30 09:01:20428} // namespace certificate_transparency