// Copyright (c) 2012 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/base/multi_threaded_cert_verifier.h" #include #include "base/bind.h" #include "base/bind_helpers.h" #include "base/compiler_specific.h" #include "base/message_loop.h" #include "base/metrics/histogram.h" #include "base/stl_util.h" #include "base/synchronization/lock.h" #include "base/time.h" #include "base/threading/worker_pool.h" #include "net/base/cert_verify_proc.h" #include "net/base/crl_set.h" #include "net/base/net_errors.h" #include "net/base/net_log.h" #include "net/base/x509_certificate.h" #include "net/base/x509_certificate_net_log_param.h" #if defined(USE_NSS) || defined(OS_IOS) #include // PR_DetachThread #endif namespace net { //////////////////////////////////////////////////////////////////////////// // Life of a request: // // MultiThreadedCertVerifier CertVerifierJob CertVerifierWorker Request // | (origin loop) (worker loop) // | // Verify() // |---->------------------------------------- // | // |---->------------------- // | // |---->------------------------------------------------------- // | // |---->---------------------------------------Start // | | // | PostTask // | // | // |---->-------------------AddRequest | // | // | // | // Finish // | // PostTask // // | // DoReply // |----<-----------------------------------------| // HandleResult // | // |---->------------------HandleResult // | // |------>---------------------------Post // // // // On a cache hit, MultiThreadedCertVerifier::Verify() returns synchronously // without posting a task to a worker thread. namespace { // The default value of max_cache_entries_. const unsigned kMaxCacheEntries = 256; // The number of seconds for which we'll cache a cache entry. const unsigned kTTLSecs = 1800; // 30 minutes. } // namespace MultiThreadedCertVerifier::CachedResult::CachedResult() : error(ERR_FAILED) {} MultiThreadedCertVerifier::CachedResult::~CachedResult() {} MultiThreadedCertVerifier::CacheValidityPeriod::CacheValidityPeriod( const base::Time& now) : verification_time(now), expiration_time(now) { } MultiThreadedCertVerifier::CacheValidityPeriod::CacheValidityPeriod( const base::Time& now, const base::Time& expiration) : verification_time(now), expiration_time(expiration) { } bool MultiThreadedCertVerifier::CacheExpirationFunctor::operator()( const CacheValidityPeriod& now, const CacheValidityPeriod& expiration) const { // Ensure this functor is being used for expiration only, and not strict // weak ordering/sorting. |now| should only ever contain a single // base::Time. // Note: DCHECK_EQ is not used due to operator<< overloading requirements. DCHECK(now.verification_time == now.expiration_time); // |now| contains only a single time (verification_time), while |expiration| // contains the validity range - both when the certificate was verified and // when the verification result should expire. // // If the user receives a "not yet valid" message, and adjusts their clock // foward to the correct time, this will (typically) cause // now.verification_time to advance past expiration.expiration_time, thus // treating the cached result as an expired entry and re-verifying. // If the user receives a "expired" message, and adjusts their clock // backwards to the correct time, this will cause now.verification_time to // be less than expiration_verification_time, thus treating the cached // result as an expired entry and re-verifying. // If the user receives either of those messages, and does not adjust their // clock, then the result will be (typically) be cached until the expiration // TTL. // // This algorithm is only problematic if the user consistently keeps // adjusting their clock backwards in increments smaller than the expiration // TTL, in which case, cached elements continue to be added. However, // because the cache has a fixed upper bound, if no entries are expired, a // 'random' entry will be, thus keeping the memory constraints bounded over // time. return now.verification_time >= expiration.verification_time && now.verification_time < expiration.expiration_time; }; // Represents the output and result callback of a request. class CertVerifierRequest { public: CertVerifierRequest(const CompletionCallback& callback, CertVerifyResult* verify_result, const BoundNetLog& net_log) : callback_(callback), verify_result_(verify_result), net_log_(net_log) { net_log_.BeginEvent(NetLog::TYPE_CERT_VERIFIER_REQUEST); } ~CertVerifierRequest() { } // Ensures that the result callback will never be made. void Cancel() { callback_.Reset(); verify_result_ = NULL; net_log_.AddEvent(NetLog::TYPE_CANCELLED); net_log_.EndEvent(NetLog::TYPE_CERT_VERIFIER_REQUEST); } // Copies the contents of |verify_result| to the caller's // CertVerifyResult and calls the callback. void Post(const MultiThreadedCertVerifier::CachedResult& verify_result) { if (!callback_.is_null()) { net_log_.EndEvent(NetLog::TYPE_CERT_VERIFIER_REQUEST); *verify_result_ = verify_result.result; callback_.Run(verify_result.error); } delete this; } bool canceled() const { return callback_.is_null(); } const BoundNetLog& net_log() const { return net_log_; } private: CompletionCallback callback_; CertVerifyResult* verify_result_; const BoundNetLog net_log_; }; // CertVerifierWorker runs on a worker thread and takes care of the blocking // process of performing the certificate verification. Deletes itself // eventually if Start() succeeds. class CertVerifierWorker { public: CertVerifierWorker(CertVerifyProc* verify_proc, X509Certificate* cert, const std::string& hostname, int flags, CRLSet* crl_set, MultiThreadedCertVerifier* cert_verifier) : verify_proc_(verify_proc), cert_(cert), hostname_(hostname), flags_(flags), crl_set_(crl_set), origin_loop_(MessageLoop::current()), cert_verifier_(cert_verifier), canceled_(false), error_(ERR_FAILED) { } // Returns the certificate being verified. May only be called /before/ // Start() is called. X509Certificate* certificate() const { return cert_; } bool Start() { DCHECK_EQ(MessageLoop::current(), origin_loop_); return base::WorkerPool::PostTask( FROM_HERE, base::Bind(&CertVerifierWorker::Run, base::Unretained(this)), true /* task is slow */); } // Cancel is called from the origin loop when the MultiThreadedCertVerifier is // getting deleted. void Cancel() { DCHECK_EQ(MessageLoop::current(), origin_loop_); base::AutoLock locked(lock_); canceled_ = true; } private: void Run() { // Runs on a worker thread. error_ = verify_proc_->Verify(cert_, hostname_, flags_, crl_set_, &verify_result_); #if defined(USE_NSS) || defined(OS_IOS) // Detach the thread from NSPR. // Calling NSS functions attaches the thread to NSPR, which stores // the NSPR thread ID in thread-specific data. // The threads in our thread pool terminate after we have called // PR_Cleanup. Unless we detach them from NSPR, net_unittests gets // segfaults on shutdown when the threads' thread-specific data // destructors run. PR_DetachThread(); #endif Finish(); } // DoReply runs on the origin thread. void DoReply() { DCHECK_EQ(MessageLoop::current(), origin_loop_); { // We lock here because the worker thread could still be in Finished, // after the PostTask, but before unlocking |lock_|. If we do not lock in // this case, we will end up deleting a locked Lock, which can lead to // memory leaks or worse errors. base::AutoLock locked(lock_); if (!canceled_) { cert_verifier_->HandleResult(cert_, hostname_, flags_, error_, verify_result_); } } delete this; } void Finish() { // Runs on the worker thread. // We assume that the origin loop outlives the MultiThreadedCertVerifier. If // the MultiThreadedCertVerifier is deleted, it will call Cancel on us. If // it does so before the Acquire, we'll delete ourselves and return. If it's // trying to do so concurrently, then it'll block on the lock and we'll call // PostTask while the MultiThreadedCertVerifier (and therefore the // MessageLoop) is still alive. // If it does so after this function, we assume that the MessageLoop will // process pending tasks. In which case we'll notice the |canceled_| flag // in DoReply. bool canceled; { base::AutoLock locked(lock_); canceled = canceled_; if (!canceled) { origin_loop_->PostTask( FROM_HERE, base::Bind( &CertVerifierWorker::DoReply, base::Unretained(this))); } } if (canceled) delete this; } scoped_refptr verify_proc_; scoped_refptr cert_; const std::string hostname_; const int flags_; scoped_refptr crl_set_; MessageLoop* const origin_loop_; MultiThreadedCertVerifier* const cert_verifier_; // lock_ protects canceled_. base::Lock lock_; // If canceled_ is true, // * origin_loop_ cannot be accessed by the worker thread, // * cert_verifier_ cannot be accessed by any thread. bool canceled_; int error_; CertVerifyResult verify_result_; DISALLOW_COPY_AND_ASSIGN(CertVerifierWorker); }; // A CertVerifierJob is a one-to-one counterpart of a CertVerifierWorker. It // lives only on the CertVerifier's origin message loop. class CertVerifierJob { public: CertVerifierJob(CertVerifierWorker* worker, const BoundNetLog& net_log) : start_time_(base::TimeTicks::Now()), worker_(worker), net_log_(net_log) { net_log_.BeginEvent( NetLog::TYPE_CERT_VERIFIER_JOB, base::Bind(&NetLogX509CertificateCallback, base::Unretained(worker_->certificate()))); } ~CertVerifierJob() { if (worker_) { net_log_.AddEvent(NetLog::TYPE_CANCELLED); net_log_.EndEvent(NetLog::TYPE_CERT_VERIFIER_JOB); worker_->Cancel(); DeleteAllCanceled(); } } void AddRequest(CertVerifierRequest* request) { request->net_log().AddEvent( NetLog::TYPE_CERT_VERIFIER_REQUEST_BOUND_TO_JOB, net_log_.source().ToEventParametersCallback()); requests_.push_back(request); } void HandleResult( const MultiThreadedCertVerifier::CachedResult& verify_result) { worker_ = NULL; net_log_.EndEvent(NetLog::TYPE_CERT_VERIFIER_JOB); UMA_HISTOGRAM_CUSTOM_TIMES("Net.CertVerifier_Job_Latency", base::TimeTicks::Now() - start_time_, base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromMinutes(10), 100); PostAll(verify_result); } private: void PostAll(const MultiThreadedCertVerifier::CachedResult& verify_result) { std::vector requests; requests_.swap(requests); for (std::vector::iterator i = requests.begin(); i != requests.end(); i++) { (*i)->Post(verify_result); // Post() causes the CertVerifierRequest to delete itself. } } void DeleteAllCanceled() { for (std::vector::iterator i = requests_.begin(); i != requests_.end(); i++) { if ((*i)->canceled()) { delete *i; } else { LOG(DFATAL) << "CertVerifierRequest leaked!"; } } } const base::TimeTicks start_time_; std::vector requests_; CertVerifierWorker* worker_; const BoundNetLog net_log_; }; MultiThreadedCertVerifier::MultiThreadedCertVerifier( CertVerifyProc* verify_proc) : cache_(kMaxCacheEntries), requests_(0), cache_hits_(0), inflight_joins_(0), verify_proc_(verify_proc) { CertDatabase::GetInstance()->AddObserver(this); } MultiThreadedCertVerifier::~MultiThreadedCertVerifier() { STLDeleteValues(&inflight_); CertDatabase::GetInstance()->RemoveObserver(this); } int MultiThreadedCertVerifier::Verify(X509Certificate* cert, const std::string& hostname, int flags, CRLSet* crl_set, CertVerifyResult* verify_result, const CompletionCallback& callback, RequestHandle* out_req, const BoundNetLog& net_log) { DCHECK(CalledOnValidThread()); if (callback.is_null() || !verify_result || hostname.empty()) { *out_req = NULL; return ERR_INVALID_ARGUMENT; } requests_++; const RequestParams key(cert->fingerprint(), cert->ca_fingerprint(), hostname, flags); const CertVerifierCache::value_type* cached_entry = cache_.Get(key, CacheValidityPeriod(base::Time::Now())); if (cached_entry) { ++cache_hits_; *out_req = NULL; *verify_result = cached_entry->result; return cached_entry->error; } // No cache hit. See if an identical request is currently in flight. CertVerifierJob* job; std::map::const_iterator j; j = inflight_.find(key); if (j != inflight_.end()) { // An identical request is in flight already. We'll just attach our // callback. inflight_joins_++; job = j->second; } else { // Need to make a new request. CertVerifierWorker* worker = new CertVerifierWorker(verify_proc_, cert, hostname, flags, crl_set, this); job = new CertVerifierJob( worker, BoundNetLog::Make(net_log.net_log(), NetLog::SOURCE_CERT_VERIFIER_JOB)); if (!worker->Start()) { delete job; delete worker; *out_req = NULL; // TODO(wtc): log to the NetLog. LOG(ERROR) << "CertVerifierWorker couldn't be started."; return ERR_INSUFFICIENT_RESOURCES; // Just a guess. } inflight_.insert(std::make_pair(key, job)); } CertVerifierRequest* request = new CertVerifierRequest(callback, verify_result, net_log); job->AddRequest(request); *out_req = request; return ERR_IO_PENDING; } void MultiThreadedCertVerifier::CancelRequest(RequestHandle req) { DCHECK(CalledOnValidThread()); CertVerifierRequest* request = reinterpret_cast(req); request->Cancel(); } MultiThreadedCertVerifier::RequestParams::RequestParams( const SHA1HashValue& cert_fingerprint_arg, const SHA1HashValue& ca_fingerprint_arg, const std::string& hostname_arg, int flags_arg) : cert_fingerprint(cert_fingerprint_arg), ca_fingerprint(ca_fingerprint_arg), hostname(hostname_arg), flags(flags_arg) {} // HandleResult is called by CertVerifierWorker on the origin message loop. // It deletes CertVerifierJob. void MultiThreadedCertVerifier::HandleResult( X509Certificate* cert, const std::string& hostname, int flags, int error, const CertVerifyResult& verify_result) { DCHECK(CalledOnValidThread()); const RequestParams key(cert->fingerprint(), cert->ca_fingerprint(), hostname, flags); CachedResult cached_result; cached_result.error = error; cached_result.result = verify_result; base::Time now = base::Time::Now(); cache_.Put( key, cached_result, CacheValidityPeriod(now), CacheValidityPeriod(now, now + base::TimeDelta::FromSeconds(kTTLSecs))); std::map::iterator j; j = inflight_.find(key); if (j == inflight_.end()) { NOTREACHED(); return; } CertVerifierJob* job = j->second; inflight_.erase(j); job->HandleResult(cached_result); delete job; } void MultiThreadedCertVerifier::OnCertTrustChanged( const X509Certificate* cert) { DCHECK(CalledOnValidThread()); ClearCache(); } } // namespace net