| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/cert/coalescing_cert_verifier.h" |
| |
| #include "base/containers/linked_list.h" |
| #include "base/containers/unique_ptr_adapters.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/time/time.h" |
| #include "net/base/net_errors.h" |
| #include "net/cert/cert_verify_result.h" |
| #include "net/cert/crl_set.h" |
| #include "net/cert/pem.h" |
| #include "net/cert/x509_certificate_net_log_param.h" |
| #include "net/log/net_log_event_type.h" |
| #include "net/log/net_log_source.h" |
| #include "net/log/net_log_source_type.h" |
| #include "net/log/net_log_values.h" |
| #include "net/log/net_log_with_source.h" |
| |
| namespace net { |
| |
| // DESIGN OVERVIEW: |
| // |
| // The CoalescingCertVerifier implements an algorithm to group multiple calls |
| // to Verify() into a single Job. This avoids overloading the underlying |
| // CertVerifier, particularly those that are expensive to talk to (e.g. |
| // talking to the system verifier or across processes), batching multiple |
| // requests to CoaleacingCertVerifier::Verify() into a single underlying call. |
| // |
| // However, this makes lifetime management a bit more complex. |
| // - The Job object represents all of the state for a single verification to |
| // the CoalescingCertVerifier's underlying CertVerifier. |
| // * It keeps the CertVerifyResult alive, which is required as long as |
| // there is a pending verification. |
| // * It keeps the CertVerify::Request to the underlying verifier alive, |
| // as long as there is a pending Request attached to the Job. |
| // * It keeps track of every CoalescingCertVerifier::Request that is |
| // interested in receiving notification. However, it does NOT own |
| // these objects, and thus needs to coordinate with the Request (via |
| // AddRequest/AbortRequest) to make sure it never has a stale |
| // pointer. |
| // NB: It would have also been possible for the Job to only |
| // hold WeakPtr<Request>s, rather than Request*, but that seemed less |
| // clear as to the lifetime invariants, even if it was more clear |
| // about how the pointers are used. |
| // - The Job object is always owned by the CoalescingCertVerifier. If the |
| // CoalescingCertVerifier is deleted, all in-flight requests to the |
| // underlying verifier should be cancelled. When the Job goes away, all the |
| // Requests will be orphaned. |
| // - The Request object is always owned by the CALLER. It is a handle to |
| // allow a caller to cancel a request, per the CertVerifier interface. If |
| // the Request goes away, no caller callbacks should be invoked if the Job |
| // it was (previously) attached to completes. |
| // - Per the CertVerifier interface, when the CoalescingCertVerifier is |
| // deleted, then regardless of there being any live Requests, none of those |
| // caller callbacks should be invoked. |
| // |
| // Finally, to add to the complexity, it's possible that, during the handling |
| // of a result from the underlying CertVerifier, a Job may begin dispatching |
| // to its Requests. The Request may delete the CoalescingCertVerifier. If that |
| // happens, then the Job being processed is also deleted, and none of the |
| // other Requests should be notified. |
| |
| namespace { |
| |
| base::Value::Dict CertVerifierParams( |
| const CertVerifier::RequestParams& params) { |
| base::Value::Dict dict; |
| dict.Set("certificates", |
| NetLogX509CertificateList(params.certificate().get())); |
| if (!params.ocsp_response().empty()) { |
| dict.Set("ocsp_response", |
| PEMEncode(params.ocsp_response(), "NETLOG OCSP RESPONSE")); |
| } |
| if (!params.sct_list().empty()) { |
| dict.Set("sct_list", PEMEncode(params.sct_list(), "NETLOG SCT LIST")); |
| } |
| dict.Set("host", NetLogStringValue(params.hostname())); |
| dict.Set("verifier_flags", params.flags()); |
| |
| return dict; |
| } |
| |
| } // namespace |
| |
| // Job contains all the state for a single verification using the underlying |
| // verifier. |
| class CoalescingCertVerifier::Job { |
| public: |
| Job(CoalescingCertVerifier* parent, |
| const CertVerifier::RequestParams& params, |
| NetLog* net_log, |
| bool is_first_job); |
| ~Job(); |
| |
| const CertVerifier::RequestParams& params() const { return params_; } |
| const CertVerifyResult& verify_result() const { return verify_result_; } |
| |
| // Attaches |request|, causing it to be notified once this Job completes. |
| void AddRequest(CoalescingCertVerifier::Request* request); |
| |
| // Stops |request| from being notified. If there are no Requests remaining, |
| // the Job will be cancelled. |
| // NOTE: It's only necessary to call this if the Job has not yet completed. |
| // If the Request has been notified of completion, this should not be called. |
| void AbortRequest(CoalescingCertVerifier::Request* request); |
| |
| // Starts a verification using |underlying_verifier|. If this completes |
| // synchronously, returns the result code, with the associated result being |
| // available via |verify_result()|. Otherwise, it will complete |
| // asynchronously, notifying any Requests associated via |AttachRequest|. |
| int Start(CertVerifier* underlying_verifier); |
| |
| private: |
| void OnVerifyComplete(int result); |
| |
| void LogMetrics(); |
| |
| raw_ptr<CoalescingCertVerifier> parent_verifier_; |
| const CertVerifier::RequestParams params_; |
| const NetLogWithSource net_log_; |
| bool is_first_job_ = false; |
| CertVerifyResult verify_result_; |
| |
| base::TimeTicks start_time_; |
| std::unique_ptr<CertVerifier::Request> pending_request_; |
| |
| base::LinkedList<CoalescingCertVerifier::Request> attached_requests_; |
| base::WeakPtrFactory<Job> weak_ptr_factory_{this}; |
| }; |
| |
| // Tracks the state associated with a single CoalescingCertVerifier::Verify |
| // request. |
| // |
| // There are two ways for requests to be cancelled: |
| // - The caller of Verify() can delete the Request object, indicating |
| // they are no longer interested in this particular request. |
| // - The caller can delete the CoalescingCertVerifier, which should cause |
| // all in-process Jobs to be aborted and deleted. Any Requests attached to |
| // Jobs should be orphaned, and do nothing when the Request is (eventually) |
| // deleted. |
| class CoalescingCertVerifier::Request |
| : public base::LinkNode<CoalescingCertVerifier::Request>, |
| public CertVerifier::Request { |
| public: |
| // Create a request that will be attached to |job|, and will notify |
| // |callback| and fill |verify_result| if the Job completes successfully. |
| // If the Request is deleted, or the Job is deleted, |callback| will not |
| // be notified. |
| Request(CoalescingCertVerifier::Job* job, |
| CertVerifyResult* verify_result, |
| CompletionOnceCallback callback, |
| const NetLogWithSource& net_log); |
| |
| ~Request() override; |
| |
| const NetLogWithSource& net_log() const { return net_log_; } |
| |
| // Called by Job to complete the requests (either successfully or as a sign |
| // that the underlying Job is going away). |
| void Complete(int result); |
| |
| // Called when |job_| is being deleted, to ensure that the Request does not |
| // attempt to access the Job further. No callbacks will be invoked, |
| // consistent with the CoalescingCertVerifier's contract. |
| void OnJobAbort(); |
| |
| private: |
| raw_ptr<CoalescingCertVerifier::Job, DanglingUntriaged> job_; |
| |
| raw_ptr<CertVerifyResult, DanglingUntriaged> verify_result_; |
| CompletionOnceCallback callback_; |
| const NetLogWithSource net_log_; |
| }; |
| |
| CoalescingCertVerifier::Job::Job(CoalescingCertVerifier* parent, |
| const CertVerifier::RequestParams& params, |
| NetLog* net_log, |
| bool is_first_job) |
| : parent_verifier_(parent), |
| params_(params), |
| net_log_( |
| NetLogWithSource::Make(net_log, NetLogSourceType::CERT_VERIFIER_JOB)), |
| is_first_job_(is_first_job) {} |
| |
| CoalescingCertVerifier::Job::~Job() { |
| // If there was at least one outstanding Request still pending, then this |
| // Job was aborted, rather than being completed normally and cleaned up. |
| if (!attached_requests_.empty() && pending_request_) { |
| net_log_.AddEvent(NetLogEventType::CANCELLED); |
| net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_JOB); |
| } |
| |
| while (!attached_requests_.empty()) { |
| auto* link_node = attached_requests_.head(); |
| link_node->RemoveFromList(); |
| link_node->value()->OnJobAbort(); |
| } |
| } |
| |
| void CoalescingCertVerifier::Job::AddRequest( |
| CoalescingCertVerifier::Request* request) { |
| // There must be a pending asynchronous verification in process. |
| DCHECK(pending_request_); |
| |
| request->net_log().AddEventReferencingSource( |
| NetLogEventType::CERT_VERIFIER_REQUEST_BOUND_TO_JOB, net_log_.source()); |
| attached_requests_.Append(request); |
| } |
| |
| void CoalescingCertVerifier::Job::AbortRequest( |
| CoalescingCertVerifier::Request* request) { |
| // Check to make sure |request| hasn't already been removed. |
| DCHECK(request->previous() || request->next()); |
| |
| request->RemoveFromList(); |
| |
| // If there are no more pending requests, abort. This isn't strictly |
| // necessary; the request could be allowed to run to completion (and |
| // potentially to allow later Requests to join in), but in keeping with the |
| // idea of providing more stable guarantees about resources, clean up early. |
| if (attached_requests_.empty()) { |
| // If this was the last Request, then the Job had not yet completed; this |
| // matches the logic in the dtor, which handles when it's the Job that is |
| // deleted first, rather than the last Request. |
| net_log_.AddEvent(NetLogEventType::CANCELLED); |
| net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_JOB); |
| |
| // DANGER: This will cause |this_| to be deleted! |
| parent_verifier_->RemoveJob(this); |
| return; |
| } |
| } |
| |
| int CoalescingCertVerifier::Job::Start(CertVerifier* underlying_verifier) { |
| // Requests are only attached for asynchronous completion, so they must |
| // always be attached after Start() has been called. |
| DCHECK(attached_requests_.empty()); |
| // There should not be a pending request already started (e.g. Start called |
| // multiple times). |
| DCHECK(!pending_request_); |
| |
| net_log_.BeginEvent(NetLogEventType::CERT_VERIFIER_JOB, |
| [&] { return CertVerifierParams(params_); }); |
| |
| verify_result_.Reset(); |
| |
| start_time_ = base::TimeTicks::Now(); |
| int result = underlying_verifier->Verify( |
| params_, &verify_result_, |
| // Safe, because |verify_request_| is self-owned and guarantees the |
| // callback won't be called if |this| is deleted. |
| base::BindOnce(&CoalescingCertVerifier::Job::OnVerifyComplete, |
| base::Unretained(this)), |
| &pending_request_, net_log_); |
| if (result != ERR_IO_PENDING) { |
| LogMetrics(); |
| net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_JOB, |
| [&] { return verify_result_.NetLogParams(result); }); |
| } |
| |
| return result; |
| } |
| |
| void CoalescingCertVerifier::Job::OnVerifyComplete(int result) { |
| LogMetrics(); |
| |
| pending_request_.reset(); // Reset to signal clean completion. |
| net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_JOB, |
| [&] { return verify_result_.NetLogParams(result); }); |
| |
| // It's possible that during the process of invoking a callback for a |
| // Request, |this| may get deleted (along with the associated parent). If |
| // that happens, it's important to ensure that processing of the Job is |
| // stopped - i.e. no other callbacks are invoked for other Requests, nor is |
| // |this| accessed. |
| // |
| // To help detect and protect against this, a WeakPtr to |this| is taken. If |
| // |this| is deleted, the destructor will have invalidated the WeakPtr. |
| // |
| // Note that if a Job had already been deleted, this method would not have |
| // been invoked in the first place, as the Job (via |pending_request_|) owns |
| // the underlying CertVerifier::Request that this method was bound to as a |
| // callback. This is why it's OK to grab the WeakPtr from |this| initially. |
| base::WeakPtr<Job> weak_this = weak_ptr_factory_.GetWeakPtr(); |
| while (!attached_requests_.empty()) { |
| // Note: It's also possible for additional Requests to be attached to the |
| // current Job while processing a Request. |
| auto* link_node = attached_requests_.head(); |
| link_node->RemoveFromList(); |
| |
| // Note: |this| MAY be deleted here. |
| // - If the CoalescingCertVerifier is deleted, it will delete the |
| // Jobs (including |this|) |
| // - If this is the second-to-last Request, and the completion of this |
| // event causes the other Request to be deleted, detaching that Request |
| // from this Job will lead to this Job being deleted (via |
| // Job::AbortRequest()) |
| link_node->value()->Complete(result); |
| |
| // Check if |this| has been deleted (which implicitly includes |
| // |parent_verifier_|), and abort if so, since no further cleanup is |
| // needed. |
| if (!weak_this) |
| return; |
| } |
| |
| // DANGER: |this| will be invalidated (deleted) after this point. |
| return parent_verifier_->RemoveJob(this); |
| } |
| |
| void CoalescingCertVerifier::Job::LogMetrics() { |
| base::TimeDelta latency = base::TimeTicks::Now() - start_time_; |
| UMA_HISTOGRAM_CUSTOM_TIMES("Net.CertVerifier_Job_Latency", latency, |
| base::Milliseconds(1), base::Minutes(10), 100); |
| if (is_first_job_) { |
| UMA_HISTOGRAM_CUSTOM_TIMES("Net.CertVerifier_First_Job_Latency", latency, |
| base::Milliseconds(1), base::Minutes(10), 100); |
| } |
| } |
| |
| CoalescingCertVerifier::Request::Request(CoalescingCertVerifier::Job* job, |
| CertVerifyResult* verify_result, |
| CompletionOnceCallback callback, |
| const NetLogWithSource& net_log) |
| : job_(job), |
| verify_result_(verify_result), |
| callback_(std::move(callback)), |
| net_log_(net_log) { |
| net_log_.BeginEvent(NetLogEventType::CERT_VERIFIER_REQUEST); |
| } |
| |
| CoalescingCertVerifier::Request::~Request() { |
| if (job_) { |
| net_log_.AddEvent(NetLogEventType::CANCELLED); |
| net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_REQUEST); |
| |
| // If the Request is deleted before the Job, then detach from the Job. |
| // Note: This may cause |job_| to be deleted. |
| job_->AbortRequest(this); |
| job_ = nullptr; |
| } |
| } |
| |
| void CoalescingCertVerifier::Request::Complete(int result) { |
| DCHECK(job_); // There must be a pending/non-aborted job to complete. |
| |
| *verify_result_ = job_->verify_result(); |
| |
| // On successful completion, the Job removes the Request from its set; |
| // similarly, break the association here so that when the Request is |
| // deleted, it does not try to abort the (now-completed) Job. |
| job_ = nullptr; |
| |
| net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_REQUEST); |
| |
| // Run |callback_|, which may delete |this|. |
| std::move(callback_).Run(result); |
| } |
| |
| void CoalescingCertVerifier::Request::OnJobAbort() { |
| DCHECK(job_); // There must be a pending job to abort. |
| |
| // If the Job is deleted before the Request, just clean up. The Request will |
| // eventually be deleted by the caller. |
| net_log_.AddEvent(NetLogEventType::CANCELLED); |
| net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_REQUEST); |
| |
| job_ = nullptr; |
| // Note: May delete |this|, if the caller made |callback_| own the Request. |
| callback_.Reset(); |
| } |
| |
| CoalescingCertVerifier::CoalescingCertVerifier( |
| std::unique_ptr<CertVerifier> verifier) |
| : verifier_(std::move(verifier)) { |
| verifier_->AddObserver(this); |
| } |
| |
| CoalescingCertVerifier::~CoalescingCertVerifier() { |
| verifier_->RemoveObserver(this); |
| } |
| |
| int CoalescingCertVerifier::Verify( |
| const RequestParams& params, |
| CertVerifyResult* verify_result, |
| CompletionOnceCallback callback, |
| std::unique_ptr<CertVerifier::Request>* out_req, |
| const NetLogWithSource& net_log) { |
| DCHECK(verify_result); |
| DCHECK(!callback.is_null()); |
| |
| out_req->reset(); |
| ++requests_; |
| |
| Job* job = FindJob(params); |
| if (job) { |
| // An identical request is in-flight and joinable, so just attach the |
| // callback. |
| ++inflight_joins_; |
| } else { |
| // No existing Jobs can be used. Create and start a new one. |
| std::unique_ptr<Job> new_job = |
| std::make_unique<Job>(this, params, net_log.net_log(), requests_ == 1); |
| int result = new_job->Start(verifier_.get()); |
| if (result != ERR_IO_PENDING) { |
| *verify_result = new_job->verify_result(); |
| return result; |
| } |
| |
| job = new_job.get(); |
| joinable_jobs_[params] = std::move(new_job); |
| } |
| |
| std::unique_ptr<CoalescingCertVerifier::Request> request = |
| std::make_unique<CoalescingCertVerifier::Request>( |
| job, verify_result, std::move(callback), net_log); |
| job->AddRequest(request.get()); |
| *out_req = std::move(request); |
| return ERR_IO_PENDING; |
| } |
| |
| void CoalescingCertVerifier::SetConfig(const CertVerifier::Config& config) { |
| verifier_->SetConfig(config); |
| |
| IncrementGenerationAndMakeCurrentJobsUnjoinable(); |
| } |
| |
| void CoalescingCertVerifier::AddObserver(CertVerifier::Observer* observer) { |
| verifier_->AddObserver(observer); |
| } |
| |
| void CoalescingCertVerifier::RemoveObserver(CertVerifier::Observer* observer) { |
| verifier_->RemoveObserver(observer); |
| } |
| |
| CoalescingCertVerifier::Job* CoalescingCertVerifier::FindJob( |
| const RequestParams& params) { |
| auto it = joinable_jobs_.find(params); |
| if (it != joinable_jobs_.end()) |
| return it->second.get(); |
| return nullptr; |
| } |
| |
| void CoalescingCertVerifier::RemoveJob(Job* job) { |
| // See if this was a job from the current configuration generation. |
| // Note: It's also necessary to compare that the underlying pointer is the |
| // same, and not merely a Job with the same parameters. |
| auto joinable_it = joinable_jobs_.find(job->params()); |
| if (joinable_it != joinable_jobs_.end() && joinable_it->second.get() == job) { |
| joinable_jobs_.erase(joinable_it); |
| return; |
| } |
| |
| // Otherwise, it MUST have been a job from a previous generation. |
| auto inflight_it = |
| base::ranges::find_if(inflight_jobs_, base::MatchesUniquePtr(job)); |
| DCHECK(inflight_it != inflight_jobs_.end()); |
| inflight_jobs_.erase(inflight_it); |
| return; |
| } |
| |
| void CoalescingCertVerifier::IncrementGenerationAndMakeCurrentJobsUnjoinable() { |
| for (auto& job : joinable_jobs_) { |
| inflight_jobs_.emplace_back(std::move(job.second)); |
| } |
| joinable_jobs_.clear(); |
| } |
| |
| void CoalescingCertVerifier::OnCertVerifierChanged() { |
| IncrementGenerationAndMakeCurrentJobsUnjoinable(); |
| } |
| |
| } // namespace net |