| // 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 <vector> |
| |
| #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 <private/pprthred.h> // PR_DetachThread |
| #endif |
| |
| namespace net { |
| |
| //////////////////////////////////////////////////////////////////////////// |
| |
| // Life of a request: |
| // |
| // MultiThreadedCertVerifier CertVerifierJob CertVerifierWorker Request |
| // | (origin loop) (worker loop) |
| // | |
| // Verify() |
| // |---->-------------------------------------<creates> |
| // | |
| // |---->-------------------<creates> |
| // | |
| // |---->-------------------------------------------------------<creates> |
| // | |
| // |---->---------------------------------------Start |
| // | | |
| // | PostTask |
| // | |
| // | <starts verifying> |
| // |---->-------------------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<CertVerifyProc> verify_proc_; |
| scoped_refptr<X509Certificate> cert_; |
| const std::string hostname_; |
| const int flags_; |
| scoped_refptr<CRLSet> 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<CertVerifierRequest*> requests; |
| requests_.swap(requests); |
| |
| for (std::vector<CertVerifierRequest*>::iterator |
| i = requests.begin(); i != requests.end(); i++) { |
| (*i)->Post(verify_result); |
| // Post() causes the CertVerifierRequest to delete itself. |
| } |
| } |
| |
| void DeleteAllCanceled() { |
| for (std::vector<CertVerifierRequest*>::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<CertVerifierRequest*> 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<RequestParams, CertVerifierJob*>::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<CertVerifierRequest*>(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<RequestParams, CertVerifierJob*>::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 |