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