Commit 39d15481 authored by Oleg Davydov's avatar Oleg Davydov Committed by Commit Bot

Check hashes for extensions without verified contents

Extensions outside Chrome Web Store (self-hosted ones) don't have
hashes signed by Chrome Web Store (stored in verified_contents.json
file), so historically are not checked.

This commit adds support for extensions to be "should check, but
shouldn't verify": if such an extension has computed_hashes.json file,
we'll use it to check extension's content and report appropriately to
delegate.

Bug: 958794
Change-Id: Id8bce11fa42e5a8099f65d3013cb25f2271f51c2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1735454
Commit-Queue: Oleg Davydov <burunduk@chromium.org>
Reviewed-by: default avatarNikita Podguzov <nikitapodguzov@chromium.org>
Reviewed-by: default avatarIstiaque Ahmed <lazyboy@chromium.org>
Cr-Commit-Position: refs/heads/master@{#712724}
parent 73b433f1
...@@ -82,6 +82,11 @@ bool HasPageFileExt(const base::FilePath& requested_path) { ...@@ -82,6 +82,11 @@ bool HasPageFileExt(const base::FilePath& requested_path) {
std::unique_ptr<ContentVerifierIOData::ExtensionData> CreateIOData( std::unique_ptr<ContentVerifierIOData::ExtensionData> CreateIOData(
const Extension* extension, const Extension* extension,
ContentVerifierDelegate* delegate) { ContentVerifierDelegate* delegate) {
ContentVerifierDelegate::VerifierSourceType source_type =
delegate->GetVerifierSourceType(*extension);
if (source_type == ContentVerifierDelegate::VerifierSourceType::NONE)
return nullptr;
// The browser image paths from the extension may not be relative (eg // The browser image paths from the extension may not be relative (eg
// they might have leading '/' or './'), so we strip those to make // they might have leading '/' or './'), so we strip those to make
// comparing to actual relative paths work later on. // comparing to actual relative paths work later on.
...@@ -115,7 +120,7 @@ std::unique_ptr<ContentVerifierIOData::ExtensionData> CreateIOData( ...@@ -115,7 +120,7 @@ std::unique_ptr<ContentVerifierIOData::ExtensionData> CreateIOData(
return std::make_unique<ContentVerifierIOData::ExtensionData>( return std::make_unique<ContentVerifierIOData::ExtensionData>(
std::move(image_paths), std::move(background_or_content_paths), std::move(image_paths), std::move(background_or_content_paths),
extension->version()); extension->version(), source_type);
} }
} // namespace } // namespace
...@@ -180,6 +185,7 @@ class ContentVerifier::HashHelper { ...@@ -180,6 +185,7 @@ class ContentVerifier::HashHelper {
// Must be called on IO thread. The method responds through |callback| on IO // Must be called on IO thread. The method responds through |callback| on IO
// thread. // thread.
void GetContentHash(ContentHash::FetchKey fetch_key, void GetContentHash(ContentHash::FetchKey fetch_key,
ContentVerifierDelegate::VerifierSourceType source_type,
bool force_missing_computed_hashes_creation, bool force_missing_computed_hashes_creation,
ContentHashCallback callback) { ContentHashCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO); DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
...@@ -204,6 +210,7 @@ class ContentVerifier::HashHelper { ...@@ -204,6 +210,7 @@ class ContentVerifier::HashHelper {
FROM_HERE, FROM_HERE,
base::BindOnce( base::BindOnce(
&HashHelper::ReadHashOnFileTaskRunner, std::move(fetch_key), &HashHelper::ReadHashOnFileTaskRunner, std::move(fetch_key),
source_type,
base::BindRepeating(&IsCancelledChecker::IsCancelled, checker), base::BindRepeating(&IsCancelledChecker::IsCancelled, checker),
base::BindOnce(&HashHelper::DidReadHash, weak_factory_.GetWeakPtr(), base::BindOnce(&HashHelper::DidReadHash, weak_factory_.GetWeakPtr(),
callback_key, checker))); callback_key, checker)));
...@@ -286,12 +293,11 @@ class ContentVerifier::HashHelper { ...@@ -286,12 +293,11 @@ class ContentVerifier::HashHelper {
static void ReadHashOnFileTaskRunner( static void ReadHashOnFileTaskRunner(
ContentHash::FetchKey fetch_key, ContentHash::FetchKey fetch_key,
ContentVerifierDelegate::VerifierSourceType source_type,
const IsCancelledCallback& is_cancelled, const IsCancelledCallback& is_cancelled,
ContentHash::CreatedCallback created_callback) { ContentHash::CreatedCallback created_callback) {
ContentHash::Create( ContentHash::Create(
std::move(fetch_key), std::move(fetch_key), source_type, is_cancelled,
ContentVerifierDelegate::VerifierSourceType::SIGNED_HASHES,
is_cancelled,
base::BindOnce(&HashHelper::ForwardToIO, std::move(created_callback))); base::BindOnce(&HashHelper::ForwardToIO, std::move(created_callback)));
} }
...@@ -483,12 +489,16 @@ void ContentVerifier::GetContentHash( ...@@ -483,12 +489,16 @@ void ContentVerifier::GetContentHash(
return; return;
} }
const ContentVerifierIOData::ExtensionData* data =
io_data_.GetData(extension_id);
DCHECK(data);
ContentHash::FetchKey fetch_key = ContentHash::FetchKey fetch_key =
GetFetchKey(extension_id, extension_root, extension_version); GetFetchKey(extension_id, extension_root, extension_version);
// Since |shutdown_on_io_| = false, GetOrCreateHashHelper() must return // Since |shutdown_on_io_| = false, GetOrCreateHashHelper() must return
// non-nullptr instance of HashHelper. // non-nullptr instance of HashHelper.
GetOrCreateHashHelper()->GetContentHash( GetOrCreateHashHelper()->GetContentHash(
std::move(fetch_key), force_missing_computed_hashes_creation, std::move(fetch_key), data->source_type,
force_missing_computed_hashes_creation,
base::BindOnce(&ContentVerifier::DidGetContentHash, this, cache_key, base::BindOnce(&ContentVerifier::DidGetContentHash, this, cache_key,
std::move(callback))); std::move(callback)));
} }
...@@ -516,13 +526,13 @@ void ContentVerifier::OnExtensionLoaded( ...@@ -516,13 +526,13 @@ void ContentVerifier::OnExtensionLoaded(
if (shutdown_on_ui_) if (shutdown_on_ui_)
return; return;
if (delegate_->GetVerifierSourceType(*extension) == std::unique_ptr<ContentVerifierIOData::ExtensionData> io_data =
ContentVerifierDelegate::VerifierSourceType::SIGNED_HASHES) { CreateIOData(extension, delegate_.get());
base::PostTask( if (io_data) {
FROM_HERE, {content::BrowserThread::IO}, base::PostTask(FROM_HERE, {content::BrowserThread::IO},
base::BindOnce(&ContentVerifier::OnExtensionLoadedOnIO, this, base::BindOnce(&ContentVerifier::OnExtensionLoadedOnIO, this,
extension->id(), extension->path(), extension->version(), extension->id(), extension->path(),
CreateIOData(extension, delegate_.get()))); extension->version(), std::move(io_data)));
} }
} }
...@@ -612,6 +622,16 @@ ContentHash::FetchKey ContentVerifier::GetFetchKey( ...@@ -612,6 +622,16 @@ ContentHash::FetchKey ContentVerifier::GetFetchKey(
const base::Version& extension_version) { const base::Version& extension_version) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO); DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
const ContentVerifierIOData::ExtensionData* data =
io_data_.GetData(extension_id);
DCHECK(data);
if (data->source_type ==
ContentVerifierDelegate::VerifierSourceType::UNSIGNED_HASHES) {
return ContentHash::FetchKey(extension_id, extension_root,
extension_version, mojo::NullRemote(),
GURL::EmptyGURL(), ContentVerifierKey());
}
// Create a new mojo pipe. It's safe to pass this around and use immediately, // Create a new mojo pipe. It's safe to pass this around and use immediately,
// even though it needs to finish initialization on the UI thread. // even though it needs to finish initialization on the UI thread.
mojo::PendingRemote<network::mojom::URLLoaderFactory> mojo::PendingRemote<network::mojom::URLLoaderFactory>
......
...@@ -150,24 +150,50 @@ class ContentHash : public base::RefCountedThreadSafe<ContentHash> { ...@@ -150,24 +150,50 @@ class ContentHash : public base::RefCountedThreadSafe<ContentHash> {
private: private:
friend class base::RefCountedThreadSafe<ContentHash>; friend class base::RefCountedThreadSafe<ContentHash>;
using GetVerifiedContentsCallback = base::OnceCallback<void(
FetchKey key,
std::unique_ptr<VerifiedContents> verified_contents,
bool did_attempt_fetch)>;
ContentHash(const ExtensionId& id, ContentHash(const ExtensionId& id,
const base::FilePath& root, const base::FilePath& root,
ContentVerifierDelegate::VerifierSourceType source_type,
std::unique_ptr<VerifiedContents> verified_contents, std::unique_ptr<VerifiedContents> verified_contents,
std::unique_ptr<ComputedHashes::Reader> computed_hashes); std::unique_ptr<ComputedHashes::Reader> computed_hashes);
~ContentHash(); ~ContentHash();
// Step 1/2: verified_contents.json.
static void GetVerifiedContents(
FetchKey key,
ContentVerifierDelegate::VerifierSourceType source_type,
const IsCancelledCallback& is_cancelled,
GetVerifiedContentsCallback);
static void FetchVerifiedContents(FetchKey key, static void FetchVerifiedContents(FetchKey key,
const IsCancelledCallback& is_cancelled, const IsCancelledCallback& is_cancelled,
CreatedCallback created_callback); GetVerifiedContentsCallback callback);
static std::unique_ptr<VerifiedContents> StoreAndRetrieveVerifiedContents(
std::unique_ptr<std::string> fetched_contents,
const FetchKey& key);
static void DidFetchVerifiedContents( static void DidFetchVerifiedContents(
CreatedCallback created_callback, GetVerifiedContentsCallback callback,
const IsCancelledCallback& is_cancelled,
FetchKey key, FetchKey key,
std::unique_ptr<std::string> fetched_contents); std::unique_ptr<std::string> fetched_contents);
static void DispatchFetchFailure(FetchKey key, // Step 2/2: computed_hashes.json.
CreatedCallback created_callback, static void GetComputedHashes(
const IsCancelledCallback& is_cancelled); ContentVerifierDelegate::VerifierSourceType source_type,
const IsCancelledCallback& is_cancelled,
CreatedCallback created_callback,
FetchKey key,
std::unique_ptr<VerifiedContents> verified_contents,
bool did_attempt_fetch);
static void DispatchFetchFailure(
const ExtensionId& extension_id,
const base::FilePath& extension_root,
ContentVerifierDelegate::VerifierSourceType source_type,
CreatedCallback created_callback,
const IsCancelledCallback& is_cancelled);
static void RecordFetchResult(bool success); static void RecordFetchResult(bool success);
...@@ -183,14 +209,24 @@ class ContentHash : public base::RefCountedThreadSafe<ContentHash> { ...@@ -183,14 +209,24 @@ class ContentHash : public base::RefCountedThreadSafe<ContentHash> {
bool CreateHashes(const base::FilePath& hashes_file, bool CreateHashes(const base::FilePath& hashes_file,
const IsCancelledCallback& is_cancelled); const IsCancelledCallback& is_cancelled);
// Builds hashes for one resource and checks them against
// verified_contents.json if needed. Returns nullopt if nothing should be
// added to computed_hashes.json for this resource.
base::Optional<std::vector<std::string>> ComputeAndCheckResourceHash(
const base::FilePath& full_path,
const base::FilePath& relative_unix_path);
// Builds computed_hashes. Possibly after creating computed_hashes.json file // Builds computed_hashes. Possibly after creating computed_hashes.json file
// if necessary. // if necessary.
void BuildComputedHashes(bool attempted_fetching_verified_contents, void BuildComputedHashes(bool did_fetch_verified_contents,
bool force_build, bool force_build,
const IsCancelledCallback& is_cancelled); const IsCancelledCallback& is_cancelled);
bool has_verified_contents() const { return verified_contents_ != nullptr; }
const ExtensionId extension_id_; const ExtensionId extension_id_;
const base::FilePath extension_root_; const base::FilePath extension_root_;
ContentVerifierDelegate::VerifierSourceType source_type_;
bool succeeded_ = false; bool succeeded_ = false;
......
...@@ -231,4 +231,41 @@ TEST_F(ContentHashUnittest, ExtensionWithSignedHashes) { ...@@ -231,4 +231,41 @@ TEST_F(ContentHashUnittest, ExtensionWithSignedHashes) {
EXPECT_TRUE(result->success); EXPECT_TRUE(result->success);
} }
TEST_F(ContentHashUnittest, ExtensionWithUnsignedHashes) {
TestExtensionBuilder builder;
builder.WriteManifest();
builder.WriteResource(FILE_PATH_LITERAL("background.js"),
"console.log('Nothing special');");
builder.WriteComputedHashes();
scoped_refptr<Extension> extension = LoadExtension(builder);
ASSERT_NE(nullptr, extension);
std::unique_ptr<ContentHashResult> result = CreateContentHash(
extension.get(),
ContentVerifierDelegate::VerifierSourceType::UNSIGNED_HASHES,
builder.GetTestContentVerifierPublicKey());
DCHECK(result);
EXPECT_TRUE(result->success);
}
TEST_F(ContentHashUnittest, ExtensionWithoutHashes) {
TestExtensionBuilder builder;
builder.WriteManifest();
builder.WriteResource(FILE_PATH_LITERAL("background.js"),
"console.log('Nothing special');");
scoped_refptr<Extension> extension = LoadExtension(builder);
ASSERT_NE(nullptr, extension);
std::unique_ptr<ContentHashResult> result = CreateContentHash(
extension.get(),
ContentVerifierDelegate::VerifierSourceType::UNSIGNED_HASHES,
builder.GetTestContentVerifierPublicKey());
DCHECK(result);
EXPECT_FALSE(result->success);
}
} // namespace extensions } // namespace extensions
...@@ -136,10 +136,11 @@ MockContentVerifierDelegate::~MockContentVerifierDelegate() = default; ...@@ -136,10 +136,11 @@ MockContentVerifierDelegate::~MockContentVerifierDelegate() = default;
ContentVerifierDelegate::VerifierSourceType ContentVerifierDelegate::VerifierSourceType
MockContentVerifierDelegate::GetVerifierSourceType(const Extension& extension) { MockContentVerifierDelegate::GetVerifierSourceType(const Extension& extension) {
return VerifierSourceType::SIGNED_HASHES; return verifier_source_type_;
} }
ContentVerifierKey MockContentVerifierDelegate::GetPublicKey() { ContentVerifierKey MockContentVerifierDelegate::GetPublicKey() {
DCHECK_EQ(VerifierSourceType::SIGNED_HASHES, verifier_source_type_);
return ContentVerifierKey(kWebstoreSignaturesPublicKey, return ContentVerifierKey(kWebstoreSignaturesPublicKey,
kWebstoreSignaturesPublicKeySize); kWebstoreSignaturesPublicKeySize);
} }
...@@ -147,6 +148,7 @@ ContentVerifierKey MockContentVerifierDelegate::GetPublicKey() { ...@@ -147,6 +148,7 @@ ContentVerifierKey MockContentVerifierDelegate::GetPublicKey() {
GURL MockContentVerifierDelegate::GetSignatureFetchUrl( GURL MockContentVerifierDelegate::GetSignatureFetchUrl(
const ExtensionId& extension_id, const ExtensionId& extension_id,
const base::Version& version) { const base::Version& version) {
DCHECK_EQ(VerifierSourceType::SIGNED_HASHES, verifier_source_type_);
std::string url = std::string url =
base::StringPrintf("http://localhost/getsignature?id=%s&version=%s", base::StringPrintf("http://localhost/getsignature?id=%s&version=%s",
extension_id.c_str(), version.GetString().c_str()); extension_id.c_str(), version.GetString().c_str());
...@@ -155,7 +157,6 @@ GURL MockContentVerifierDelegate::GetSignatureFetchUrl( ...@@ -155,7 +157,6 @@ GURL MockContentVerifierDelegate::GetSignatureFetchUrl(
std::set<base::FilePath> MockContentVerifierDelegate::GetBrowserImagePaths( std::set<base::FilePath> MockContentVerifierDelegate::GetBrowserImagePaths(
const extensions::Extension* extension) { const extensions::Extension* extension) {
ADD_FAILURE() << "Unexpected call for this test";
return std::set<base::FilePath>(); return std::set<base::FilePath>();
} }
...@@ -167,6 +168,11 @@ void MockContentVerifierDelegate::VerifyFailed( ...@@ -167,6 +168,11 @@ void MockContentVerifierDelegate::VerifyFailed(
void MockContentVerifierDelegate::Shutdown() {} void MockContentVerifierDelegate::Shutdown() {}
void MockContentVerifierDelegate::SetVerifierSourceType(
VerifierSourceType type) {
verifier_source_type_ = type;
}
// VerifierObserver ----------------------------------------------------------- // VerifierObserver -----------------------------------------------------------
VerifierObserver::VerifierObserver() { VerifierObserver::VerifierObserver() {
EXPECT_TRUE( EXPECT_TRUE(
......
...@@ -123,7 +123,12 @@ class MockContentVerifierDelegate : public ContentVerifierDelegate { ...@@ -123,7 +123,12 @@ class MockContentVerifierDelegate : public ContentVerifierDelegate {
ContentVerifyJob::FailureReason reason) override; ContentVerifyJob::FailureReason reason) override;
void Shutdown() override; void Shutdown() override;
// Modifier.
void SetVerifierSourceType(VerifierSourceType type);
private: private:
VerifierSourceType verifier_source_type_ = VerifierSourceType::SIGNED_HASHES;
DISALLOW_COPY_AND_ASSIGN(MockContentVerifierDelegate); DISALLOW_COPY_AND_ASSIGN(MockContentVerifierDelegate);
}; };
......
...@@ -13,10 +13,12 @@ namespace extensions { ...@@ -13,10 +13,12 @@ namespace extensions {
ContentVerifierIOData::ExtensionData::ExtensionData( ContentVerifierIOData::ExtensionData::ExtensionData(
std::unique_ptr<std::set<base::FilePath>> browser_image_paths, std::unique_ptr<std::set<base::FilePath>> browser_image_paths,
std::unique_ptr<std::set<base::FilePath>> background_or_content_paths, std::unique_ptr<std::set<base::FilePath>> background_or_content_paths,
const base::Version& version) const base::Version& version,
ContentVerifierDelegate::VerifierSourceType source_type)
: browser_image_paths(std::move(browser_image_paths)), : browser_image_paths(std::move(browser_image_paths)),
background_or_content_paths(std::move(background_or_content_paths)), background_or_content_paths(std::move(background_or_content_paths)),
version(version) {} version(version),
source_type(source_type) {}
ContentVerifierIOData::ContentVerifierIOData() { ContentVerifierIOData::ContentVerifierIOData() {
} }
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
#include "base/files/file_path.h" #include "base/files/file_path.h"
#include "base/memory/ref_counted.h" #include "base/memory/ref_counted.h"
#include "base/version.h" #include "base/version.h"
#include "extensions/browser/content_verifier_delegate.h"
namespace extensions { namespace extensions {
...@@ -26,11 +27,13 @@ class ContentVerifierIOData { ...@@ -26,11 +27,13 @@ class ContentVerifierIOData {
// Set of file paths used as background scripts, pages or content scripts. // Set of file paths used as background scripts, pages or content scripts.
std::unique_ptr<std::set<base::FilePath>> background_or_content_paths; std::unique_ptr<std::set<base::FilePath>> background_or_content_paths;
base::Version version; base::Version version;
ContentVerifierDelegate::VerifierSourceType source_type;
ExtensionData( ExtensionData(
std::unique_ptr<std::set<base::FilePath>> browser_image_paths, std::unique_ptr<std::set<base::FilePath>> browser_image_paths,
std::unique_ptr<std::set<base::FilePath>> background_or_content_paths, std::unique_ptr<std::set<base::FilePath>> background_or_content_paths,
const base::Version& version); const base::Version& version,
ContentVerifierDelegate::VerifierSourceType source_type);
~ExtensionData(); ~ExtensionData();
}; };
......
This archive contains:
_metadata/verified_contents.json with a valid signature (see notes about
with_verified_contents archive, it's the same) and hash of one file,
background.js.
background.js, which contents differs from the original one
_metadata/computed_hashes.json, which contains hash of modified background.js
(so this hash effectively differs from one stored in verified_contents.json).
The purpose of this archive is to check that content verifier will catch such
type of corruption too.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment