blob: d896d3362542988e7b6fe5049cc95ed7cf0964c3 [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
5#include "components/certificate_transparency/ct_policy_manager.h"
6
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
rsleevi96356f82016-06-30 09:01:20164class CTPolicyManager::CTDelegate
165 : public net::TransportSecurityState::RequireCTDelegate {
166 public:
Doug Turner9e79cf0c2018-04-05 21:32:30167 explicit CTDelegate();
rsleevi96356f82016-06-30 09:01:20168 ~CTDelegate() override = default;
169
Doug Turner9e79cf0c2018-04-05 21:32:30170 // Updates the CTDelegate to require CT
rsleevi96356f82016-06-30 09:01:20171 // for |required_hosts|, and exclude |excluded_hosts| from CT policies.
Doug Turner9e79cf0c2018-04-05 21:32:30172 void UpdateCTPolicies(const std::vector<std::string>& required_hosts,
173 const std::vector<std::string>& excluded_hosts,
174 const std::vector<std::string>& excluded_spkis,
175 const std::vector<std::string>& excluded_legacy_spkis);
rsleevi96356f82016-06-30 09:01:20176
177 // RequireCTDelegate implementation
178 // Called on the network task runner.
Ryan Sleevi3dabe0b2018-04-05 03:59:01179 CTRequirementLevel IsCTRequiredForHost(
180 const std::string& hostname,
181 const net::X509Certificate* chain,
182 const net::HashValueVector& hashes) override;
rsleevi96356f82016-06-30 09:01:20183
184 private:
185 struct Filter {
186 bool ct_required = false;
187 bool match_subdomains = false;
188 size_t host_length = 0;
189 };
190
Ryan Sleevi3dabe0b2018-04-05 03:59:01191 // Returns true if a policy for |hostname| is found, setting
192 // |*ct_required| to indicate whether or not Certificate Transparency is
193 // required for the host.
194 bool MatchHostname(const std::string& hostname, bool* ct_required) const;
195
196 // Returns true if a policy for |chain|, which contains the SPKI hashes
197 // |hashes|, is found, setting |*ct_required| to indicate whether or not
198 // Certificate Transparency is required for the certificate.
199 bool MatchSPKI(const net::X509Certificate* chain,
200 const net::HashValueVector& hashes,
201 bool* ct_required) const;
202
Doug Turner9e79cf0c2018-04-05 21:32:30203 // Updates the |url_matcher_| to
rsleevi96356f82016-06-30 09:01:20204 // require CT for |required_hosts| and exclude |excluded_hosts|, both
Ryan Sleevi3dabe0b2018-04-05 03:59:01205 // of which are Lists of Strings which are URLBlacklist filters, and
206 // updates |excluded_spkis| and |excluded_legacy_spkis| to exclude CT for
207 // those SPKIs, which are encoded as strings using net::HashValue::ToString.
Doug Turner9e79cf0c2018-04-05 21:32:30208 void Update(const std::vector<std::string>& required_hosts,
209 const std::vector<std::string>& excluded_hosts,
210 const std::vector<std::string>& excluded_spkis,
211 const std::vector<std::string>& excluded_legacy_spkis);
rsleevi96356f82016-06-30 09:01:20212
213 // Parses the filters from |host_patterns|, adding them as filters to
214 // |filters_| (with |ct_required| indicating whether or not CT is required
215 // for that host), and updating |*conditions| with the corresponding
216 // URLMatcher::Conditions to match the host.
217 void AddFilters(bool ct_required,
Doug Turner9e79cf0c2018-04-05 21:32:30218 const std::vector<std::string>& host_patterns,
rsleevi96356f82016-06-30 09:01:20219 url_matcher::URLMatcherConditionSet::Vector* conditions);
220
Ryan Sleevi3dabe0b2018-04-05 03:59:01221 // Parses the SPKIs from |list|, setting |*hashes| to the sorted set of all
222 // valid SPKIs.
Doug Turner9e79cf0c2018-04-05 21:32:30223 void ParseSpkiHashes(const std::vector<std::string> list,
Ryan Sleevi3dabe0b2018-04-05 03:59:01224 net::HashValueVector* hashes) const;
225
rsleevi96356f82016-06-30 09:01:20226 // Returns true if |lhs| has greater precedence than |rhs|.
227 bool FilterTakesPrecedence(const Filter& lhs, const Filter& rhs) const;
228
rsleevi96356f82016-06-30 09:01:20229 std::unique_ptr<url_matcher::URLMatcher> url_matcher_;
230 url_matcher::URLMatcherConditionSet::ID next_id_;
231 std::map<url_matcher::URLMatcherConditionSet::ID, Filter> filters_;
232
Ryan Sleevi3dabe0b2018-04-05 03:59:01233 // Both SPKI lists are sorted.
234 net::HashValueVector spkis_;
235 net::HashValueVector legacy_spkis_;
236
rsleevi96356f82016-06-30 09:01:20237 DISALLOW_COPY_AND_ASSIGN(CTDelegate);
238};
239
Doug Turner9e79cf0c2018-04-05 21:32:30240CTPolicyManager::CTDelegate::CTDelegate()
241 : url_matcher_(new url_matcher::URLMatcher), next_id_(0) {}
rsleevi96356f82016-06-30 09:01:20242
Doug Turner9e79cf0c2018-04-05 21:32:30243void CTPolicyManager::CTDelegate::UpdateCTPolicies(
244 const std::vector<std::string>& required_hosts,
245 const std::vector<std::string>& excluded_hosts,
246 const std::vector<std::string>& excluded_spkis,
247 const std::vector<std::string>& excluded_legacy_spkis) {
248 Update(required_hosts, excluded_hosts, excluded_spkis, excluded_legacy_spkis);
rsleevi96356f82016-06-30 09:01:20249}
250
251net::TransportSecurityState::RequireCTDelegate::CTRequirementLevel
Ryan Sleevi3dabe0b2018-04-05 03:59:01252CTPolicyManager::CTDelegate::IsCTRequiredForHost(
253 const std::string& hostname,
254 const net::X509Certificate* chain,
255 const net::HashValueVector& hashes) {
rsleevi96356f82016-06-30 09:01:20256
Ryan Sleevi3dabe0b2018-04-05 03:59:01257 bool ct_required = false;
258 if (MatchHostname(hostname, &ct_required) ||
259 MatchSPKI(chain, hashes, &ct_required)) {
260 return ct_required ? CTRequirementLevel::REQUIRED
261 : CTRequirementLevel::NOT_REQUIRED;
262 }
263
264 return CTRequirementLevel::DEFAULT;
265}
266
267bool CTPolicyManager::CTDelegate::MatchHostname(const std::string& hostname,
268 bool* ct_required) const {
269 if (url_matcher_->IsEmpty())
270 return false;
271
rsleevi96356f82016-06-30 09:01:20272 // Scheme and port are ignored by the policy, so it's OK to construct a
273 // new GURL here. However, |hostname| is in network form, not URL form,
274 // so it's necessary to wrap IPv6 addresses in brackets.
275 std::set<url_matcher::URLMatcherConditionSet::ID> matching_ids =
276 url_matcher_->MatchURL(
277 GURL("https://" + net::HostPortPair(hostname, 443).HostForURL()));
278 if (matching_ids.empty())
Ryan Sleevi3dabe0b2018-04-05 03:59:01279 return false;
rsleevi96356f82016-06-30 09:01:20280
281 // Determine the overall policy by determining the most specific policy.
282 std::map<url_matcher::URLMatcherConditionSet::ID, Filter>::const_iterator it =
283 filters_.begin();
284 const Filter* active_filter = nullptr;
285 for (const auto& match : matching_ids) {
286 // Because both |filters_| and |matching_ids| are sorted on the ID,
287 // treat both as forward-only iterators.
288 while (it != filters_.end() && it->first < match)
289 ++it;
290 if (it == filters_.end()) {
291 NOTREACHED();
292 break;
293 }
294
295 if (!active_filter || FilterTakesPrecedence(it->second, *active_filter))
296 active_filter = &it->second;
297 }
298 CHECK(active_filter);
299
Ryan Sleevi3dabe0b2018-04-05 03:59:01300 *ct_required = active_filter->ct_required;
301 return true;
rsleevi96356f82016-06-30 09:01:20302}
303
Ryan Sleevi3dabe0b2018-04-05 03:59:01304bool CTPolicyManager::CTDelegate::MatchSPKI(const net::X509Certificate* chain,
305 const net::HashValueVector& hashes,
306 bool* ct_required) const {
307 // Try to scan legacy SPKIs first, if any, since they will only require
308 // comparing hash values.
309 if (!legacy_spkis_.empty()) {
310 for (const auto& hash : hashes) {
311 if (std::binary_search(legacy_spkis_.begin(), legacy_spkis_.end(),
312 hash)) {
313 *ct_required = false;
314 return true;
315 }
316 }
317 }
318
319 if (spkis_.empty())
320 return false;
321
322 // Scan the constrained SPKIs via |hashes| first, as an optimization. If
323 // there are matches, the SPKI hash will have to be recomputed anyways to
324 // find the matching certificate, but avoid recomputing all the hashes for
325 // the case where there is no match.
326 net::HashValueVector matches;
327 for (const auto& hash : hashes) {
328 if (std::binary_search(spkis_.begin(), spkis_.end(), hash)) {
329 matches.push_back(hash);
330 }
331 }
332 if (matches.empty())
333 return false;
334
335 CRYPTO_BUFFER* leaf_cert = chain->cert_buffer();
336
337 // As an optimization, since the leaf is allowed to be listed as an SPKI,
338 // a match on the leaf's SPKI hash can return early, without comparing
339 // the organization information to itself.
340 net::HashValue hash;
341 if (net::x509_util::CalculateSha256SpkiHash(leaf_cert, &hash) &&
342 std::find(matches.begin(), matches.end(), hash) != matches.end()) {
343 *ct_required = false;
344 return true;
345 }
346
347 // If there was a match (or multiple matches), it's necessary to recompute
348 // the hashes to find the associated certificate.
349 std::vector<CRYPTO_BUFFER*> candidates;
350 for (const auto& buffer : chain->intermediate_buffers()) {
351 if (net::x509_util::CalculateSha256SpkiHash(buffer.get(), &hash) &&
352 std::find(matches.begin(), matches.end(), hash) != matches.end()) {
353 candidates.push_back(buffer.get());
354 }
355 }
356
357 if (candidates.empty())
358 return false;
359
360 scoped_refptr<net::ParsedCertificate> parsed_leaf =
361 net::ParsedCertificate::Create(net::x509_util::DupCryptoBuffer(leaf_cert),
362 net::ParseCertificateOptions(), nullptr);
363 if (!parsed_leaf)
364 return false;
365 // If the leaf is not organizationally-bound, it's not a match.
366 net::RDNSequence leaf_rdn_sequence;
367 if (!ParseOrganizationBoundName(parsed_leaf->normalized_subject(),
368 &leaf_rdn_sequence)) {
369 return false;
370 }
371
372 for (auto* cert : candidates) {
373 if (AreCertsSameOrganization(leaf_rdn_sequence, cert)) {
374 *ct_required = false;
375 return true;
376 }
377 }
378
379 return false;
380}
381
382void CTPolicyManager::CTDelegate::Update(
Doug Turner9e79cf0c2018-04-05 21:32:30383 const std::vector<std::string>& required_hosts,
384 const std::vector<std::string>& excluded_hosts,
385 const std::vector<std::string>& excluded_spkis,
386 const std::vector<std::string>& excluded_legacy_spkis) {
rsleevi96356f82016-06-30 09:01:20387 url_matcher_.reset(new url_matcher::URLMatcher);
388 filters_.clear();
389 next_id_ = 0;
390
391 url_matcher::URLMatcherConditionSet::Vector all_conditions;
392 AddFilters(true, required_hosts, &all_conditions);
393 AddFilters(false, excluded_hosts, &all_conditions);
394
395 url_matcher_->AddConditionSets(all_conditions);
Ryan Sleevi3dabe0b2018-04-05 03:59:01396
397 ParseSpkiHashes(excluded_spkis, &spkis_);
398 ParseSpkiHashes(excluded_legacy_spkis, &legacy_spkis_);
399
400 // Filter out SPKIs that aren't for legacy CAs.
401 legacy_spkis_.erase(
402 std::remove_if(legacy_spkis_.begin(), legacy_spkis_.end(),
403 [](const net::HashValue& hash) {
404 if (!net::IsLegacyPubliclyTrustedCA(hash)) {
405 LOG(ERROR) << "Non-legacy SPKI configured "
406 << hash.ToString();
407 return true;
408 }
409 return false;
410 }),
411 legacy_spkis_.end());
rsleevi96356f82016-06-30 09:01:20412}
413
414void CTPolicyManager::CTDelegate::AddFilters(
415 bool ct_required,
Doug Turner9e79cf0c2018-04-05 21:32:30416 const std::vector<std::string>& hosts,
rsleevi96356f82016-06-30 09:01:20417 url_matcher::URLMatcherConditionSet::Vector* conditions) {
Doug Turner9e79cf0c2018-04-05 21:32:30418 for (const auto& pattern : hosts) {
rsleevi96356f82016-06-30 09:01:20419 Filter filter;
420 filter.ct_required = ct_required;
421
422 // Parse the pattern just to the hostname, ignoring all other portions of
423 // the URL.
424 url::Parsed parsed;
425 std::string ignored_scheme = url_formatter::SegmentURL(pattern, &parsed);
426 if (!parsed.host.is_nonempty())
427 continue; // If there is no host to match, can't apply the filter.
428
429 std::string lc_host = base::ToLowerASCII(
430 base::StringPiece(pattern).substr(parsed.host.begin, parsed.host.len));
431 if (lc_host == "*") {
432 // Wildcard hosts are not allowed and ignored.
433 continue;
434 } else if (lc_host[0] == '.') {
435 // A leading dot means exact match and to not match subdomains.
436 lc_host.erase(0, 1);
437 filter.match_subdomains = false;
438 } else {
439 // Canonicalize the host to make sure it's an actual hostname, not an
440 // IP address or a BROKEN canonical host, as matching subdomains is
441 // not desirable for those.
442 url::RawCanonOutputT<char> output;
443 url::CanonHostInfo host_info;
444 url::CanonicalizeHostVerbose(pattern.c_str(), parsed.host, &output,
445 &host_info);
446 // TODO(rsleevi): Use canonicalized form?
447 if (host_info.family == url::CanonHostInfo::NEUTRAL) {
448 // Match subdomains (implicit by the omission of '.'). Add in a
449 // leading dot to make sure matches only happen at the domain
450 // component boundary.
451 lc_host.insert(lc_host.begin(), '.');
452 filter.match_subdomains = true;
453 } else {
454 filter.match_subdomains = false;
455 }
456 }
457 filter.host_length = lc_host.size();
458
459 // Create a condition for the URLMatcher that matches the hostname (and/or
460 // subdomains).
461 url_matcher::URLMatcherConditionFactory* condition_factory =
462 url_matcher_->condition_factory();
463 std::set<url_matcher::URLMatcherCondition> condition_set;
464 condition_set.insert(
465 filter.match_subdomains
466 ? condition_factory->CreateHostSuffixCondition(lc_host)
467 : condition_factory->CreateHostEqualsCondition(lc_host));
468 conditions->push_back(
469 new url_matcher::URLMatcherConditionSet(next_id_, condition_set));
470 filters_[next_id_] = filter;
471 ++next_id_;
472 }
473}
474
Ryan Sleevi3dabe0b2018-04-05 03:59:01475void CTPolicyManager::CTDelegate::ParseSpkiHashes(
Doug Turner9e79cf0c2018-04-05 21:32:30476 const std::vector<std::string> list,
Ryan Sleevi3dabe0b2018-04-05 03:59:01477 net::HashValueVector* hashes) const {
478 hashes->clear();
Doug Turner9e79cf0c2018-04-05 21:32:30479 for (const auto& value : list) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01480 net::HashValue hash;
Doug Turner9e79cf0c2018-04-05 21:32:30481 if (!hash.FromString(value)) {
Ryan Sleevi3dabe0b2018-04-05 03:59:01482 continue;
483 }
484 hashes->push_back(std::move(hash));
485 }
486 std::sort(hashes->begin(), hashes->end());
487}
488
rsleevi96356f82016-06-30 09:01:20489bool CTPolicyManager::CTDelegate::FilterTakesPrecedence(
490 const Filter& lhs,
491 const Filter& rhs) const {
492 if (lhs.match_subdomains != rhs.match_subdomains)
493 return !lhs.match_subdomains; // Prefer the more explicit policy.
494
495 if (lhs.host_length != rhs.host_length)
496 return lhs.host_length > rhs.host_length; // Prefer the longer host match.
497
498 if (lhs.ct_required != rhs.ct_required)
499 return lhs.ct_required; // Prefer the policy that requires CT.
500
501 return false;
502}
503
Doug Turner9e79cf0c2018-04-05 21:32:30504CTPolicyManager::CTPolicyManager() : delegate_(new CTDelegate()) {}
rsleevi96356f82016-06-30 09:01:20505
506CTPolicyManager::~CTPolicyManager() {}
507
Doug Turner9e79cf0c2018-04-05 21:32:30508void CTPolicyManager::UpdateCTPolicies(
509 const std::vector<std::string>& required_hosts,
510 const std::vector<std::string>& excluded_hosts,
511 const std::vector<std::string>& excluded_spkis,
512 const std::vector<std::string>& excluded_legacy_spkis) {
513 delegate_->UpdateCTPolicies(required_hosts, excluded_hosts, excluded_spkis,
514 excluded_legacy_spkis);
rsleevi96356f82016-06-30 09:01:20515}
516
517net::TransportSecurityState::RequireCTDelegate* CTPolicyManager::GetDelegate() {
518 return delegate_.get();
519}
520
rsleevi96356f82016-06-30 09:01:20521} // namespace certificate_transparency