From 620f3e50bdbb11cf69dd239b5a976fcdf0d2dade Mon Sep 17 00:00:00 2001 From: "asargent@chromium.org" Date: Thu, 15 May 2014 17:00:52 +0000 Subject: [PATCH] A bunch of remaining parts of extension content verification -The real guts of content_hash_fetcher.cc, which fetches the verified_contents.json file from the webstore if needed and also runs tasks to compute and cache the block-level hashes of all files in an extension. -The real guts of content_hash_reader.cc, which uses the work done by the content_hash_fetcher during validation of extension file content as it's read off of disk at time of use. -Code to avoid verifying transcoded files (images used in browser process, and message catalogs). -Don't allow downgrade of mode via kForceFieldTrials command line switch -Various bits of plumbing to support all of the above BUG=369895 R=rockot@chromium.org Review URL: https://codereview.chromium.org/289533003 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@270694 0039d316-1c4b-4281-b951-d872f2087c98 --- chrome/browser/extensions/extension_system_impl.cc | 42 ++- chrome/common/extensions/extension_constants.cc | 6 +- chrome/common/extensions/extension_constants.h | 7 +- extensions/browser/computed_hashes.cc | 129 +++++++ extensions/browser/computed_hashes.h | 64 ++++ extensions/browser/content_hash_fetcher.cc | 393 ++++++++++++++++++++- extensions/browser/content_hash_fetcher.h | 15 + extensions/browser/content_hash_reader.cc | 74 +++- extensions/browser/content_hash_reader.h | 14 +- extensions/browser/content_verifier.cc | 50 ++- extensions/browser/content_verifier_delegate.h | 8 + extensions/common/constants.cc | 6 + extensions/common/constants.h | 10 + extensions/common/file_util.cc | 8 + extensions/common/file_util.h | 4 + extensions/extensions.gyp | 2 + 16 files changed, 816 insertions(+), 16 deletions(-) create mode 100644 extensions/browser/computed_hashes.cc create mode 100644 extensions/browser/computed_hashes.h diff --git a/chrome/browser/extensions/extension_system_impl.cc b/chrome/browser/extensions/extension_system_impl.cc index dab00948c4b3..3e8d350178ac 100644 --- a/chrome/browser/extensions/extension_system_impl.cc +++ b/chrome/browser/extensions/extension_system_impl.cc @@ -26,12 +26,14 @@ #include "chrome/browser/extensions/standard_management_policy_provider.h" #include "chrome/browser/extensions/state_store.h" #include "chrome/browser/extensions/unpacked_installer.h" +#include "chrome/browser/extensions/updater/manifest_fetch_data.h" #include "chrome/browser/extensions/user_script_master.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/profiles/profile_manager.h" #include "chrome/browser/ui/webui/extensions/extension_icon_source.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/chrome_version_info.h" +#include "chrome/common/extensions/extension_file_util.h" #include "chrome/common/extensions/features/feature_channel.h" #include "chrome/common/extensions/manifest_url_handler.h" #include "content/public/browser/browser_thread.h" @@ -53,6 +55,7 @@ #include "extensions/common/constants.h" #include "extensions/common/extension.h" #include "extensions/common/manifest.h" +#include "net/base/escape.h" #if defined(ENABLE_NOTIFICATIONS) #include "chrome/browser/notifications/desktop_notification_service.h" @@ -151,9 +154,21 @@ class ContentVerifierDelegateImpl : public ContentVerifierDelegate { virtual ~ContentVerifierDelegateImpl() {} virtual bool ShouldBeVerified(const Extension& extension) OVERRIDE { - return ((extension.is_extension() || extension.is_legacy_packaged_app()) && - ManifestURL::UpdatesFromGallery(&extension) && - Manifest::IsAutoUpdateableLocation(extension.location())); + if (!extension.is_extension() && !extension.is_legacy_packaged_app()) + return false; + if (!Manifest::IsAutoUpdateableLocation(extension.location())) + return false; + + if (!ManifestURL::UpdatesFromGallery(&extension)) { + // It's possible that the webstore update url was overridden for testing + // so also consider extensions with the default (production) update url + // to be from the store as well. + GURL default_webstore_url = extension_urls::GetDefaultWebstoreUpdateUrl(); + if (ManifestURL::GetUpdateURL(&extension) != default_webstore_url) + return false; + } + + return true; } virtual const ContentVerifierKey& PublicKey() OVERRIDE { @@ -165,7 +180,26 @@ class ContentVerifierDelegateImpl : public ContentVerifierDelegate { virtual GURL GetSignatureFetchUrl(const std::string& extension_id, const base::Version& version) OVERRIDE { - return GURL(); + // TODO(asargent) Factor out common code from the extension updater's + // ManifestFetchData class that can be shared for use here. + std::vector parts; + parts.push_back("uc"); + parts.push_back("installsource=signature"); + parts.push_back("id=" + extension_id); + parts.push_back("v=" + version.GetString()); + std::string x_value = + net::EscapeQueryParamValue(JoinString(parts, "&"), true); + std::string query = "response=redirect&x=" + x_value; + + GURL base_url = extension_urls::GetWebstoreUpdateUrl(); + GURL::Replacements replacements; + replacements.SetQuery(query.c_str(), url::Component(0, query.length())); + return base_url.ReplaceComponents(replacements); + } + + virtual std::set GetBrowserImagePaths( + const extensions::Extension* extension) OVERRIDE { + return extension_file_util::GetBrowserImagePaths(extension); } virtual void VerifyFailed(const std::string& extension_id) OVERRIDE { diff --git a/chrome/common/extensions/extension_constants.cc b/chrome/common/extensions/extension_constants.cc index 66bded285f14..4d0956bd906b 100644 --- a/chrome/common/extensions/extension_constants.cc +++ b/chrome/common/extensions/extension_constants.cc @@ -61,7 +61,11 @@ GURL GetWebstoreUpdateUrl() { if (cmdline->HasSwitch(switches::kAppsGalleryUpdateURL)) return GURL(cmdline->GetSwitchValueASCII(switches::kAppsGalleryUpdateURL)); else - return GURL(kGalleryUpdateHttpsUrl); + return GetDefaultWebstoreUpdateUrl(); +} + +GURL GetDefaultWebstoreUpdateUrl() { + return GURL(kGalleryUpdateHttpsUrl); } bool IsWebstoreUpdateUrl(const GURL& update_url) { diff --git a/chrome/common/extensions/extension_constants.h b/chrome/common/extensions/extension_constants.h index 936330f90314..f202165c5bbe 100644 --- a/chrome/common/extensions/extension_constants.h +++ b/chrome/common/extensions/extension_constants.h @@ -41,9 +41,14 @@ GURL GetWebstoreJsonSearchUrl(const std::string& query, const std::string& hl); // Returns the URL of the web store search results page for |query|. GURL GetWebstoreSearchPageUrl(const std::string& query); -// Return the update URL used by gallery/webstore extensions/apps. +// Return the update URL used by gallery/webstore extensions/apps. This may +// have been overridden by a command line flag for testing purposes. GURL GetWebstoreUpdateUrl(); +// This returns the compile-time constant webstore update url. Usually you +// should prefer using GetWebstoreUpdateUrl. +GURL GetDefaultWebstoreUpdateUrl(); + // Returns whether the URL is the webstore update URL (just considering host // and path, not scheme, query, etc.) bool IsWebstoreUpdateUrl(const GURL& update_url); diff --git a/extensions/browser/computed_hashes.cc b/extensions/browser/computed_hashes.cc new file mode 100644 index 000000000000..4a8a8527206a --- /dev/null +++ b/extensions/browser/computed_hashes.cc @@ -0,0 +1,129 @@ +// Copyright 2014 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 "extensions/browser/computed_hashes.h" + +#include "base/base64.h" +#include "base/file_util.h" +#include "base/files/file_path.h" +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" + +namespace { +const char kPathKey[] = "path"; +const char kBlockSizeKey[] = "block_size"; +const char kBlockHashesKey[] = "block_hashes"; +} + +namespace extensions { + +ComputedHashes::Reader::Reader() { +} +ComputedHashes::Reader::~Reader() { +} + +bool ComputedHashes::Reader::InitFromFile(const base::FilePath& path) { + std::string contents; + if (!base::ReadFileToString(path, &contents)) + return false; + + base::ListValue* all_hashes = NULL; + scoped_ptr value(base::JSONReader::Read(contents)); + if (!value.get() || !value->GetAsList(&all_hashes)) + return false; + + for (size_t i = 0; i < all_hashes->GetSize(); i++) { + base::DictionaryValue* dictionary = NULL; + if (!all_hashes->GetDictionary(i, &dictionary)) + return false; + + std::string relative_path_utf8; + if (!dictionary->GetString(kPathKey, &relative_path_utf8)) + return false; + + int block_size; + if (!dictionary->GetInteger(kBlockSizeKey, &block_size)) + return false; + if (block_size <= 0 || ((block_size % 1024) != 0)) { + LOG(ERROR) << "Invalid block size: " << block_size; + block_size = 0; + return false; + } + + base::ListValue* hashes_list = NULL; + if (!dictionary->GetList(kBlockHashesKey, &hashes_list)) + return false; + + base::FilePath relative_path = + base::FilePath::FromUTF8Unsafe(relative_path_utf8); + + data_[relative_path] = HashInfo(block_size, std::vector()); + std::vector* hashes = &(data_[relative_path].second); + + for (size_t j = 0; j < hashes_list->GetSize(); j++) { + std::string encoded; + if (!hashes_list->GetString(j, &encoded)) + return false; + + hashes->push_back(std::string()); + std::string* decoded = &hashes->back(); + if (!base::Base64Decode(encoded, decoded)) { + hashes->clear(); + return false; + } + } + } + return true; +} + +bool ComputedHashes::Reader::GetHashes(const base::FilePath& relative_path, + int* block_size, + std::vector* hashes) { + std::map::iterator i = data_.find(relative_path); + if (i == data_.end()) + return false; + HashInfo& info = i->second; + *block_size = info.first; + *hashes = info.second; + return true; +} + +ComputedHashes::Writer::Writer() { +} +ComputedHashes::Writer::~Writer() { +} + +void ComputedHashes::Writer::AddHashes(const base::FilePath& relative_path, + int block_size, + const std::vector& hashes) { + base::DictionaryValue* dict = new base::DictionaryValue(); + base::ListValue* block_hashes = new base::ListValue(); + file_list_.Append(dict); + dict->SetString(kPathKey, relative_path.AsUTF8Unsafe()); + dict->SetInteger(kBlockSizeKey, block_size); + dict->Set(kBlockHashesKey, block_hashes); + + for (std::vector::const_iterator i = hashes.begin(); + i != hashes.end(); + ++i) { + std::string encoded; + base::Base64Encode(*i, &encoded); + block_hashes->AppendString(encoded); + } +} + +bool ComputedHashes::Writer::WriteToFile(const base::FilePath& path) { + std::string json; + if (!base::JSONWriter::Write(&file_list_, &json)) + return false; + int written = base::WriteFile(path, json.data(), json.size()); + if (static_cast(written) != json.size()) { + LOG(ERROR) << "Error writing " << path.MaybeAsASCII() + << " ; write result:" << written << " expected:" << json.size(); + return false; + } + return true; +} + +} // namespace extensions diff --git a/extensions/browser/computed_hashes.h b/extensions/browser/computed_hashes.h new file mode 100644 index 000000000000..c68175c8d2c8 --- /dev/null +++ b/extensions/browser/computed_hashes.h @@ -0,0 +1,64 @@ +// Copyright 2014 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. + +#ifndef EXTENSIONS_BROWSER_COMPUTED_HASHES_H_ +#define EXTENSIONS_BROWSER_COMPUTED_HASHES_H_ + +#include +#include +#include + +#include "base/values.h" + +namespace base { +class FilePath; +} + +namespace extensions { + +// A pair of classes for serialization of a set of SHA256 block hashes computed +// over the files inside an extension. +class ComputedHashes { + public: + class Reader { + public: + Reader(); + ~Reader(); + bool InitFromFile(const base::FilePath& path); + + // The block size and hashes for |relative_path| will be copied into the + // out parameters. + bool GetHashes(const base::FilePath& relative_path, + int* block_size, + std::vector* hashes); + + private: + typedef std::pair > HashInfo; + + // This maps a relative path to a pair of (block size, hashes) + std::map data_; + }; + + class Writer { + public: + Writer(); + ~Writer(); + + // Adds hashes for |relative_path|. Should not be called more than once + // for a given |relative_path|. + void AddHashes(const base::FilePath& relative_path, + int block_size, + const std::vector& hashes); + + bool WriteToFile(const base::FilePath& path); + + private: + // The top-level object that will be serialized as JSON. + base::ListValue file_list_; + }; +}; + +} // namespace extensions + +#endif // EXTENSIONS_BROWSER_COMPUTED_HASHES_H_ diff --git a/extensions/browser/content_hash_fetcher.cc b/extensions/browser/content_hash_fetcher.cc index b40d16cc4f6b..fb2510be96d4 100644 --- a/extensions/browser/content_hash_fetcher.cc +++ b/extensions/browser/content_hash_fetcher.cc @@ -4,16 +4,367 @@ #include "extensions/browser/content_hash_fetcher.h" +#include + +#include "base/base64.h" +#include "base/file_util.h" +#include "base/files/file_enumerator.h" +#include "base/json/json_reader.h" +#include "base/memory/ref_counted.h" +#include "base/stl_util.h" +#include "base/synchronization/lock.h" +#include "base/task_runner_util.h" +#include "base/version.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_thread.h" +#include "crypto/secure_hash.h" +#include "crypto/sha2.h" +#include "extensions/browser/computed_hashes.h" #include "extensions/browser/extension_registry.h" +#include "extensions/common/constants.h" +#include "extensions/common/extension.h" +#include "extensions/common/file_util.h" +#include "net/base/load_flags.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_request_status.h" + +namespace { + +typedef std::set SortedFilePathSet; + +} // namespace namespace extensions { +// This class takes care of doing the disk and network I/O work to ensure we +// have both verified_contents.json files from the webstore and +// computed_hashes.json files computed over the files in an extension's +// directory. +class ContentHashFetcherJob + : public base::RefCountedThreadSafe, + public net::URLFetcherDelegate { + public: + typedef base::Callback CompletionCallback; + ContentHashFetcherJob(net::URLRequestContextGetter* request_context, + const std::string& extension_id, + const base::FilePath& extension_path, + const GURL& fetch_url, + const CompletionCallback& callback); + + void Start(); + + // Cancels this job, which will attempt to stop I/O operations sooner than + // just waiting for the entire job to complete. Safe to call from any thread. + void Cancel(); + + // Returns whether this job was completely successful (we have both verified + // contents and computed hashes). + bool success() { return success_; } + + // Do we have a verified_contents.json file? + bool have_verified_contents() { return have_verified_contents_; } + + private: + friend class base::RefCountedThreadSafe; + virtual ~ContentHashFetcherJob(); + + // Checks whether this job has been cancelled. Safe to call from any thread. + bool IsCancelled(); + + // Callback for when we're done doing file I/O to see if we already have + // a verified contents file. If we don't, this will kick off a network + // request to get one. + void DoneCheckingForVerifiedContents(bool found); + + // URLFetcherDelegate interface + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + // Callback for when we're done ensuring we have verified contents, and are + // ready to move on to MaybeCreateHashes. + void DoneFetchingVerifiedContents(bool success); + + // Callback for the job to write the verified contents to the filesystem. + void OnVerifiedContentsWritten(size_t expected_size, int write_result); + + // The verified contents file from the webstore only contains the treehash + // root hash, but for performance we want to cache the individual block level + // hashes. This function will create that cache with block-level hashes for + // each file in the extension if needed (the treehash root hash for each of + // these should equal what is in the verified contents file from the + // webstore). + void MaybeCreateHashes(); + + // Computes hashes for all files in |extension_path_|, and uses a + // ComputedHashes::Writer to write that information into + // |hashes_file|. Returns true on success. + bool CreateHashes(const base::FilePath& hashes_file); + + // Will call the callback, if we haven't been cancelled. + void DispatchCallback(); + + net::URLRequestContextGetter* request_context_; + std::string extension_id_; + base::FilePath extension_path_; + + // The url we'll need to use to fetch a verified_contents.json file. + GURL fetch_url_; + + CompletionCallback callback_; + content::BrowserThread::ID creation_thread_; + + // Used for fetching content signatures. + scoped_ptr url_fetcher_; + + // Whether this job succeeded. + bool success_; + + // Whether we either found a verified contents file, or were successful in + // fetching one and saving it to disk. + bool have_verified_contents_; + + // The block size to use for hashing. + int block_size_; + + // Note: this may be accessed from multiple threads, so all access should + // be protected by |cancelled_lock_|. + bool cancelled_; + + // A lock for synchronizing access to |cancelled_|. + base::Lock cancelled_lock_; +}; + +ContentHashFetcherJob::ContentHashFetcherJob( + net::URLRequestContextGetter* request_context, + const std::string& extension_id, + const base::FilePath& extension_path, + const GURL& fetch_url, + const CompletionCallback& callback) + : request_context_(request_context), + extension_id_(extension_id), + extension_path_(extension_path), + fetch_url_(fetch_url), + callback_(callback), + success_(false), + have_verified_contents_(false), + // TODO(asargent) - use the value from verified_contents.json for each + // file, instead of using a constant. + block_size_(4096), + cancelled_(false) { + DCHECK(content::BrowserThread::GetCurrentThreadIdentifier(&creation_thread_)); +} + +void ContentHashFetcherJob::Start() { + base::FilePath verified_contents_path = + file_util::GetVerifiedContentsPath(extension_path_); + base::PostTaskAndReplyWithResult( + content::BrowserThread::GetBlockingPool(), + FROM_HERE, + base::Bind(&base::PathExists, verified_contents_path), + base::Bind(&ContentHashFetcherJob::DoneCheckingForVerifiedContents, + this)); +} + +void ContentHashFetcherJob::Cancel() { + base::AutoLock autolock(cancelled_lock_); + cancelled_ = true; +} + +ContentHashFetcherJob::~ContentHashFetcherJob() { +} + +bool ContentHashFetcherJob::IsCancelled() { + base::AutoLock autolock(cancelled_lock_); + bool result = cancelled_; + return result; +} + +void ContentHashFetcherJob::DoneCheckingForVerifiedContents(bool found) { + if (IsCancelled()) + return; + if (found) { + DoneFetchingVerifiedContents(true); + } else { + url_fetcher_.reset( + net::URLFetcher::Create(fetch_url_, net::URLFetcher::GET, this)); + url_fetcher_->SetRequestContext(request_context_); + url_fetcher_->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES | + net::LOAD_DISABLE_CACHE); + url_fetcher_->SetAutomaticallyRetryOnNetworkChanges(3); + url_fetcher_->Start(); + } +} + +// Helper function to let us pass ownership of a string via base::Bind with the +// contents to be written into a file. Also ensures that the directory for +// |path| exists, creating it if needed. +static int WriteFileHelper(const base::FilePath& path, + scoped_ptr content) { + base::FilePath dir = path.DirName(); + return (base::CreateDirectoryAndGetError(dir, NULL) && + base::WriteFile(path, content->data(), content->size())); +} + +void ContentHashFetcherJob::OnURLFetchComplete(const net::URLFetcher* source) { + if (IsCancelled()) + return; + scoped_ptr response(new std::string); + if (!url_fetcher_->GetStatus().is_success() || + !url_fetcher_->GetResponseAsString(response.get())) { + DoneFetchingVerifiedContents(false); + return; + } + + // Parse the response to make sure it is valid json (on staging sometimes it + // can be a login redirect html, xml file, etc. if you aren't logged in with + // the right cookies). TODO(asargent) - It would be a nice enhancement to + // move to parsing this in a sandboxed helper (crbug.com/372878). + scoped_ptr parsed(base::JSONReader::Read(*response)); + if (parsed) { + parsed.reset(); // no longer needed + base::FilePath destination = + file_util::GetVerifiedContentsPath(extension_path_); + size_t size = response->size(); + base::PostTaskAndReplyWithResult( + content::BrowserThread::GetBlockingPool(), + FROM_HERE, + base::Bind(&WriteFileHelper, destination, base::Passed(&response)), + base::Bind( + &ContentHashFetcherJob::OnVerifiedContentsWritten, this, size)); + } else { + DoneFetchingVerifiedContents(false); + } +} + +void ContentHashFetcherJob::OnVerifiedContentsWritten(size_t expected_size, + int write_result) { + bool success = + (write_result >= 0 && static_cast(write_result) == expected_size); + DoneFetchingVerifiedContents(success); +} + +void ContentHashFetcherJob::DoneFetchingVerifiedContents(bool success) { + have_verified_contents_ = success; + + if (IsCancelled()) + return; + + // TODO(asargent) - eventually we should abort here on !success, but for + // testing purposes it's actually still helpful to continue on to create the + // computed hashes. + + content::BrowserThread::PostBlockingPoolSequencedTask( + "ContentHashFetcher", + FROM_HERE, + base::Bind(&ContentHashFetcherJob::MaybeCreateHashes, this)); +} + +void ContentHashFetcherJob::MaybeCreateHashes() { + if (IsCancelled()) + return; + base::FilePath hashes_file = + file_util::GetComputedHashesPath(extension_path_); + + if (base::PathExists(hashes_file)) + success_ = true; + else + success_ = CreateHashes(hashes_file); + + content::BrowserThread::PostTask( + creation_thread_, + FROM_HERE, + base::Bind(&ContentHashFetcherJob::DispatchCallback, this)); +} + +bool ContentHashFetcherJob::CreateHashes(const base::FilePath& hashes_file) { + if (IsCancelled()) + return false; + // Make sure the directory exists. + if (!base::CreateDirectoryAndGetError(hashes_file.DirName(), NULL)) + return false; + + base::FileEnumerator enumerator(extension_path_, + true, /* recursive */ + base::FileEnumerator::FILES); + // First discover all the file paths and put them in a sorted set. + SortedFilePathSet paths; + for (;;) { + if (IsCancelled()) + return false; + + base::FilePath full_path = enumerator.Next(); + if (full_path.empty()) + break; + paths.insert(full_path); + } + + // Now iterate over all the paths in sorted order and compute the block hashes + // for each one. + ComputedHashes::Writer writer; + for (SortedFilePathSet::iterator i = paths.begin(); i != paths.end(); ++i) { + if (IsCancelled()) + return false; + const base::FilePath& full_path = *i; + base::FilePath relative_path; + extension_path_.AppendRelativePath(full_path, &relative_path); + std::string contents; + if (!base::ReadFileToString(full_path, &contents)) { + LOG(ERROR) << "Could not read " << full_path.MaybeAsASCII(); + continue; + } + + // Iterate through taking the hash of each block of size (block_size_) of + // the file. + std::vector hashes; + size_t offset = 0; + while (offset < contents.size()) { + if (IsCancelled()) + return false; + const char* block_start = contents.data() + offset; + size_t bytes_to_read = + std::min(contents.size() - offset, static_cast(block_size_)); + DCHECK(bytes_to_read > 0); + scoped_ptr hash( + crypto::SecureHash::Create(crypto::SecureHash::SHA256)); + hash->Update(block_start, bytes_to_read); + + hashes.push_back(std::string()); + std::string* buffer = &hashes.back(); + buffer->resize(crypto::kSHA256Length); + hash->Finish(string_as_array(buffer), buffer->size()); + + // Get ready for next iteration. + offset += bytes_to_read; + } + writer.AddHashes(relative_path, block_size_, hashes); + } + return writer.WriteToFile(hashes_file); +} + +void ContentHashFetcherJob::DispatchCallback() { + { + base::AutoLock autolock(cancelled_lock_); + if (cancelled_) + return; + } + callback_.Run(this); +} + +// ---- + ContentHashFetcher::ContentHashFetcher(content::BrowserContext* context, ContentVerifierDelegate* delegate) - : context_(context), delegate_(delegate), observer_(this) { + : context_(context), + delegate_(delegate), + observer_(this), + weak_ptr_factory_(this) { } ContentHashFetcher::~ContentHashFetcher() { + for (JobMap::iterator i = jobs_.begin(); i != jobs_.end(); ++i) { + i->second->Cancel(); + } } void ContentHashFetcher::Start() { @@ -22,17 +373,57 @@ void ContentHashFetcher::Start() { } void ContentHashFetcher::DoFetch(const Extension* extension) { + if (!extension || !delegate_->ShouldBeVerified(*extension)) + return; + + IdAndVersion key(extension->id(), extension->version()->GetString()); + if (ContainsKey(jobs_, key)) + return; + + // TODO(asargent) - we should do something here to remember recent attempts + // to fetch signatures by extension id, and use exponential backoff to avoid + // hammering the server when we aren't successful in getting them. + // crbug.com/373397 + + DCHECK(extension->version()); + GURL url = + delegate_->GetSignatureFetchUrl(extension->id(), *extension->version()); + ContentHashFetcherJob* job = + new ContentHashFetcherJob(context_->GetRequestContext(), + extension->id(), + extension->path(), + url, + base::Bind(&ContentHashFetcher::JobFinished, + weak_ptr_factory_.GetWeakPtr())); + jobs_.insert(std::make_pair(key, job)); + job->Start(); } void ContentHashFetcher::OnExtensionLoaded( content::BrowserContext* browser_context, const Extension* extension) { + CHECK(extension); + DoFetch(extension); } void ContentHashFetcher::OnExtensionUnloaded( content::BrowserContext* browser_context, const Extension* extension, UnloadedExtensionInfo::Reason reason) { + CHECK(extension); + IdAndVersion key(extension->id(), extension->version()->GetString()); + JobMap::iterator found = jobs_.find(key); + if (found != jobs_.end()) + jobs_.erase(found); +} + +void ContentHashFetcher::JobFinished(ContentHashFetcherJob* job) { + for (JobMap::iterator i = jobs_.begin(); i != jobs_.end(); ++i) { + if (i->second.get() == job) { + jobs_.erase(i); + break; + } + } } } // namespace extensions diff --git a/extensions/browser/content_hash_fetcher.h b/extensions/browser/content_hash_fetcher.h index 60d746f4b29b..d437d8f10949 100644 --- a/extensions/browser/content_hash_fetcher.h +++ b/extensions/browser/content_hash_fetcher.h @@ -5,6 +5,7 @@ #ifndef EXTENSIONS_BROWSER_CONTENT_HASH_FETCHER_H_ #define EXTENSIONS_BROWSER_CONTENT_HASH_FETCHER_H_ +#include "base/memory/weak_ptr.h" #include "base/scoped_observer.h" #include "extensions/browser/content_verifier_delegate.h" #include "extensions/browser/extension_registry_observer.h" @@ -17,6 +18,7 @@ class BrowserContext; namespace extensions { class ExtensionRegistry; +class ContentHashFetcherJob; // This class is responsible for getting signed expected hashes for use in // extension content verification. As extensions are loaded it will fetch and @@ -48,12 +50,25 @@ class ContentHashFetcher : public ExtensionRegistryObserver { UnloadedExtensionInfo::Reason reason) OVERRIDE; private: + // Callback for when a job getting content hashes has completed. + void JobFinished(ContentHashFetcherJob* job); + content::BrowserContext* context_; ContentVerifierDelegate* delegate_; + // We keep around pointers to in-progress jobs, both so we can avoid + // scheduling duplicate work if fetching is already in progress, and so that + // we can cancel in-progress work at shutdown time. + typedef std::pair IdAndVersion; + typedef std::map > JobMap; + JobMap jobs_; + // For observing the ExtensionRegistry. ScopedObserver observer_; + // Used for binding callbacks passed to jobs. + base::WeakPtrFactory weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(ContentHashFetcher); }; diff --git a/extensions/browser/content_hash_reader.cc b/extensions/browser/content_hash_reader.cc index 9a327acd7b23..cc923ad92427 100644 --- a/extensions/browser/content_hash_reader.cc +++ b/extensions/browser/content_hash_reader.cc @@ -4,6 +4,22 @@ #include "extensions/browser/content_hash_reader.h" +#include "base/base64.h" +#include "base/file_util.h" +#include "base/json/json_reader.h" +#include "base/strings/string_util.h" +#include "base/values.h" +#include "crypto/sha2.h" +#include "extensions/browser/computed_hashes.h" +#include "extensions/browser/content_hash_tree.h" +#include "extensions/browser/verified_contents.h" +#include "extensions/common/extension.h" +#include "extensions/common/file_util.h" + +using base::DictionaryValue; +using base::ListValue; +using base::Value; + namespace extensions { ContentHashReader::ContentHashReader(const std::string& extension_id, @@ -15,27 +31,77 @@ ContentHashReader::ContentHashReader(const std::string& extension_id, extension_version_(extension_version.GetString()), extension_root_(extension_root), relative_path_(relative_path), - key_(key) { + key_(key), + status_(NOT_INITIALIZED), + block_size_(0) { } ContentHashReader::~ContentHashReader() { } bool ContentHashReader::Init() { + DCHECK_EQ(status_, NOT_INITIALIZED); + status_ = FAILURE; + base::FilePath verified_contents_path = + file_util::GetVerifiedContentsPath(extension_root_); + + if (base::PathExists(verified_contents_path)) { + verified_contents_.reset(new VerifiedContents(key_.data, key_.size)); + // TODO(asargent) - we need to switch on signature validation. + // (crbug.com/369895) + if (!verified_contents_->InitFrom(verified_contents_path, + true /* ignore invalid signature */)) { + verified_contents_.reset(); + } else { + if (!verified_contents_->valid_signature()) { + // Reminder to fix the TODO above. + LOG(WARNING) << "Temporarily ignoring invalid signature!"; + } + if (verified_contents_->extension_id() != extension_id_ || + !verified_contents_->version().Equals(extension_version_)) + return false; + } + } + + ComputedHashes::Reader reader; + if (!reader.InitFromFile(file_util::GetComputedHashesPath(extension_root_)) || + !reader.GetHashes(relative_path_, &block_size_, &hashes_) || + block_size_ % crypto::kSHA256Length != 0) + return false; + + std::string root = + ComputeTreeHashRoot(hashes_, block_size_ / crypto::kSHA256Length); + const std::string* expected_root = NULL; + if (verified_contents_.get()) + expected_root = verified_contents_->GetTreeHashRoot(relative_path_); + if (expected_root && *expected_root != root) + return false; + + status_ = SUCCESS; return true; } int ContentHashReader::block_count() const { - return 0; + DCHECK(status_ != NOT_INITIALIZED); + return hashes_.size(); } int ContentHashReader::block_size() const { - return 0; + DCHECK(status_ != NOT_INITIALIZED); + return block_size_; } bool ContentHashReader::GetHashForBlock(int block_index, const std::string** result) const { - return false; + if (status_ != SUCCESS) + return false; + DCHECK(block_index >= 0); + + if (static_cast(block_index) >= hashes_.size()) + return false; + *result = &hashes_[block_index]; + + return true; } } // namespace extensions diff --git a/extensions/browser/content_hash_reader.h b/extensions/browser/content_hash_reader.h index 7d11a38bf459..b68378c487c6 100644 --- a/extensions/browser/content_hash_reader.h +++ b/extensions/browser/content_hash_reader.h @@ -6,6 +6,7 @@ #define EXTENSIONS_BROWSER_CONTENT_HASH_READER_H_ #include +#include #include "base/files/file_path.h" #include "base/memory/ref_counted.h" @@ -15,6 +16,8 @@ namespace extensions { +class VerifiedContents; + // This class creates an object that will read expected hashes that may have // been fetched/calculated by the ContentHashFetcher, and vends them out for // use in ContentVerifyJob's. @@ -50,7 +53,7 @@ class ContentHashReader : public base::RefCountedThreadSafe { friend class base::RefCountedThreadSafe; virtual ~ContentHashReader(); - bool ReadHashes(const base::FilePath& hashes_file); + enum InitStatus { NOT_INITIALIZED, SUCCESS, FAILURE }; std::string extension_id_; base::Version extension_version_; @@ -58,6 +61,15 @@ class ContentHashReader : public base::RefCountedThreadSafe { base::FilePath relative_path_; ContentVerifierKey key_; + InitStatus status_; + + // The blocksize used for generating the hashes. + int block_size_; + + scoped_ptr verified_contents_; + + std::vector hashes_; + DISALLOW_COPY_AND_ASSIGN(ContentHashReader); }; diff --git a/extensions/browser/content_verifier.cc b/extensions/browser/content_verifier.cc index 548be16fb7a6..1e164ea43390 100644 --- a/extensions/browser/content_verifier.cc +++ b/extensions/browser/content_verifier.cc @@ -10,10 +10,13 @@ #include "base/files/file_path.h" #include "base/metrics/field_trial.h" #include "content/public/browser/browser_thread.h" +#include "content/public/common/content_switches.h" #include "extensions/browser/content_hash_fetcher.h" #include "extensions/browser/content_hash_reader.h" #include "extensions/browser/content_verifier_delegate.h" #include "extensions/browser/extension_registry.h" +#include "extensions/common/constants.h" +#include "extensions/common/extension_l10n_util.h" #include "extensions/common/switches.h" namespace { @@ -49,17 +52,42 @@ ContentVerifyJob* ContentVerifier::CreateJobFor( const std::string& extension_id, const base::FilePath& extension_root, const base::FilePath& relative_path) { - if (!delegate_) + if (mode_ < BOOTSTRAP || !delegate_) return NULL; ExtensionRegistry* registry = ExtensionRegistry::Get(context_); const Extension* extension = registry->GetExtensionById(extension_id, ExtensionRegistry::EVERYTHING); - if (!extension || !delegate_->ShouldBeVerified(*extension) || - !extension->version()) + if (!extension || !extension->version() || + !delegate_->ShouldBeVerified(*extension)) return NULL; + // Images used in the browser get transcoded during install, so skip checking + // them for now. TODO(asargent) - see if we can cache this list for a given + // extension id/version pair. + std::set browser_images = + delegate_->GetBrowserImagePaths(extension); + if (ContainsKey(browser_images, relative_path)) + return NULL; + + base::FilePath locales_dir = extension_root.Append(kLocaleFolder); + base::FilePath full_path = extension_root.Append(relative_path); + if (locales_dir.IsParent(full_path)) { + // TODO(asargent) - see if we can cache this list to avoid having to fetch + // it every time. Maybe it can never change at runtime? (Or if it can, + // maybe there is an event we can listen for to know to drop our cache). + std::set all_locales; + extension_l10n_util::GetAllLocales(&all_locales); + // Since message catalogs get transcoded during installation, we want to + // ignore only those paths that the localization transcoding *did* ignore. + if (!extension_l10n_util::ShouldSkipValidation( + locales_dir, full_path, all_locales)) + return NULL; + } + + // TODO(asargent) - we can probably get some good performance wins by having + // a cache of ContentHashReader's that we hold onto past the end of each job. return new ContentVerifyJob( new ContentHashReader(extension_id, *extension->version(), @@ -98,6 +126,8 @@ void ContentVerifier::VerifyFailed(const std::string& extension_id, // static ContentVerifier::Mode ContentVerifier::GetMode() { + base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); + Mode experiment_value = NONE; const std::string group = base::FieldTrialList::FindFullName(kExperimentName); if (group == "EnforceStrict") @@ -107,8 +137,20 @@ ContentVerifier::Mode ContentVerifier::GetMode() { else if (group == "Bootstrap") experiment_value = BOOTSTRAP; + // The field trial value that normally comes from the server can be + // overridden on the command line, which we don't want to allow since malware + // can set chrome command line flags. There isn't currently a way to find out + // what the server-provided value is in this case, so we conservatively + // default to the strictest mode if we detect our experiment name being + // overridden. + if (command_line->HasSwitch(::switches::kForceFieldTrials)) { + std::string forced_trials = + command_line->GetSwitchValueASCII(::switches::kForceFieldTrials); + if (forced_trials.find(kExperimentName) != std::string::npos) + experiment_value = ENFORCE_STRICT; + } + Mode cmdline_value = NONE; - base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); if (command_line->HasSwitch(switches::kExtensionContentVerification)) { std::string switch_value = command_line->GetSwitchValueASCII( switches::kExtensionContentVerification); diff --git a/extensions/browser/content_verifier_delegate.h b/extensions/browser/content_verifier_delegate.h index c702a2808cf3..a6b909f43f40 100644 --- a/extensions/browser/content_verifier_delegate.h +++ b/extensions/browser/content_verifier_delegate.h @@ -5,9 +5,12 @@ #ifndef EXTENSIONS_BROWSER_CONTENT_VERIFIER_DELEGATE_H_ #define EXTENSIONS_BROWSER_CONTENT_VERIFIER_DELEGATE_H_ +#include + #include "url/gurl.h" namespace base { +class FilePath; class Version; } @@ -46,6 +49,11 @@ class ContentVerifierDelegate { virtual GURL GetSignatureFetchUrl(const std::string& extension_id, const base::Version& version) = 0; + // This should return the set of file paths for images used within the + // browser process. (These may get transcoded during the install process). + virtual std::set GetBrowserImagePaths( + const extensions::Extension* extension) = 0; + // Called when the content verifier detects that a read of a file inside // an extension did not match its expected hash. virtual void VerifyFailed(const std::string& extension_id) = 0; diff --git a/extensions/common/constants.cc b/extensions/common/constants.cc index c55c4e1e772f..bd8e157188ac 100644 --- a/extensions/common/constants.cc +++ b/extensions/common/constants.cc @@ -19,6 +19,12 @@ const base::FilePath::CharType kMessagesFilename[] = FILE_PATH_LITERAL("messages.json"); const base::FilePath::CharType kPlatformSpecificFolder[] = FILE_PATH_LITERAL("_platform_specific"); +const base::FilePath::CharType kMetadataFolder[] = + FILE_PATH_LITERAL("_metadata"); +const base::FilePath::CharType kVerifiedContentsFilename[] = + FILE_PATH_LITERAL("verified_contents.json"); +const base::FilePath::CharType kComputedHashesFilename[] = + FILE_PATH_LITERAL("computed_hashes.json"); const char kInstallDirectoryName[] = "Extensions"; diff --git a/extensions/common/constants.h b/extensions/common/constants.h index 20bf421cd654..66fe2ba4d1e6 100644 --- a/extensions/common/constants.h +++ b/extensions/common/constants.h @@ -28,6 +28,16 @@ extern const base::FilePath::CharType kMessagesFilename[]; // The base directory for subdirectories with platform-specific code. extern const base::FilePath::CharType kPlatformSpecificFolder[]; +// A directory reserved for metadata, generated either by the webstore +// or chrome. +extern const base::FilePath::CharType kMetadataFolder[]; + +// Name of the verified contents file within the metadata folder. +extern const base::FilePath::CharType kVerifiedContentsFilename[]; + +// Name of the computed hashes file within the metadata folder. +extern const base::FilePath::CharType kComputedHashesFilename[]; + // The name of the directory inside the profile where extensions are // installed to. extern const char kInstallDirectoryName[]; diff --git a/extensions/common/file_util.cc b/extensions/common/file_util.cc index fcf704bfc72f..0a116ad32957 100644 --- a/extensions/common/file_util.cc +++ b/extensions/common/file_util.cc @@ -437,5 +437,13 @@ std::map* LoadMessageBundleSubstitutionMap( return return_value; } +base::FilePath GetVerifiedContentsPath(const base::FilePath& extension_path) { + return extension_path.Append(kMetadataFolder) + .Append(kVerifiedContentsFilename); +} +base::FilePath GetComputedHashesPath(const base::FilePath& extension_path) { + return extension_path.Append(kMetadataFolder).Append(kComputedHashesFilename); +} + } // namespace file_util } // namespace extensions diff --git a/extensions/common/file_util.h b/extensions/common/file_util.h index 597d085bef0a..32c370ee96bd 100644 --- a/extensions/common/file_util.h +++ b/extensions/common/file_util.h @@ -122,6 +122,10 @@ std::map* LoadMessageBundleSubstitutionMap( const std::string& extension_id, const std::string& default_locale); +// Helper functions for getting paths for files used in content verification. +base::FilePath GetVerifiedContentsPath(const base::FilePath& extension_path); +base::FilePath GetComputedHashesPath(const base::FilePath& extension_path); + } // namespace file_util } // namespace extensions diff --git a/extensions/extensions.gyp b/extensions/extensions.gyp index 0ca834015d13..7233abb70fa1 100644 --- a/extensions/extensions.gyp +++ b/extensions/extensions.gyp @@ -312,6 +312,8 @@ 'browser/browser_context_keyed_api_factory.h', 'browser/browser_context_keyed_service_factories.cc', 'browser/browser_context_keyed_service_factories.h', + 'browser/computed_hashes.cc', + 'browser/computed_hashes.h', 'browser/content_hash_fetcher.cc', 'browser/content_hash_fetcher.h', 'browser/content_hash_reader.cc', -- 2.11.4.GIT