| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // 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 <memory> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/location.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/task/post_task.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "crypto/sha2.h" |
| #include "net/base/net_errors.h" |
| #include "net/cert/cert_verify_proc.h" |
| #include "net/cert/cert_verify_result.h" |
| #include "net/cert/ev_root_ca_metadata.h" |
| #include "net/cert/internal/cert_errors.h" |
| #include "net/cert/internal/parsed_certificate.h" |
| #include "net/cert/multi_threaded_cert_verifier.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 { |
| |
| std::unique_ptr<base::Value> TrialVerificationJobResultCallback( |
| bool trial_success, |
| NetLogCaptureMode capture_mode) { |
| std::unique_ptr<base::DictionaryValue> results(new base::DictionaryValue()); |
| results->SetKey("trial_success", base::Value(trial_success)); |
| return std::move(results); |
| } |
| |
| bool CertVerifyResultEqual(const CertVerifyResult& a, |
| const CertVerifyResult& b) { |
| return std::tie(a.cert_status, a.is_issued_by_known_root) == |
| std::tie(b.cert_status, b.is_issued_by_known_root) && |
| (!!a.verified_cert == !!b.verified_cert) && |
| (!a.verified_cert || |
| a.verified_cert->EqualsIncludingChain(b.verified_cert.get())); |
| } |
| |
| scoped_refptr<ParsedCertificate> ParsedCertificateFromBuffer( |
| CRYPTO_BUFFER* cert_handle, |
| CertErrors* errors) { |
| return ParsedCertificate::Create(bssl::UpRef(cert_handle), |
| x509_util::DefaultParseCertificateOptions(), |
| errors); |
| } |
| |
| ParsedCertificateList ParsedCertificateListFromX509Certificate( |
| const X509Certificate* cert) { |
| CertErrors parsing_errors; |
| |
| ParsedCertificateList certs; |
| scoped_refptr<ParsedCertificate> target = |
| ParsedCertificateFromBuffer(cert->cert_buffer(), &parsing_errors); |
| if (!target) |
| return {}; |
| certs.push_back(target); |
| |
| for (const auto& buf : cert->intermediate_buffers()) { |
| scoped_refptr<ParsedCertificate> intermediate = |
| ParsedCertificateFromBuffer(buf.get(), &parsing_errors); |
| if (!intermediate) |
| return {}; |
| certs.push_back(intermediate); |
| } |
| |
| return certs; |
| } |
| |
| // Tests whether cert has multiple EV policies, and at least one matches the |
| // root. This is not a complete test of EV, but just enough to give a possible |
| // explanation as to why the platform verifier did not validate as EV while |
| // builtin did. (Since only the builtin verifier correctly handles multiple |
| // candidate EV policies.) |
| bool CertHasMultipleEVPoliciesAndOneMatchesRoot(const X509Certificate* cert) { |
| if (cert->intermediate_buffers().empty()) |
| return false; |
| |
| ParsedCertificateList certs = ParsedCertificateListFromX509Certificate(cert); |
| if (certs.empty()) |
| return false; |
| |
| ParsedCertificate* leaf = certs.front().get(); |
| ParsedCertificate* root = certs.back().get(); |
| |
| if (!leaf->has_policy_oids()) |
| return false; |
| |
| const EVRootCAMetadata* ev_metadata = EVRootCAMetadata::GetInstance(); |
| std::set<der::Input> candidate_oids; |
| for (const der::Input& oid : leaf->policy_oids()) { |
| if (ev_metadata->IsEVPolicyOIDGivenBytes(oid)) |
| candidate_oids.insert(oid); |
| } |
| |
| if (candidate_oids.size() <= 1) |
| return false; |
| |
| SHA256HashValue root_fingerprint; |
| crypto::SHA256HashString(root->der_cert().AsStringPiece(), |
| root_fingerprint.data, |
| sizeof(root_fingerprint.data)); |
| |
| for (const der::Input& oid : candidate_oids) { |
| if (ev_metadata->HasEVPolicyOIDGivenBytes(root_fingerprint, oid)) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| } // namespace |
| |
| class TrialComparisonCertVerifier::TrialVerificationJob { |
| public: |
| TrialVerificationJob(const CertVerifier::Config& config, |
| const CertVerifier::RequestParams& params, |
| const NetLogWithSource& source_net_log, |
| TrialComparisonCertVerifier* cert_verifier, |
| int primary_error, |
| const CertVerifyResult& primary_result) |
| : config_(config), |
| config_changed_(false), |
| params_(params), |
| net_log_( |
| NetLogWithSource::Make(source_net_log.net_log(), |
| NetLogSourceType::TRIAL_CERT_VERIFIER_JOB)), |
| cert_verifier_(cert_verifier), |
| primary_error_(primary_error), |
| primary_result_(primary_result) { |
| net_log_.BeginEvent(NetLogEventType::TRIAL_CERT_VERIFIER_JOB); |
| source_net_log.AddEvent( |
| NetLogEventType::TRIAL_CERT_VERIFIER_JOB_COMPARISON_STARTED, |
| net_log_.source().ToEventParametersCallback()); |
| } |
| |
| ~TrialVerificationJob() { |
| if (cert_verifier_) { |
| net_log_.AddEvent(NetLogEventType::CANCELLED); |
| net_log_.EndEvent(NetLogEventType::TRIAL_CERT_VERIFIER_JOB); |
| } |
| } |
| |
| void Start() { |
| // Unretained is safe because trial_request_ will cancel the callback on |
| // destruction. |
| int rv = cert_verifier_->trial_verifier()->Verify( |
| params_, &trial_result_, |
| base::BindOnce(&TrialVerificationJob::OnJobCompleted, |
| base::Unretained(this)), |
| &trial_request_, net_log_); |
| if (rv != ERR_IO_PENDING) |
| OnJobCompleted(rv); |
| } |
| |
| void OnConfigChanged() { config_changed_ = true; } |
| |
| void Finish(bool is_success, TrialComparisonResult result_code) { |
| TrialComparisonCertVerifier* cert_verifier = cert_verifier_; |
| cert_verifier_ = nullptr; |
| |
| UMA_HISTOGRAM_ENUMERATION("Net.CertVerifier_TrialComparisonResult", |
| result_code); |
| |
| net_log_.EndEvent( |
| NetLogEventType::TRIAL_CERT_VERIFIER_JOB, |
| base::BindRepeating(&TrialVerificationJobResultCallback, is_success)); |
| |
| if (!is_success) { |
| cert_verifier->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, primary_result_, trial_result_); |
| } |
| |
| // |this| is deleted after RemoveJob returns. |
| cert_verifier->RemoveJob(this); |
| } |
| |
| void FinishSuccess(TrialComparisonResult result_code) { |
| Finish(true /* is_success */, result_code); |
| } |
| |
| void FinishWithError() { |
| DCHECK(trial_error_ != primary_error_ || |
| !CertVerifyResultEqual(trial_result_, primary_result_)); |
| |
| TrialComparisonResult result_code = kInvalid; |
| |
| if (primary_error_ == OK && trial_error_ == OK) { |
| result_code = kBothValidDifferentDetails; |
| } else if (primary_error_ == OK) { |
| result_code = kPrimaryValidSecondaryError; |
| } else if (trial_error_ == OK) { |
| result_code = kPrimaryErrorSecondaryValid; |
| } else { |
| result_code = kBothErrorDifferentDetails; |
| } |
| Finish(false /* is_success */, result_code); |
| } |
| |
| void OnJobCompleted(int trial_result_error) { |
| DCHECK(primary_result_.verified_cert); |
| DCHECK(trial_result_.verified_cert); |
| |
| trial_error_ = trial_result_error; |
| |
| bool errors_equal = trial_result_error == primary_error_; |
| bool details_equal = CertVerifyResultEqual(trial_result_, primary_result_); |
| bool trial_success = errors_equal && details_equal; |
| |
| if (trial_success) { |
| FinishSuccess(kEqual); |
| return; |
| } |
| |
| #if defined(OS_MACOSX) |
| if (primary_error_ == ERR_CERT_REVOKED && !config_.enable_rev_checking && |
| !(primary_result_.cert_status & CERT_STATUS_REV_CHECKING_ENABLED) && |
| !(trial_result_.cert_status & |
| (CERT_STATUS_REVOKED | CERT_STATUS_REV_CHECKING_ENABLED))) { |
| if (config_changed_) { |
| FinishSuccess(kIgnoredConfigurationChanged); |
| return; |
| } |
| // CertVerifyProcMac does some revocation checking even if we didn't want |
| // it. Try verifying with the trial verifier with revocation checking |
| // enabled, see if it then returns REVOKED. |
| |
| int rv = cert_verifier_->revocation_trial_verifier()->Verify( |
| params_, &reverification_result_, |
| base::BindOnce( |
| &TrialVerificationJob::OnMacRevcheckingReverificationJobCompleted, |
| base::Unretained(this)), |
| &reverification_request_, net_log_); |
| if (rv != ERR_IO_PENDING) |
| OnMacRevcheckingReverificationJobCompleted(rv); |
| return; |
| } |
| #endif |
| |
| 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_) { |
| FinishSuccess(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()); |
| |
| int rv = cert_verifier_->primary_reverifier()->Verify( |
| reverification_params, &reverification_result_, |
| base::BindOnce(&TrialVerificationJob:: |
| OnPrimaryReverifiyWithSecondaryChainCompleted, |
| base::Unretained(this)), |
| &reverification_request_, net_log_); |
| if (rv != ERR_IO_PENDING) |
| OnPrimaryReverifiyWithSecondaryChainCompleted(rv); |
| return; |
| } |
| |
| TrialComparisonResult ignorable_difference = |
| IsSynchronouslyIgnorableDifference(primary_error_, primary_result_, |
| trial_error_, trial_result_); |
| if (ignorable_difference != kInvalid) { |
| FinishSuccess(ignorable_difference); |
| return; |
| } |
| |
| FinishWithError(); |
| } |
| |
| // Check if the differences between the primary and trial verifiers can be |
| // ignored. This only handles differences that can be checked synchronously. |
| // If the difference is ignorable, returns the relevant TrialComparisonResult, |
| // otherwise returns kInvalid. |
| static TrialComparisonResult IsSynchronouslyIgnorableDifference( |
| int primary_error, |
| const CertVerifyResult& primary_result, |
| int trial_error, |
| const CertVerifyResult& trial_result) { |
| DCHECK(primary_result.verified_cert); |
| DCHECK(trial_result.verified_cert); |
| |
| if (primary_error == OK && |
| primary_result.verified_cert->intermediate_buffers().empty()) { |
| // Platform may support trusting a leaf certificate directly. Builtin |
| // verifier does not. See https://crbug.com/814994. |
| return kIgnoredLocallyTrustedLeaf; |
| } |
| |
| const bool chains_equal = |
| primary_result.verified_cert->EqualsIncludingChain( |
| trial_result.verified_cert.get()); |
| |
| if (chains_equal && (trial_result.cert_status & CERT_STATUS_IS_EV) && |
| !(primary_result.cert_status & CERT_STATUS_IS_EV) && |
| (primary_error == trial_error)) { |
| // The platform CertVerifyProc impls only check a single potential EV |
| // policy from the leaf. If the leaf had multiple policies, builtin |
| // verifier may verify it as EV when the platform verifier did not. |
| if (CertHasMultipleEVPoliciesAndOneMatchesRoot( |
| trial_result.verified_cert.get())) { |
| return kIgnoredMultipleEVPoliciesAndOneMatchesRoot; |
| } |
| } |
| return kInvalid; |
| } |
| |
| #if defined(OS_MACOSX) |
| void OnMacRevcheckingReverificationJobCompleted(int reverification_error) { |
| if (reverification_error == ERR_CERT_REVOKED) { |
| FinishSuccess(kIgnoredMacUndesiredRevocationChecking); |
| return; |
| } |
| FinishWithError(); |
| } |
| #endif |
| |
| void OnPrimaryReverifiyWithSecondaryChainCompleted(int reverification_error) { |
| if (reverification_error == 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. |
| FinishSuccess(kIgnoredDifferentPathReVerifiesEquivalent); |
| return; |
| } |
| |
| if (IsSynchronouslyIgnorableDifference(reverification_error, |
| reverification_result_, trial_error_, |
| trial_result_) != 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. |
| FinishSuccess(kIgnoredDifferentPathReVerifiesEquivalent); |
| return; |
| } |
| |
| FinishWithError(); |
| } |
| |
| private: |
| const CertVerifier::Config config_; |
| bool config_changed_; |
| const CertVerifier::RequestParams params_; |
| const NetLogWithSource net_log_; |
| TrialComparisonCertVerifier* cert_verifier_; // Non-owned. |
| |
| // Results from the trial verification. |
| int trial_error_; |
| CertVerifyResult trial_result_; |
| std::unique_ptr<CertVerifier::Request> trial_request_; |
| |
| // Saved results of the primary verification. |
| int primary_error_; |
| const CertVerifyResult primary_result_; |
| |
| // Results from re-verification attempt. |
| CertVerifyResult reverification_result_; |
| std::unique_ptr<CertVerifier::Request> reverification_request_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TrialVerificationJob); |
| }; |
| |
| TrialComparisonCertVerifier::TrialComparisonCertVerifier( |
| bool initial_allowed, |
| scoped_refptr<CertVerifyProc> primary_verify_proc, |
| scoped_refptr<CertVerifyProc> trial_verify_proc, |
| ReportCallback report_callback) |
| : allowed_(initial_allowed), |
| report_callback_(report_callback), |
| primary_verifier_( |
| MultiThreadedCertVerifier::CreateForDualVerificationTrial( |
| primary_verify_proc, |
| // Unretained is safe since the callback won't be called after |
| // |primary_verifier_| is destroyed. |
| base::BindRepeating( |
| &TrialComparisonCertVerifier::OnPrimaryVerifierComplete, |
| base::Unretained(this)), |
| true /* should_record_histograms */)), |
| primary_reverifier_( |
| std::make_unique<MultiThreadedCertVerifier>(primary_verify_proc)), |
| trial_verifier_(MultiThreadedCertVerifier::CreateForDualVerificationTrial( |
| trial_verify_proc, |
| // Unretained is safe since the callback won't be called after |
| // |trial_verifier_| is destroyed. |
| base::BindRepeating( |
| &TrialComparisonCertVerifier::OnTrialVerifierComplete, |
| base::Unretained(this)), |
| false /* should_record_histograms */)), |
| revocation_trial_verifier_( |
| MultiThreadedCertVerifier::CreateForDualVerificationTrial( |
| trial_verify_proc, |
| // Unretained is safe since the callback won't be called after |
| // |trial_verifier_| is destroyed. |
| base::BindRepeating( |
| &TrialComparisonCertVerifier::OnTrialVerifierComplete, |
| base::Unretained(this)), |
| false /* should_record_histograms */)) { |
| CertVerifier::Config config; |
| config.enable_rev_checking = true; |
| revocation_trial_verifier_->SetConfig(config); |
| } |
| |
| TrialComparisonCertVerifier::~TrialComparisonCertVerifier() = default; |
| |
| int TrialComparisonCertVerifier::Verify(const RequestParams& params, |
| CertVerifyResult* verify_result, |
| CompletionOnceCallback callback, |
| std::unique_ptr<Request>* out_req, |
| const NetLogWithSource& net_log) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| return primary_verifier_->Verify(params, verify_result, std::move(callback), |
| out_req, net_log); |
| } |
| |
| void TrialComparisonCertVerifier::SetConfig(const Config& config) { |
| config_ = config; |
| |
| primary_verifier_->SetConfig(config); |
| primary_reverifier_->SetConfig(config); |
| trial_verifier_->SetConfig(config); |
| |
| // Always enable revocation checking for the revocation trial verifier. |
| CertVerifier::Config config_with_revocation = config; |
| config_with_revocation.enable_rev_checking = true; |
| revocation_trial_verifier_->SetConfig(config_with_revocation); |
| |
| // Notify all in-process jobs that the underlying configuration has changed. |
| for (auto& job : jobs_) { |
| job->OnConfigChanged(); |
| } |
| } |
| |
| void TrialComparisonCertVerifier::OnPrimaryVerifierComplete( |
| const RequestParams& params, |
| const NetLogWithSource& net_log, |
| int primary_error, |
| const CertVerifyResult& primary_result, |
| base::TimeDelta primary_latency, |
| bool is_first_job) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| if (!trial_allowed()) |
| 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::TimeDelta::FromMilliseconds(1), |
| base::TimeDelta::FromMinutes(10), 100); |
| if (is_first_job) { |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "Net.CertVerifier_First_Job_Latency_TrialPrimary", primary_latency, |
| base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromMinutes(10), |
| 100); |
| } |
| |
| std::unique_ptr<TrialVerificationJob> job = |
| std::make_unique<TrialVerificationJob>(config_, params, net_log, this, |
| primary_error, primary_result); |
| TrialVerificationJob* job_ptr = job.get(); |
| jobs_.insert(std::move(job)); |
| job_ptr->Start(); |
| } |
| |
| void TrialComparisonCertVerifier::OnTrialVerifierComplete( |
| const RequestParams& params, |
| const NetLogWithSource& net_log, |
| int trial_error, |
| const CertVerifyResult& trial_result, |
| base::TimeDelta latency, |
| bool is_first_job) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| UMA_HISTOGRAM_CUSTOM_TIMES("Net.CertVerifier_Job_Latency_TrialSecondary", |
| latency, base::TimeDelta::FromMilliseconds(1), |
| base::TimeDelta::FromMinutes(10), 100); |
| if (is_first_job) { |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "Net.CertVerifier_First_Job_Latency_TrialSecondary", latency, |
| base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromMinutes(10), |
| 100); |
| } |
| } |
| |
| void TrialComparisonCertVerifier::RemoveJob(TrialVerificationJob* job_ptr) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| auto it = jobs_.find(job_ptr); |
| DCHECK(it != jobs_.end()); |
| jobs_.erase(it); |
| } |
| |
| } // namespace net |