// Copyright 2018 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/trial_comparison_cert_verifier.h" #include #include #include "base/functional/bind.h" #include "base/location.h" #include "base/memory/raw_ptr.h" #include "base/metrics/histogram_macros.h" #include "base/time/time.h" #include "base/values.h" #include "build/build_config.h" #include "net/base/net_errors.h" #include "net/cert/cert_verify_proc.h" #include "net/cert/cert_verify_result.h" #include "net/cert/crl_set.h" #include "net/cert/multi_threaded_cert_verifier.h" #include "net/cert/trial_comparison_cert_verifier_util.h" #include "net/cert/x509_util.h" #include "net/log/net_log.h" #include "net/log/net_log_event_type.h" #include "net/log/net_log_source_type.h" #include "net/log/net_log_with_source.h" namespace net { namespace { base::Value::Dict JobResultParams(bool trial_success) { base::Value::Dict results; results.Set("trial_success", trial_success); return results; } } // namespace // The Job represents the state machine for a trial cert verification. // The Job is always owned by the TrialComparisonCertVerifier. However, a // reference to the Job is given by the CertVerifier::Request returned by // Start(), allowing the caller to indicate they're no longer interested in // the Job if it's not yet completed. // // The Job may be deleted while processing the initial verification completion, // by the client callback deleting the associated TrialComparisonCertVerifier. class TrialComparisonCertVerifier::Job { public: Job(const CertVerifier::Config& config, const CertVerifier::RequestParams& params, const NetLogWithSource& source_net_log, TrialComparisonCertVerifier* parent); Job(const Job&) = delete; Job& operator=(const Job&) = delete; ~Job(); // Start the Job, attempting first to verify with the parent's primary // verifier. |client_result|, |client_callback|, and |client_request| are // the parameters to the TrialComparisonCertVerifier::Verify(), allowing the // caller to register interest in the primary results. |client_request| will // be filled with a handle that the caller can use to abort the request. int Start(CertVerifyResult* client_result, CompletionOnceCallback client_callback, std::unique_ptr* client_request); void OnConfigChanged(); private: class Request; friend class Request; // If the Job has not yet completed the primary verification, this can be // called to indicate that the Request is no longer interested (e.g. the // Request is being deleted). void DetachRequest(); void Finish(bool is_success, TrialComparisonResult result_code); void FinishSuccess(TrialComparisonResult result_code); void FinishWithError(); // Called when the primary verifier is completed. // DANGER: |this| may be deleted when calling this. void OnPrimaryJobCompleted(int result); // Called when the initial trial comparison is completed. void OnTrialJobCompleted(int result); // The primary (system) and trial (built-in) verifiers may both construct // valid chains, but they use different paths. If that happens, a second // verification with the system verifier is used, using the path that the // built-in verifier constructed, to compare results. This is called when // that re-verification completes. void OnPrimaryReverifyWithSecondaryChainCompleted(int result); const CertVerifier::Config config_; bool config_changed_ = false; const CertVerifier::RequestParams params_; const NetLogWithSource net_log_; raw_ptr parent_ = nullptr; // Non-owned. raw_ptr request_ = nullptr; // Non-owned. // Results from the primary verification. base::TimeTicks primary_start_; int primary_error_; CertVerifyResult primary_result_; std::unique_ptr primary_request_; // Results from the trial verification. base::TimeTicks trial_start_; int trial_error_; CertVerifyResult trial_result_; std::unique_ptr trial_request_; // Results from the re-verification attempt. CertVerifyResult reverification_result_; std::unique_ptr reverification_request_; base::WeakPtrFactory weak_factory_{this}; }; // The Request is vended to the TrialComparisonCertVerifier::Verify() callers, // which they fully own and will ultimately destroy. It's used to coordinate // state with the Job. // // If the Job has not yet completed the primary verification request, deleting // this will abort that Job, ultimately leading to the Job being deleted. // However, if the primary verification has completed, deleting the Request // simply becomes a no-op. class TrialComparisonCertVerifier::Job::Request : public CertVerifier::Request { public: Request(TrialComparisonCertVerifier::Job* parent, CertVerifyResult* client_result, CompletionOnceCallback client_callback); Request(const Request&) = delete; Request& operator=(const Request&) = delete; ~Request() override; // Called when the Job has completed, and used to invoke the client // callback. // Note: |this| may be deleted after calling this method. void OnJobComplete(int result, const CertVerifyResult& verify_result); // Called when the Job is aborted (e.g. the underlying // TrialComparisonCertVerifier is being deleted). // Note: |this| may be deleted after calling this method. void OnJobAborted(); private: raw_ptr parent_; raw_ptr client_result_; CompletionOnceCallback client_callback_; }; TrialComparisonCertVerifier::Job::Job(const CertVerifier::Config& config, const CertVerifier::RequestParams& params, const NetLogWithSource& source_net_log, TrialComparisonCertVerifier* parent) : config_(config), params_(params), net_log_( NetLogWithSource::Make(source_net_log.net_log(), NetLogSourceType::TRIAL_CERT_VERIFIER_JOB)), parent_(parent) { net_log_.BeginEvent(NetLogEventType::TRIAL_CERT_VERIFIER_JOB); source_net_log.AddEventReferencingSource( NetLogEventType::TRIAL_CERT_VERIFIER_JOB_COMPARISON_STARTED, net_log_.source()); } TrialComparisonCertVerifier::Job::~Job() { if (request_) { // Note: May delete |request_|. request_->OnJobAborted(); request_ = nullptr; } if (parent_) { net_log_.AddEvent(NetLogEventType::CANCELLED); net_log_.EndEvent(NetLogEventType::TRIAL_CERT_VERIFIER_JOB); } } int TrialComparisonCertVerifier::Job::Start( CertVerifyResult* client_result, CompletionOnceCallback client_callback, std::unique_ptr* client_request) { DCHECK(!request_); DCHECK(parent_); primary_start_ = base::TimeTicks::Now(); // Unretained is safe because |primary_request_| will cancel the // callback on destruction. primary_error_ = parent_->primary_verifier()->Verify( params_, &primary_result_, base::BindOnce(&Job::OnPrimaryJobCompleted, base::Unretained(this)), &primary_request_, net_log_); if (primary_error_ != ERR_IO_PENDING) { *client_result = primary_result_; int result = primary_error_; // NOTE: |this| may be deleted here, in the event that every resulting // trial comparison also completes synchronously. OnPrimaryJobCompleted(result); return result; } // Create a new Request that will be used to manage the state for the // primary verification and allow cancellation. auto request = std::make_unique(this, client_result, std::move(client_callback)); request_ = request.get(); *client_request = std::move(request); return ERR_IO_PENDING; } void TrialComparisonCertVerifier::Job::OnConfigChanged() { config_changed_ = true; } void TrialComparisonCertVerifier::Job::DetachRequest() { // This should only be called while waiting for the primary verification. DCHECK(primary_request_); DCHECK(request_); request_ = nullptr; } void TrialComparisonCertVerifier::Job::Finish( bool is_success, TrialComparisonResult result_code) { // There should never be a pending initial verification. DCHECK(!request_); DCHECK(!primary_request_); UMA_HISTOGRAM_ENUMERATION("Net.CertVerifier_TrialComparisonResult", result_code); net_log_.EndEvent(NetLogEventType::TRIAL_CERT_VERIFIER_JOB, [&] { return JobResultParams(is_success); }); // Reset |parent_| to indicate the Job successfully completed (i.e. it was // not deleted by the TrialComparisonCertVerifier while still waiting for // results). TrialComparisonCertVerifier* parent = parent_; parent_ = nullptr; // Invoking the report callback may result in the // TrialComparisonCertVerifier being deleted, which will delete this Job. // Guard against this by grabbing a WeakPtr to |this|. base::WeakPtr weak_this = weak_factory_.GetWeakPtr(); if (!is_success) { parent->report_callback_.Run( params_.hostname(), params_.certificate(), config_.enable_rev_checking, config_.require_rev_checking_local_anchors, config_.enable_sha1_local_anchors, config_.disable_symantec_enforcement, params_.ocsp_response(), params_.sct_list(), primary_result_, trial_result_); } if (weak_this) { // If the Job is still alive, delete it now. parent->RemoveJob(this); return; } } void TrialComparisonCertVerifier::Job::FinishSuccess( TrialComparisonResult result_code) { Finish(/*is_success=*/true, result_code); } void TrialComparisonCertVerifier::Job::FinishWithError() { DCHECK(trial_error_ != primary_error_ || !CertVerifyResultEqual(trial_result_, primary_result_)); TrialComparisonResult result_code = TrialComparisonResult::kInvalid; if (primary_error_ == OK && trial_error_ == OK) { result_code = TrialComparisonResult::kBothValidDifferentDetails; } else if (primary_error_ == OK) { result_code = TrialComparisonResult::kPrimaryValidSecondaryError; } else if (trial_error_ == OK) { result_code = TrialComparisonResult::kPrimaryErrorSecondaryValid; } else { result_code = TrialComparisonResult::kBothErrorDifferentDetails; } Finish(/*is_success=*/false, result_code); } void TrialComparisonCertVerifier::Job::OnPrimaryJobCompleted(int result) { base::TimeDelta primary_latency = base::TimeTicks::Now() - primary_start_; primary_error_ = result; primary_request_.reset(); // Notify the original requestor that the primary verification has now // completed. This may result in |this| being deleted (if the associated // TrialComparisonCertVerifier is deleted); to detect this situation, grab // a WeakPtr to |this|. base::WeakPtr weak_this = weak_factory_.GetWeakPtr(); if (request_) { Request* request = request_; request_ = nullptr; // Note: May delete |this|. request->OnJobComplete(primary_error_, primary_result_); } if (!weak_this) return; if (config_changed_ || !parent_->trial_allowed()) { // If the trial will not be run, then delete |this|. parent_->RemoveJob(this); return; } // Only record the TrialPrimary histograms for the same set of requests // that TrialSecondary histograms will be recorded for, in order to get a // direct comparison. UMA_HISTOGRAM_CUSTOM_TIMES("Net.CertVerifier_Job_Latency_TrialPrimary", primary_latency, base::Milliseconds(1), base::Minutes(10), 100); trial_start_ = base::TimeTicks::Now(); int rv = parent_->trial_verifier()->Verify( params_, &trial_result_, base::BindOnce(&Job::OnTrialJobCompleted, base::Unretained(this)), &trial_request_, net_log_); if (rv != ERR_IO_PENDING) OnTrialJobCompleted(rv); // Note: May delete |this|. } void TrialComparisonCertVerifier::Job::OnTrialJobCompleted(int result) { DCHECK(primary_result_.verified_cert); DCHECK(trial_result_.verified_cert); base::TimeDelta latency = base::TimeTicks::Now() - trial_start_; trial_error_ = result; UMA_HISTOGRAM_CUSTOM_TIMES("Net.CertVerifier_Job_Latency_TrialSecondary", latency, base::Milliseconds(1), base::Minutes(10), 100); bool errors_equal = trial_error_ == primary_error_; bool details_equal = CertVerifyResultEqual(trial_result_, primary_result_); bool trial_success = errors_equal && details_equal; if (trial_success) { // Note: Will delete |this|. FinishSuccess(TrialComparisonResult::kEqual); return; } TrialComparisonResult ignorable_difference = IsSynchronouslyIgnorableDifference(primary_error_, primary_result_, trial_error_, trial_result_, config_.enable_sha1_local_anchors); if (ignorable_difference != TrialComparisonResult::kInvalid) { FinishSuccess(ignorable_difference); // Note: Will delete |this|. return; } const bool chains_equal = primary_result_.verified_cert->EqualsIncludingChain( trial_result_.verified_cert.get()); if (!chains_equal && (trial_error_ == OK || primary_error_ != OK)) { if (config_changed_) { // Note: Will delete |this|. FinishSuccess(TrialComparisonResult::kIgnoredConfigurationChanged); return; } // Chains were different, reverify the trial_result_.verified_cert chain // using the platform verifier and compare results again. RequestParams reverification_params( trial_result_.verified_cert, params_.hostname(), params_.flags(), params_.ocsp_response(), params_.sct_list()); int rv = parent_->primary_reverifier()->Verify( reverification_params, &reverification_result_, base::BindOnce(&Job::OnPrimaryReverifyWithSecondaryChainCompleted, base::Unretained(this)), &reverification_request_, net_log_); if (rv != ERR_IO_PENDING) { // Note: May delete |this|. OnPrimaryReverifyWithSecondaryChainCompleted(rv); } return; } FinishWithError(); // Note: Will delete |this|. } void TrialComparisonCertVerifier::Job:: OnPrimaryReverifyWithSecondaryChainCompleted(int result) { if (result == trial_error_ && CertVerifyResultEqual(reverification_result_, trial_result_)) { // The new result matches the builtin verifier, so this was just a // difference in the platform's path-building ability. // Ignore the difference. // // Note: Will delete |this|. FinishSuccess( TrialComparisonResult::kIgnoredDifferentPathReVerifiesEquivalent); return; } if (IsSynchronouslyIgnorableDifference(result, reverification_result_, trial_error_, trial_result_, config_.enable_sha1_local_anchors) != TrialComparisonResult::kInvalid) { // The new result matches if ignoring differences. Still use the // |kIgnoredDifferentPathReVerifiesEquivalent| code rather than the result // of IsSynchronouslyIgnorableDifference, since it's the higher level // description of what the difference is in this case. // // Note: Will delete |this|. FinishSuccess( TrialComparisonResult::kIgnoredDifferentPathReVerifiesEquivalent); return; } // Note: Will delete |this|. FinishWithError(); } TrialComparisonCertVerifier::Job::Request::Request( TrialComparisonCertVerifier::Job* parent, CertVerifyResult* client_result, CompletionOnceCallback client_callback) : parent_(parent), client_result_(client_result), client_callback_(std::move(client_callback)) {} TrialComparisonCertVerifier::Job::Request::~Request() { if (parent_) parent_->DetachRequest(); } void TrialComparisonCertVerifier::Job::Request::OnJobComplete( int result, const CertVerifyResult& verify_result) { DCHECK(parent_); parent_ = nullptr; *client_result_ = verify_result; // DANGER: |this| may be deleted when this callback is run (as well as // |parent_|, but that's been reset above). std::move(client_callback_).Run(result); } void TrialComparisonCertVerifier::Job::Request::OnJobAborted() { DCHECK(parent_); parent_ = nullptr; // DANGER: |this| may be deleted when this callback is destroyed. client_callback_.Reset(); } TrialComparisonCertVerifier::TrialComparisonCertVerifier( scoped_refptr verify_proc_factory, scoped_refptr cert_net_fetcher, const CertVerifyProcFactory::ImplParams& impl_params, ReportCallback report_callback) : report_callback_(std::move(report_callback)) { auto [primary_impl_params, trial_impl_params] = ProcessImplParams(impl_params); primary_verifier_ = std::make_unique( verify_proc_factory->CreateCertVerifyProc(cert_net_fetcher, primary_impl_params), verify_proc_factory); primary_reverifier_ = std::make_unique( verify_proc_factory->CreateCertVerifyProc(cert_net_fetcher, primary_impl_params), verify_proc_factory); trial_verifier_ = std::make_unique( verify_proc_factory->CreateCertVerifyProc(cert_net_fetcher, trial_impl_params), verify_proc_factory); primary_verifier_->AddObserver(this); primary_reverifier_->AddObserver(this); trial_verifier_->AddObserver(this); } TrialComparisonCertVerifier::~TrialComparisonCertVerifier() { primary_verifier_->RemoveObserver(this); primary_reverifier_->RemoveObserver(this); trial_verifier_->RemoveObserver(this); } int TrialComparisonCertVerifier::Verify(const RequestParams& params, CertVerifyResult* verify_result, CompletionOnceCallback callback, std::unique_ptr* out_req, const NetLogWithSource& net_log) { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); // Technically, the trial could still be active when // actual_use_chrome_root_store_=true, just by reversing everything. (Doing // the trial verifier first and returning that result, then doing the primary // verifier and comparing.) Probably not worth the trouble though. if (!trial_allowed() || actual_use_chrome_root_store_) { if (actual_use_chrome_root_store_) { return trial_verifier_->Verify(params, verify_result, std::move(callback), out_req, net_log); } else { return primary_verifier_->Verify(params, verify_result, std::move(callback), out_req, net_log); } } std::unique_ptr job = std::make_unique(config_, params, net_log, this); Job* job_ptr = job.get(); jobs_.insert(std::move(job)); return job_ptr->Start(verify_result, std::move(callback), out_req); } void TrialComparisonCertVerifier::SetConfig(const Config& config) { config_ = config; primary_verifier_->SetConfig(config); primary_reverifier_->SetConfig(config); trial_verifier_->SetConfig(config); // Notify all in-process jobs that the underlying configuration has changed. NotifyJobsOfConfigChange(); } void TrialComparisonCertVerifier::AddObserver( CertVerifier::Observer* observer) { // Delegate the notifications to the wrapped verifiers. We add the observer // to both `primary_verifier_` and `trial_verifier_` since either one might // be the one that matters depending on the configuration at the time. This // does mean the caller will get double notified, but shouldn't really matter // that much. It would be possible to avoid this by making a notification // proxy that only forwards notifications from the verifier that is currently // the "main" one, but it's probably not worth the trouble. // // Also this assumes that there is never a case where the // TrialComparisonCertVerifier itself would change something without the // wrapped verifiers also generating a notification. primary_verifier_->AddObserver(observer); trial_verifier_->AddObserver(observer); } void TrialComparisonCertVerifier::RemoveObserver( CertVerifier::Observer* observer) { trial_verifier_->RemoveObserver(observer); primary_verifier_->RemoveObserver(observer); } void TrialComparisonCertVerifier::UpdateVerifyProcData( scoped_refptr cert_net_fetcher, const CertVerifyProcFactory::ImplParams& impl_params) { bool previous_actual_use_chrome_root_store = actual_use_chrome_root_store_; auto [primary_impl_params, trial_impl_params] = ProcessImplParams(impl_params); // If the only change in the params was to switch the // `actual_use_chrome_root_store_` value, updating the underlying verifiers // is unnecessary, but it's probably not worth the trouble to try to avoid // it. primary_verifier_->UpdateVerifyProcData(cert_net_fetcher, primary_impl_params); primary_reverifier_->UpdateVerifyProcData(cert_net_fetcher, primary_impl_params); trial_verifier_->UpdateVerifyProcData(cert_net_fetcher, trial_impl_params); // The TrialComparisonCertVerifier is registered as an observer of the // underlying verifiers, and so the OnCertVerifierChanged method should be // triggered to call NotifyJobsOfConfigChange, so it isn't explicitly called // here for cases other than the `actual_use_chrome_root_store_` changing. if (previous_actual_use_chrome_root_store != actual_use_chrome_root_store_) { NotifyJobsOfConfigChange(); } } void TrialComparisonCertVerifier::RemoveJob(Job* job_ptr) { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); auto it = jobs_.find(job_ptr); DCHECK(it != jobs_.end()); jobs_.erase(it); } std::tuple TrialComparisonCertVerifier::ProcessImplParams( const CertVerifyProcFactory::ImplParams& impl_params) { actual_use_chrome_root_store_ = impl_params.use_chrome_root_store; CertVerifyProcFactory::ImplParams primary_impl_params(impl_params); primary_impl_params.use_chrome_root_store = false; CertVerifyProcFactory::ImplParams trial_impl_params(impl_params); trial_impl_params.use_chrome_root_store = true; return {std::move(primary_impl_params), std::move(trial_impl_params)}; } void TrialComparisonCertVerifier::NotifyJobsOfConfigChange() { for (auto& job : jobs_) { job->OnConfigChanged(); } } void TrialComparisonCertVerifier::OnCertVerifierChanged() { NotifyJobsOfConfigChange(); } } // namespace net