summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorasargent@chromium.org <asargent@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-05-15 17:00:52 +0000
committerasargent@chromium.org <asargent@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-05-15 17:00:52 +0000
commit620f3e50bdbb11cf69dd239b5a976fcdf0d2dade (patch)
treebba45635583c93f799d82b4750a6210fcd5a72fe
parent8e3a653ada2c5c2fc78d7d9b9ef48f07a66beb71 (diff)
downloadchromium_src-620f3e50bdbb11cf69dd239b5a976fcdf0d2dade.zip
chromium_src-620f3e50bdbb11cf69dd239b5a976fcdf0d2dade.tar.gz
chromium_src-620f3e50bdbb11cf69dd239b5a976fcdf0d2dade.tar.bz2
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
-rw-r--r--chrome/browser/extensions/extension_system_impl.cc42
-rw-r--r--chrome/common/extensions/extension_constants.cc6
-rw-r--r--chrome/common/extensions/extension_constants.h7
-rw-r--r--extensions/browser/computed_hashes.cc129
-rw-r--r--extensions/browser/computed_hashes.h64
-rw-r--r--extensions/browser/content_hash_fetcher.cc393
-rw-r--r--extensions/browser/content_hash_fetcher.h15
-rw-r--r--extensions/browser/content_hash_reader.cc74
-rw-r--r--extensions/browser/content_hash_reader.h14
-rw-r--r--extensions/browser/content_verifier.cc50
-rw-r--r--extensions/browser/content_verifier_delegate.h8
-rw-r--r--extensions/common/constants.cc6
-rw-r--r--extensions/common/constants.h10
-rw-r--r--extensions/common/file_util.cc8
-rw-r--r--extensions/common/file_util.h4
-rw-r--r--extensions/extensions.gyp2
16 files changed, 816 insertions, 16 deletions
diff --git a/chrome/browser/extensions/extension_system_impl.cc b/chrome/browser/extensions/extension_system_impl.cc
index dab0094..3e8d350 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<std::string> 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<base::FilePath> 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 66bded2..4d0956bd 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 936330f..f202165 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 0000000..4a8a852
--- /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<base::Value> 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::string>());
+ std::vector<std::string>* 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<std::string>* hashes) {
+ std::map<base::FilePath, HashInfo>::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<std::string>& 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<std::string>::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<unsigned>(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 0000000..c68175c
--- /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 <map>
+#include <string>
+#include <vector>
+
+#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<std::string>* hashes);
+
+ private:
+ typedef std::pair<int, std::vector<std::string> > HashInfo;
+
+ // This maps a relative path to a pair of (block size, hashes)
+ std::map<base::FilePath, HashInfo> 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<std::string>& 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 b40d16c..fb2510b 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 <algorithm>
+
+#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<base::FilePath> 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<ContentHashFetcherJob>,
+ public net::URLFetcherDelegate {
+ public:
+ typedef base::Callback<void(ContentHashFetcherJob*)> 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<ContentHashFetcherJob>;
+ 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<net::URLFetcher> 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<std::string> 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<std::string> 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<base::Value> 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<size_t>(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<std::string> 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<size_t>(block_size_));
+ DCHECK(bytes_to_read > 0);
+ scoped_ptr<crypto::SecureHash> 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 60d746f..d437d8f 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<ExtensionId, std::string> IdAndVersion;
+ typedef std::map<IdAndVersion, scoped_refptr<ContentHashFetcherJob> > JobMap;
+ JobMap jobs_;
+
// For observing the ExtensionRegistry.
ScopedObserver<ExtensionRegistry, ExtensionRegistryObserver> observer_;
+ // Used for binding callbacks passed to jobs.
+ base::WeakPtrFactory<ContentHashFetcher> 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 9a327ac..cc923ad 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<unsigned>(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 7d11a38..b68378c 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 <string>
+#include <vector>
#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<ContentHashReader> {
friend class base::RefCountedThreadSafe<ContentHashReader>;
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<ContentHashReader> {
base::FilePath relative_path_;
ContentVerifierKey key_;
+ InitStatus status_;
+
+ // The blocksize used for generating the hashes.
+ int block_size_;
+
+ scoped_ptr<VerifiedContents> verified_contents_;
+
+ std::vector<std::string> hashes_;
+
DISALLOW_COPY_AND_ASSIGN(ContentHashReader);
};
diff --git a/extensions/browser/content_verifier.cc b/extensions/browser/content_verifier.cc
index 548be16..1e164ea 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<base::FilePath> 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<std::string> 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 c702a28..a6b909f 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 <set>
+
#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<base::FilePath> 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 c55c4e1..bd8e157 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 20bf421..66fe2ba 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 fcf704b..0a116ad 100644
--- a/extensions/common/file_util.cc
+++ b/extensions/common/file_util.cc
@@ -437,5 +437,13 @@ std::map<std::string, std::string>* 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 597d085..32c370e 100644
--- a/extensions/common/file_util.h
+++ b/extensions/common/file_util.h
@@ -122,6 +122,10 @@ std::map<std::string, std::string>* 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 0ca8340..7233abb 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',