blob: eb82799d3253ab9b7ae3fd71bc5a4bd2ef407e69 [file] [log] [blame]
// 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