1 // Copyright 2019 The Chromium Authors
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 "net/cert/coalescing_cert_verifier.h"
6
7 #include "base/containers/linked_list.h"
8 #include "base/containers/unique_ptr_adapters.h"
9 #include "base/functional/bind.h"
10 #include "base/memory/raw_ptr.h"
11 #include "base/memory/weak_ptr.h"
12 #include "base/metrics/histogram_macros.h"
13 #include "base/not_fatal_until.h"
14 #include "base/ranges/algorithm.h"
15 #include "base/strings/string_number_conversions.h"
16 #include "base/time/time.h"
17 #include "net/base/net_errors.h"
18 #include "net/cert/cert_verify_result.h"
19 #include "net/cert/crl_set.h"
20 #include "net/cert/x509_certificate_net_log_param.h"
21 #include "net/log/net_log_event_type.h"
22 #include "net/log/net_log_source.h"
23 #include "net/log/net_log_source_type.h"
24 #include "net/log/net_log_values.h"
25 #include "net/log/net_log_with_source.h"
26 #include "third_party/boringssl/src/pki/pem.h"
27
28 namespace net {
29
30 // DESIGN OVERVIEW:
31 //
32 // The CoalescingCertVerifier implements an algorithm to group multiple calls
33 // to Verify() into a single Job. This avoids overloading the underlying
34 // CertVerifier, particularly those that are expensive to talk to (e.g.
35 // talking to the system verifier or across processes), batching multiple
36 // requests to CoaleacingCertVerifier::Verify() into a single underlying call.
37 //
38 // However, this makes lifetime management a bit more complex.
39 // - The Job object represents all of the state for a single verification to
40 // the CoalescingCertVerifier's underlying CertVerifier.
41 // * It keeps the CertVerifyResult alive, which is required as long as
42 // there is a pending verification.
43 // * It keeps the CertVerify::Request to the underlying verifier alive,
44 // as long as there is a pending Request attached to the Job.
45 // * It keeps track of every CoalescingCertVerifier::Request that is
46 // interested in receiving notification. However, it does NOT own
47 // these objects, and thus needs to coordinate with the Request (via
48 // AddRequest/AbortRequest) to make sure it never has a stale
49 // pointer.
50 // NB: It would have also been possible for the Job to only
51 // hold WeakPtr<Request>s, rather than Request*, but that seemed less
52 // clear as to the lifetime invariants, even if it was more clear
53 // about how the pointers are used.
54 // - The Job object is always owned by the CoalescingCertVerifier. If the
55 // CoalescingCertVerifier is deleted, all in-flight requests to the
56 // underlying verifier should be cancelled. When the Job goes away, all the
57 // Requests will be orphaned.
58 // - The Request object is always owned by the CALLER. It is a handle to
59 // allow a caller to cancel a request, per the CertVerifier interface. If
60 // the Request goes away, no caller callbacks should be invoked if the Job
61 // it was (previously) attached to completes.
62 // - Per the CertVerifier interface, when the CoalescingCertVerifier is
63 // deleted, then regardless of there being any live Requests, none of those
64 // caller callbacks should be invoked.
65 //
66 // Finally, to add to the complexity, it's possible that, during the handling
67 // of a result from the underlying CertVerifier, a Job may begin dispatching
68 // to its Requests. The Request may delete the CoalescingCertVerifier. If that
69 // happens, then the Job being processed is also deleted, and none of the
70 // other Requests should be notified.
71
72 namespace {
73
CertVerifierParams(const CertVerifier::RequestParams & params)74 base::Value::Dict CertVerifierParams(
75 const CertVerifier::RequestParams& params) {
76 base::Value::Dict dict;
77 dict.Set("certificates",
78 NetLogX509CertificateList(params.certificate().get()));
79 if (!params.ocsp_response().empty()) {
80 dict.Set("ocsp_response",
81 bssl::PEMEncode(params.ocsp_response(), "NETLOG OCSP RESPONSE"));
82 }
83 if (!params.sct_list().empty()) {
84 dict.Set("sct_list", bssl::PEMEncode(params.sct_list(), "NETLOG SCT LIST"));
85 }
86 dict.Set("host", NetLogStringValue(params.hostname()));
87 dict.Set("verifier_flags", params.flags());
88
89 return dict;
90 }
91
92 } // namespace
93
94 // Job contains all the state for a single verification using the underlying
95 // verifier.
96 class CoalescingCertVerifier::Job {
97 public:
98 Job(CoalescingCertVerifier* parent,
99 const CertVerifier::RequestParams& params,
100 NetLog* net_log,
101 bool is_first_job);
102 ~Job();
103
params() const104 const CertVerifier::RequestParams& params() const { return params_; }
verify_result() const105 const CertVerifyResult& verify_result() const { return verify_result_; }
106
107 // Attaches |request|, causing it to be notified once this Job completes.
108 void AddRequest(CoalescingCertVerifier::Request* request);
109
110 // Stops |request| from being notified. If there are no Requests remaining,
111 // the Job will be cancelled.
112 // NOTE: It's only necessary to call this if the Job has not yet completed.
113 // If the Request has been notified of completion, this should not be called.
114 void AbortRequest(CoalescingCertVerifier::Request* request);
115
116 // Starts a verification using |underlying_verifier|. If this completes
117 // synchronously, returns the result code, with the associated result being
118 // available via |verify_result()|. Otherwise, it will complete
119 // asynchronously, notifying any Requests associated via |AttachRequest|.
120 int Start(CertVerifier* underlying_verifier);
121
122 private:
123 void OnVerifyComplete(int result);
124
125 void LogMetrics();
126
127 raw_ptr<CoalescingCertVerifier> parent_verifier_;
128 const CertVerifier::RequestParams params_;
129 const NetLogWithSource net_log_;
130 bool is_first_job_ = false;
131 CertVerifyResult verify_result_;
132
133 base::TimeTicks start_time_;
134 std::unique_ptr<CertVerifier::Request> pending_request_;
135
136 base::LinkedList<CoalescingCertVerifier::Request> attached_requests_;
137 base::WeakPtrFactory<Job> weak_ptr_factory_{this};
138 };
139
140 // Tracks the state associated with a single CoalescingCertVerifier::Verify
141 // request.
142 //
143 // There are two ways for requests to be cancelled:
144 // - The caller of Verify() can delete the Request object, indicating
145 // they are no longer interested in this particular request.
146 // - The caller can delete the CoalescingCertVerifier, which should cause
147 // all in-process Jobs to be aborted and deleted. Any Requests attached to
148 // Jobs should be orphaned, and do nothing when the Request is (eventually)
149 // deleted.
150 class CoalescingCertVerifier::Request
151 : public base::LinkNode<CoalescingCertVerifier::Request>,
152 public CertVerifier::Request {
153 public:
154 // Create a request that will be attached to |job|, and will notify
155 // |callback| and fill |verify_result| if the Job completes successfully.
156 // If the Request is deleted, or the Job is deleted, |callback| will not
157 // be notified.
158 Request(CoalescingCertVerifier::Job* job,
159 CertVerifyResult* verify_result,
160 CompletionOnceCallback callback,
161 const NetLogWithSource& net_log);
162
163 ~Request() override;
164
net_log() const165 const NetLogWithSource& net_log() const { return net_log_; }
166
167 // Called by Job to complete the requests (either successfully or as a sign
168 // that the underlying Job is going away).
169 void Complete(int result);
170
171 // Called when |job_| is being deleted, to ensure that the Request does not
172 // attempt to access the Job further. No callbacks will be invoked,
173 // consistent with the CoalescingCertVerifier's contract.
174 void OnJobAbort();
175
176 private:
177 raw_ptr<CoalescingCertVerifier::Job> job_;
178
179 raw_ptr<CertVerifyResult> verify_result_;
180 CompletionOnceCallback callback_;
181 const NetLogWithSource net_log_;
182 };
183
Job(CoalescingCertVerifier * parent,const CertVerifier::RequestParams & params,NetLog * net_log,bool is_first_job)184 CoalescingCertVerifier::Job::Job(CoalescingCertVerifier* parent,
185 const CertVerifier::RequestParams& params,
186 NetLog* net_log,
187 bool is_first_job)
188 : parent_verifier_(parent),
189 params_(params),
190 net_log_(
191 NetLogWithSource::Make(net_log, NetLogSourceType::CERT_VERIFIER_JOB)),
192 is_first_job_(is_first_job) {}
193
~Job()194 CoalescingCertVerifier::Job::~Job() {
195 // If there was at least one outstanding Request still pending, then this
196 // Job was aborted, rather than being completed normally and cleaned up.
197 if (!attached_requests_.empty() && pending_request_) {
198 net_log_.AddEvent(NetLogEventType::CANCELLED);
199 net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_JOB);
200 }
201
202 while (!attached_requests_.empty()) {
203 auto* link_node = attached_requests_.head();
204 link_node->RemoveFromList();
205 link_node->value()->OnJobAbort();
206 }
207 }
208
AddRequest(CoalescingCertVerifier::Request * request)209 void CoalescingCertVerifier::Job::AddRequest(
210 CoalescingCertVerifier::Request* request) {
211 // There must be a pending asynchronous verification in process.
212 DCHECK(pending_request_);
213
214 request->net_log().AddEventReferencingSource(
215 NetLogEventType::CERT_VERIFIER_REQUEST_BOUND_TO_JOB, net_log_.source());
216 attached_requests_.Append(request);
217 }
218
AbortRequest(CoalescingCertVerifier::Request * request)219 void CoalescingCertVerifier::Job::AbortRequest(
220 CoalescingCertVerifier::Request* request) {
221 // Check to make sure |request| hasn't already been removed.
222 DCHECK(request->previous() || request->next());
223
224 request->RemoveFromList();
225
226 // If there are no more pending requests, abort. This isn't strictly
227 // necessary; the request could be allowed to run to completion (and
228 // potentially to allow later Requests to join in), but in keeping with the
229 // idea of providing more stable guarantees about resources, clean up early.
230 if (attached_requests_.empty()) {
231 // If this was the last Request, then the Job had not yet completed; this
232 // matches the logic in the dtor, which handles when it's the Job that is
233 // deleted first, rather than the last Request.
234 net_log_.AddEvent(NetLogEventType::CANCELLED);
235 net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_JOB);
236
237 // DANGER: This will cause |this_| to be deleted!
238 parent_verifier_->RemoveJob(this);
239 return;
240 }
241 }
242
Start(CertVerifier * underlying_verifier)243 int CoalescingCertVerifier::Job::Start(CertVerifier* underlying_verifier) {
244 // Requests are only attached for asynchronous completion, so they must
245 // always be attached after Start() has been called.
246 DCHECK(attached_requests_.empty());
247 // There should not be a pending request already started (e.g. Start called
248 // multiple times).
249 DCHECK(!pending_request_);
250
251 net_log_.BeginEvent(NetLogEventType::CERT_VERIFIER_JOB,
252 [&] { return CertVerifierParams(params_); });
253
254 verify_result_.Reset();
255
256 start_time_ = base::TimeTicks::Now();
257 int result = underlying_verifier->Verify(
258 params_, &verify_result_,
259 // Safe, because |verify_request_| is self-owned and guarantees the
260 // callback won't be called if |this| is deleted.
261 base::BindOnce(&CoalescingCertVerifier::Job::OnVerifyComplete,
262 base::Unretained(this)),
263 &pending_request_, net_log_);
264 if (result != ERR_IO_PENDING) {
265 LogMetrics();
266 net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_JOB,
267 [&] { return verify_result_.NetLogParams(result); });
268 }
269
270 return result;
271 }
272
OnVerifyComplete(int result)273 void CoalescingCertVerifier::Job::OnVerifyComplete(int result) {
274 LogMetrics();
275
276 pending_request_.reset(); // Reset to signal clean completion.
277 net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_JOB,
278 [&] { return verify_result_.NetLogParams(result); });
279
280 // It's possible that during the process of invoking a callback for a
281 // Request, |this| may get deleted (along with the associated parent). If
282 // that happens, it's important to ensure that processing of the Job is
283 // stopped - i.e. no other callbacks are invoked for other Requests, nor is
284 // |this| accessed.
285 //
286 // To help detect and protect against this, a WeakPtr to |this| is taken. If
287 // |this| is deleted, the destructor will have invalidated the WeakPtr.
288 //
289 // Note that if a Job had already been deleted, this method would not have
290 // been invoked in the first place, as the Job (via |pending_request_|) owns
291 // the underlying CertVerifier::Request that this method was bound to as a
292 // callback. This is why it's OK to grab the WeakPtr from |this| initially.
293 base::WeakPtr<Job> weak_this = weak_ptr_factory_.GetWeakPtr();
294 while (!attached_requests_.empty()) {
295 // Note: It's also possible for additional Requests to be attached to the
296 // current Job while processing a Request.
297 auto* link_node = attached_requests_.head();
298 link_node->RemoveFromList();
299
300 // Note: |this| MAY be deleted here.
301 // - If the CoalescingCertVerifier is deleted, it will delete the
302 // Jobs (including |this|)
303 // - If this is the second-to-last Request, and the completion of this
304 // event causes the other Request to be deleted, detaching that Request
305 // from this Job will lead to this Job being deleted (via
306 // Job::AbortRequest())
307 link_node->value()->Complete(result);
308
309 // Check if |this| has been deleted (which implicitly includes
310 // |parent_verifier_|), and abort if so, since no further cleanup is
311 // needed.
312 if (!weak_this)
313 return;
314 }
315
316 // DANGER: |this| will be invalidated (deleted) after this point.
317 return parent_verifier_->RemoveJob(this);
318 }
319
LogMetrics()320 void CoalescingCertVerifier::Job::LogMetrics() {
321 base::TimeDelta latency = base::TimeTicks::Now() - start_time_;
322 UMA_HISTOGRAM_CUSTOM_TIMES("Net.CertVerifier_Job_Latency", latency,
323 base::Milliseconds(1), base::Minutes(10), 100);
324 if (is_first_job_) {
325 UMA_HISTOGRAM_CUSTOM_TIMES("Net.CertVerifier_First_Job_Latency", latency,
326 base::Milliseconds(1), base::Minutes(10), 100);
327 }
328 }
329
Request(CoalescingCertVerifier::Job * job,CertVerifyResult * verify_result,CompletionOnceCallback callback,const NetLogWithSource & net_log)330 CoalescingCertVerifier::Request::Request(CoalescingCertVerifier::Job* job,
331 CertVerifyResult* verify_result,
332 CompletionOnceCallback callback,
333 const NetLogWithSource& net_log)
334 : job_(job),
335 verify_result_(verify_result),
336 callback_(std::move(callback)),
337 net_log_(net_log) {
338 net_log_.BeginEvent(NetLogEventType::CERT_VERIFIER_REQUEST);
339 }
340
~Request()341 CoalescingCertVerifier::Request::~Request() {
342 if (job_) {
343 net_log_.AddEvent(NetLogEventType::CANCELLED);
344 net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_REQUEST);
345
346 // Need to null out `job_` before aborting the request to avoid a dangling
347 // pointer warning, as aborting the request may delete `job_`.
348 auto* job = job_.get();
349 job_ = nullptr;
350
351 // If the Request is deleted before the Job, then detach from the Job.
352 // Note: This may cause |job_| to be deleted.
353 job->AbortRequest(this);
354 }
355 }
356
Complete(int result)357 void CoalescingCertVerifier::Request::Complete(int result) {
358 DCHECK(job_); // There must be a pending/non-aborted job to complete.
359
360 *verify_result_ = job_->verify_result();
361
362 // On successful completion, the Job removes the Request from its set;
363 // similarly, break the association here so that when the Request is
364 // deleted, it does not try to abort the (now-completed) Job.
365 job_ = nullptr;
366
367 // Also need to break the association with `verify_result_`, so that
368 // dangling pointer checks the result and the Request be destroyed
369 // in any order.
370 verify_result_ = nullptr;
371
372 net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_REQUEST);
373
374 // Run |callback_|, which may delete |this|.
375 std::move(callback_).Run(result);
376 }
377
OnJobAbort()378 void CoalescingCertVerifier::Request::OnJobAbort() {
379 DCHECK(job_); // There must be a pending job to abort.
380
381 // If the Job is deleted before the Request, just clean up. The Request will
382 // eventually be deleted by the caller.
383 net_log_.AddEvent(NetLogEventType::CANCELLED);
384 net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_REQUEST);
385
386 job_ = nullptr;
387 // Note: May delete |this|, if the caller made |callback_| own the Request.
388 callback_.Reset();
389 }
390
CoalescingCertVerifier(std::unique_ptr<CertVerifier> verifier)391 CoalescingCertVerifier::CoalescingCertVerifier(
392 std::unique_ptr<CertVerifier> verifier)
393 : verifier_(std::move(verifier)) {
394 verifier_->AddObserver(this);
395 }
396
~CoalescingCertVerifier()397 CoalescingCertVerifier::~CoalescingCertVerifier() {
398 verifier_->RemoveObserver(this);
399 }
400
Verify(const RequestParams & params,CertVerifyResult * verify_result,CompletionOnceCallback callback,std::unique_ptr<CertVerifier::Request> * out_req,const NetLogWithSource & net_log)401 int CoalescingCertVerifier::Verify(
402 const RequestParams& params,
403 CertVerifyResult* verify_result,
404 CompletionOnceCallback callback,
405 std::unique_ptr<CertVerifier::Request>* out_req,
406 const NetLogWithSource& net_log) {
407 DCHECK(verify_result);
408 DCHECK(!callback.is_null());
409
410 out_req->reset();
411 ++requests_;
412
413 Job* job = FindJob(params);
414 if (job) {
415 // An identical request is in-flight and joinable, so just attach the
416 // callback.
417 ++inflight_joins_;
418 } else {
419 // No existing Jobs can be used. Create and start a new one.
420 std::unique_ptr<Job> new_job =
421 std::make_unique<Job>(this, params, net_log.net_log(), requests_ == 1);
422 int result = new_job->Start(verifier_.get());
423 if (result != ERR_IO_PENDING) {
424 *verify_result = new_job->verify_result();
425 return result;
426 }
427
428 job = new_job.get();
429 joinable_jobs_[params] = std::move(new_job);
430 }
431
432 std::unique_ptr<CoalescingCertVerifier::Request> request =
433 std::make_unique<CoalescingCertVerifier::Request>(
434 job, verify_result, std::move(callback), net_log);
435 job->AddRequest(request.get());
436 *out_req = std::move(request);
437 return ERR_IO_PENDING;
438 }
439
SetConfig(const CertVerifier::Config & config)440 void CoalescingCertVerifier::SetConfig(const CertVerifier::Config& config) {
441 verifier_->SetConfig(config);
442
443 IncrementGenerationAndMakeCurrentJobsUnjoinable();
444 }
445
AddObserver(CertVerifier::Observer * observer)446 void CoalescingCertVerifier::AddObserver(CertVerifier::Observer* observer) {
447 verifier_->AddObserver(observer);
448 }
449
RemoveObserver(CertVerifier::Observer * observer)450 void CoalescingCertVerifier::RemoveObserver(CertVerifier::Observer* observer) {
451 verifier_->RemoveObserver(observer);
452 }
453
FindJob(const RequestParams & params)454 CoalescingCertVerifier::Job* CoalescingCertVerifier::FindJob(
455 const RequestParams& params) {
456 auto it = joinable_jobs_.find(params);
457 if (it != joinable_jobs_.end())
458 return it->second.get();
459 return nullptr;
460 }
461
RemoveJob(Job * job)462 void CoalescingCertVerifier::RemoveJob(Job* job) {
463 // See if this was a job from the current configuration generation.
464 // Note: It's also necessary to compare that the underlying pointer is the
465 // same, and not merely a Job with the same parameters.
466 auto joinable_it = joinable_jobs_.find(job->params());
467 if (joinable_it != joinable_jobs_.end() && joinable_it->second.get() == job) {
468 joinable_jobs_.erase(joinable_it);
469 return;
470 }
471
472 // Otherwise, it MUST have been a job from a previous generation.
473 auto inflight_it =
474 base::ranges::find_if(inflight_jobs_, base::MatchesUniquePtr(job));
475 CHECK(inflight_it != inflight_jobs_.end(), base::NotFatalUntil::M130);
476 inflight_jobs_.erase(inflight_it);
477 return;
478 }
479
IncrementGenerationAndMakeCurrentJobsUnjoinable()480 void CoalescingCertVerifier::IncrementGenerationAndMakeCurrentJobsUnjoinable() {
481 for (auto& job : joinable_jobs_) {
482 inflight_jobs_.emplace_back(std::move(job.second));
483 }
484 joinable_jobs_.clear();
485 }
486
OnCertVerifierChanged()487 void CoalescingCertVerifier::OnCertVerifierChanged() {
488 IncrementGenerationAndMakeCurrentJobsUnjoinable();
489 }
490
491 } // namespace net
492