#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/test/bind.h"
#include "base/values.h"
#include "base/version.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/url_loader_interceptor.h"
#include "extensions/browser/content_verifier/content_verifier.h"
#include "extensions/browser/content_verifier/test_utils.h"
#include "extensions/browser/extensions_test.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_paths.h"
#include "extensions/common/file_util.h"
#include "extensions/test/test_extension_dir.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/zlib/google/zip.h"
namespace extensions {
namespace {
enum ContentVerifyJobAsyncRunMode {
kNone,
kContentReadBeforeHashesReady,
kHashesReadyBeforeContentRead,
};
std::string GetVerifiedContents(const Extension& extension) {
std::string verified_contents;
EXPECT_TRUE(base::ReadFileToString(
file_util::GetVerifiedContentsPath(extension.path()),
&verified_contents));
return verified_contents;
}
void WriteManifest(TestExtensionDir* dir) {
dir->WriteManifest(base::Value::Dict()
.Set("manifest_version", 2)
.Set("name", "Test extension")
.Set("version", "1.0"));
}
void WriteComputedHashes(
const base::FilePath& extension_root,
const std::map<base::FilePath, std::string>& contents) {
int block_size = extension_misc::kContentVerificationDefaultBlockSize;
ComputedHashes::Data computed_hashes_data;
for (const auto& resource : contents) {
std::vector<std::string> hashes =
ComputedHashes::GetHashesForContent(resource.second, block_size);
computed_hashes_data.Add(resource.first, block_size, std::move(hashes));
}
base::CreateDirectory(extension_root.Append(kMetadataFolder));
ASSERT_TRUE(
ComputedHashes(std::move(computed_hashes_data))
.WriteToFile(file_util::GetComputedHashesPath(extension_root)));
}
}
class ContentVerifyJobUnittest : public ExtensionsTest {
public:
ContentVerifyJobUnittest() {}
ContentVerifyJobUnittest(const ContentVerifyJobUnittest&) = delete;
ContentVerifyJobUnittest& operator=(const ContentVerifyJobUnittest&) = delete;
~ContentVerifyJobUnittest() override {}
base::FilePath GetTestPath(const std::string& relative_path) {
base::FilePath base_path;
EXPECT_TRUE(base::PathService::Get(DIR_TEST_DATA, &base_path));
return base_path.AppendASCII("content_hash_fetcher")
.AppendASCII(relative_path);
}
void SetUp() override {
ExtensionsTest::SetUp();
auto delegate = std::make_unique<MockContentVerifierDelegate>();
content_verifier_delegate_ = delegate.get();
content_verifier_ = base::MakeRefCounted<ContentVerifier>(
&testing_context_, std::move(delegate));
}
void TearDown() override {
content_verifier_->Shutdown();
content_verifier_delegate_ = nullptr;
ExtensionsTest::TearDown();
}
scoped_refptr<ContentVerifier> content_verifier() {
return content_verifier_;
}
protected:
ContentVerifyJob::FailureReason RunContentVerifyJob(
const Extension& extension,
const base::FilePath& resource_path,
std::optional<std::string_view> resource_contents,
ContentVerifyJobAsyncRunMode run_mode) {
TestContentVerifySingleJobObserver observer(extension.id(), resource_path);
auto verify_job = base::MakeRefCounted<ContentVerifyJob>(
extension.id(), extension.version(), extension.path(), resource_path);
auto run_content_read_step = base::BindRepeating(
[](std::optional<std::string_view> resource_contents,
ContentVerifyJob* verify_job) {
auto read_data = resource_contents.value_or(std::string_view{});
MojoResult read_result =
resource_contents ? MOJO_RESULT_OK : MOJO_RESULT_NOT_FOUND;
verify_job->BytesRead(read_data, read_result);
verify_job->DoneReading();
},
std::move(resource_contents));
switch (run_mode) {
case kNone:
StartJob(verify_job, extension.version(), extension.manifest_version(),
base::DoNothing());
run_content_read_step.Run(verify_job.get());
break;
case kContentReadBeforeHashesReady:
run_content_read_step.Run(verify_job.get());
StartJob(verify_job, extension.version(), extension.manifest_version(),
base::DoNothing());
break;
case kHashesReadyBeforeContentRead:
StartJob(verify_job, extension.version(), extension.manifest_version(),
base::DoNothing());
observer.WaitForOnHashesReady();
run_content_read_step.Run(verify_job.get());
break;
}
return observer.WaitForJobFinished();
}
ContentVerifyJob::FailureReason RunContentVerifyJob(
const Extension& extension,
const base::FilePath& resource_path,
std::optional<std::string_view> resource_contents) {
return RunContentVerifyJob(extension, resource_path, resource_contents,
kNone);
}
ContentVerifyJob::FailureReason RunContentVerifyJobWithMultipleReadErrors(
const Extension& extension,
const base::FilePath& resource_path,
base::span<MojoResult> read_errors) {
TestContentVerifySingleJobObserver observer(extension.id(), resource_path);
auto verify_job = base::MakeRefCounted<ContentVerifyJob>(
extension.id(), extension.version(), extension.path(), resource_path);
StartJob(verify_job, extension.version(), extension.manifest_version(),
base::DoNothing());
for (const MojoResult read_error : read_errors) {
verify_job->BytesRead({}, read_error);
}
verify_job->DoneReading();
return observer.WaitForJobFinished();
}
void StartContentVerifyJob(const Extension& extension,
const base::FilePath& resource_path) {
auto verify_job = base::MakeRefCounted<ContentVerifyJob>(
extension.id(), extension.version(), extension.path(), resource_path);
StartJob(verify_job, extension.version(), extension.manifest_version(),
base::DoNothing());
}
scoped_refptr<Extension> LoadTestExtensionFromZipPathToTempDir(
TestExtensionDir* temp_dir,
const std::string& zip_directory_name,
const std::string& zip_filename) {
base::FilePath unzipped_path = temp_dir->UnpackedPath();
base::FilePath test_dir_base = GetTestPath(zip_directory_name);
scoped_refptr<Extension> extension =
content_verifier_test_utils::UnzipToDirAndLoadExtension(
test_dir_base.AppendASCII(zip_filename), unzipped_path);
EXPECT_TRUE(extension);
if (content_verifier_delegate()->GetVerifierSourceType(*extension) ==
ContentVerifierDelegate::VerifierSourceType::SIGNED_HASHES &&
!base::PathExists(
file_util::GetVerifiedContentsPath(extension->path()))) {
ADD_FAILURE() << "verified_contents.json not found.";
return nullptr;
}
content_verifier_->OnExtensionLoaded(&testing_context_, extension.get());
return extension;
}
scoped_refptr<Extension> CreateAndLoadTestExtensionToTempDir(
TestExtensionDir* temp_dir,
std::optional<std::map<base::FilePath, std::string>>
resources_for_hashes) {
WriteManifest(temp_dir);
if (resources_for_hashes) {
WriteComputedHashes(temp_dir->UnpackedPath(),
resources_for_hashes.value());
}
std::u16string error;
scoped_refptr<Extension> extension = file_util::LoadExtension(
temp_dir->UnpackedPath(), mojom::ManifestLocation::kInternal,
Extension::InitFromValueFlags::NO_FLAGS, &error);
EXPECT_NE(nullptr, extension.get()) << " error:'" << error << "'";
content_verifier_->OnExtensionLoaded(&testing_context_, extension.get());
return extension;
}
MockContentVerifierDelegate* content_verifier_delegate() {
DCHECK(content_verifier_);
DCHECK(content_verifier_delegate_);
return content_verifier_delegate_;
}
private:
void StartJob(scoped_refptr<ContentVerifyJob> job,
const base::Version& extension_version,
int manifest_version,
ContentVerifyJob::FailureCallback failure_callback) {
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&ContentVerifyJob::Start, job,
base::Unretained(content_verifier_.get()),
extension_version, manifest_version,
std::move(failure_callback)));
}
scoped_refptr<ContentVerifier> content_verifier_;
raw_ptr<MockContentVerifierDelegate> content_verifier_delegate_ =
nullptr;
content::TestBrowserContext testing_context_;
};
TEST_F(ContentVerifyJobUnittest, DeletedAndMissingFiles) {
TestExtensionDir temp_dir;
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "with_verified_contents", "source_all.zip");
ASSERT_TRUE(extension.get());
base::FilePath unzipped_path = temp_dir.UnpackedPath();
const base::FilePath::CharType kExistentResource[] =
FILE_PATH_LITERAL("background.js");
base::FilePath existent_resource_path(kExistentResource);
{
std::string contents;
base::ReadFileToString(unzipped_path.Append(existent_resource_path),
&contents);
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), existent_resource_path,
contents));
}
{
EXPECT_TRUE(base::DeleteFile(unzipped_path.Append(existent_resource_path)));
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH,
RunContentVerifyJob(*extension.get(), existent_resource_path,
std::nullopt));
}
{
const base::FilePath::CharType kNonExistentResource[] =
FILE_PATH_LITERAL("non-existent.js");
base::FilePath non_existent_resource_path(kNonExistentResource);
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), non_existent_resource_path,
std::nullopt));
}
{
const base::FilePath::CharType kUnexpectedResource[] =
FILE_PATH_LITERAL("foo.js");
base::FilePath unexpected_resource_path(kUnexpectedResource);
base::FilePath full_path = unzipped_path.Append(unexpected_resource_path);
EXPECT_TRUE(base::WriteFile(full_path, "42"));
std::string contents;
base::ReadFileToString(full_path, &contents);
EXPECT_EQ(ContentVerifyJob::NO_HASHES_FOR_FILE,
RunContentVerifyJob(*extension.get(), unexpected_resource_path,
contents));
}
{
base::FilePath empty_path_resource_path(FILE_PATH_LITERAL(""));
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), empty_path_resource_path,
std::nullopt));
}
{
const base::FilePath::CharType kUnexpectedFolder[] =
FILE_PATH_LITERAL("bar/");
base::FilePath unexpected_folder_path(kUnexpectedFolder);
base::CreateDirectory(unzipped_path.Append(unexpected_folder_path));
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), unexpected_folder_path,
std::nullopt));
}
}
TEST_F(ContentVerifyJobUnittest, MultipleReadErrors) {
TestExtensionDir temp_dir;
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "with_verified_contents", "source_all.zip");
ASSERT_TRUE(extension.get());
base::FilePath unzipped_path = temp_dir.UnpackedPath();
const base::FilePath::CharType kExistentResource[] =
FILE_PATH_LITERAL("background.js");
base::FilePath existent_resource_path(kExistentResource);
MojoResult read_errors[] = {
MOJO_RESULT_DATA_LOSS,
MOJO_RESULT_ABORTED,
};
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH,
RunContentVerifyJobWithMultipleReadErrors(
*extension.get(), existent_resource_path, read_errors));
}
namespace {
void WriteIncorrectComputedHashes(const base::FilePath& extension_path,
const base::FilePath& resource_path) {
ASSERT_TRUE(
base::PathExists(file_util::GetComputedHashesPath(extension_path)));
base::DeleteFile(file_util::GetComputedHashesPath(extension_path));
int block_size = extension_misc::kContentVerificationDefaultBlockSize;
ComputedHashes::Data incorrect_computed_hashes_data;
const std::string kFakeContents = "fake contents";
std::vector<std::string> hashes =
ComputedHashes::GetHashesForContent(kFakeContents, block_size);
incorrect_computed_hashes_data.Add(resource_path, block_size,
std::move(hashes));
ASSERT_TRUE(
ComputedHashes(std::move(incorrect_computed_hashes_data))
.WriteToFile(file_util::GetComputedHashesPath(extension_path)));
}
void WriteEmptyComputedHashes(const base::FilePath& extension_path) {
ASSERT_TRUE(
base::PathExists(file_util::GetComputedHashesPath(extension_path)));
base::DeleteFile(file_util::GetComputedHashesPath(extension_path));
ComputedHashes::Data incorrect_computed_hashes_data;
ASSERT_TRUE(
ComputedHashes(std::move(incorrect_computed_hashes_data))
.WriteToFile(file_util::GetComputedHashesPath(extension_path)));
}
}
TEST_F(ContentVerifyJobUnittest, DeletedResourceAndCorruptedComputedHashes) {
TestExtensionDir temp_dir;
const base::FilePath::CharType kResource[] =
FILE_PATH_LITERAL("background.js");
base::FilePath resource_path(kResource);
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "with_verified_contents", "source_all.zip");
ASSERT_TRUE(extension.get());
base::FilePath unzipped_path = temp_dir.UnpackedPath();
WriteIncorrectComputedHashes(unzipped_path, resource_path);
EXPECT_TRUE(
base::DeleteFile(unzipped_path.Append(base::FilePath(kResource))));
content_verifier()->ClearCacheForTesting();
EXPECT_EQ(ContentVerifyJob::CORRUPTED_HASHES,
RunContentVerifyJob(*extension.get(), resource_path, std::nullopt));
}
TEST_F(ContentVerifyJobUnittest, DeletedResourceAndCleanedComputedHashes) {
TestExtensionDir temp_dir;
const base::FilePath::CharType kResource[] =
FILE_PATH_LITERAL("background.js");
base::FilePath resource_path(kResource);
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "with_verified_contents", "source_all.zip");
ASSERT_TRUE(extension.get());
base::FilePath unzipped_path = temp_dir.UnpackedPath();
WriteEmptyComputedHashes(unzipped_path);
EXPECT_TRUE(
base::DeleteFile(unzipped_path.Append(base::FilePath(kResource))));
content_verifier()->ClearCacheForTesting();
EXPECT_EQ(ContentVerifyJob::CORRUPTED_HASHES,
RunContentVerifyJob(*extension.get(), resource_path, std::nullopt));
}
TEST_F(ContentVerifyJobUnittest, LegitimateZeroByteFile) {
TestExtensionDir temp_dir;
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "zero_byte_file", "source.zip");
ASSERT_TRUE(extension.get());
base::FilePath unzipped_path = temp_dir.UnpackedPath();
const base::FilePath::CharType kResource[] =
FILE_PATH_LITERAL("background.js");
base::FilePath resource_path(kResource);
{
std::string contents;
base::ReadFileToString(unzipped_path.Append(resource_path), &contents);
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), resource_path, contents));
}
{
std::string modified_contents = "console.log('non empty');";
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH,
RunContentVerifyJob(*extension.get(), resource_path,
modified_contents));
}
}
TEST_F(ContentVerifyJobUnittest, DifferentSizedFiles) {
TestExtensionDir temp_dir;
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "different_sized_files", "source.zip");
ASSERT_TRUE(extension.get());
base::FilePath unzipped_path = temp_dir.UnpackedPath();
const struct {
const char* name;
size_t byte_size;
} kFilesToTest[] = {
{"1024.js", 1024}, {"4096.js", 4096}, {"8192.js", 8192},
{"8191.js", 8191}, {"8193.js", 8193},
};
for (const auto& file_to_test : kFilesToTest) {
base::FilePath resource_path = base::FilePath::FromASCII(file_to_test.name);
std::string contents;
base::ReadFileToString(unzipped_path.AppendASCII(file_to_test.name),
&contents);
EXPECT_EQ(file_to_test.byte_size, contents.size());
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), resource_path, contents));
}
}
TEST_F(ContentVerifyJobUnittest, ModifiedComputedHashes) {
TestExtensionDir temp_dir;
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "with_verified_contents_corrupted", "source_all.zip");
ASSERT_TRUE(extension.get());
base::FilePath unzipped_path = temp_dir.UnpackedPath();
const base::FilePath::CharType kExistentResource[] =
FILE_PATH_LITERAL("background.js");
base::FilePath existent_resource_path(kExistentResource);
{
std::string contents;
base::ReadFileToString(unzipped_path.Append(existent_resource_path),
&contents);
EXPECT_EQ(ContentVerifyJob::CORRUPTED_HASHES,
RunContentVerifyJob(*extension.get(), existent_resource_path,
contents));
}
}
using ContentVerifyJobWithoutSignedHashesUnittest = ContentVerifyJobUnittest;
TEST_F(ContentVerifyJobWithoutSignedHashesUnittest, ComputedHashesLoad) {
TestExtensionDir temp_dir;
content_verifier_delegate()->SetVerifierSourceType(
ContentVerifierDelegate::VerifierSourceType::UNSIGNED_HASHES);
const base::FilePath kResourcePath(FILE_PATH_LITERAL("script.js"));
const std::string kResourceContents = "console.log('Nothing special');";
std::map<base::FilePath, std::string> resource_map = {
{kResourcePath, kResourceContents}};
const std::string kCorruptedContents = "not a json";
scoped_refptr<Extension> extension =
CreateAndLoadTestExtensionToTempDir(&temp_dir, std::move(resource_map));
ASSERT_TRUE(extension);
base::FilePath unzipped_path = temp_dir.UnpackedPath();
{
TestContentVerifySingleJobObserver observer(extension->id(), kResourcePath);
content_verifier()->ClearCacheForTesting();
StartContentVerifyJob(*extension, kResourcePath);
ContentHashReader::InitStatus hashes_status =
observer.WaitForOnHashesReady();
EXPECT_EQ(ContentHashReader::InitStatus::SUCCESS, hashes_status);
}
{
ASSERT_TRUE(base::WriteFile(file_util::GetComputedHashesPath(unzipped_path),
kCorruptedContents));
TestContentVerifySingleJobObserver observer(extension->id(), kResourcePath);
content_verifier()->ClearCacheForTesting();
StartContentVerifyJob(*extension, kResourcePath);
ContentHashReader::InitStatus hashes_status =
observer.WaitForOnHashesReady();
EXPECT_EQ(ContentHashReader::InitStatus::HASHES_DAMAGED, hashes_status);
}
{
base::DeleteFile(file_util::GetComputedHashesPath(unzipped_path));
TestContentVerifySingleJobObserver observer(extension->id(), kResourcePath);
content_verifier()->ClearCacheForTesting();
StartContentVerifyJob(*extension, kResourcePath);
ContentHashReader::InitStatus hashes_status =
observer.WaitForOnHashesReady();
EXPECT_EQ(ContentHashReader::InitStatus::HASHES_MISSING, hashes_status);
}
}
TEST_F(ContentVerifyJobWithoutSignedHashesUnittest, UnverifiedExtension) {
TestExtensionDir temp_dir;
content_verifier_delegate()->SetVerifierSourceType(
ContentVerifierDelegate::VerifierSourceType::UNSIGNED_HASHES);
const base::FilePath kResourceOkPath(FILE_PATH_LITERAL("script-ok.js"));
const base::FilePath kResourceCorruptedPath(
FILE_PATH_LITERAL("script-corrupted.js"));
const base::FilePath kResourceMissingPath(
FILE_PATH_LITERAL("script-missing.js"));
const base::FilePath kResourceUnexpectedPath(
FILE_PATH_LITERAL("script-unexpected.js"));
const std::string kOkContents = "console.log('Nothing special');";
const std::string kCorruptedContents = "alert('Evil corrupted script');";
std::map<base::FilePath, std::string> resource_map = {
{kResourceOkPath, kOkContents}, {kResourceCorruptedPath, kOkContents}};
scoped_refptr<Extension> extension =
CreateAndLoadTestExtensionToTempDir(&temp_dir, std::move(resource_map));
ASSERT_TRUE(extension);
base::FilePath unzipped_path = temp_dir.UnpackedPath();
ASSERT_TRUE(
base::WriteFile(unzipped_path.Append(kResourceOkPath), kOkContents));
ASSERT_TRUE(base::WriteFile(unzipped_path.Append(kResourceCorruptedPath),
kCorruptedContents));
ASSERT_TRUE(base::WriteFile(unzipped_path.Append(kResourceUnexpectedPath),
kOkContents));
{
std::string contents;
base::ReadFileToString(unzipped_path.Append(kResourceOkPath), &contents);
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), kResourceOkPath, contents));
}
{
std::string contents;
base::ReadFileToString(unzipped_path.Append(kResourceCorruptedPath),
&contents);
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH,
RunContentVerifyJob(*extension.get(), kResourceCorruptedPath,
contents));
}
{
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), kResourceMissingPath,
std::nullopt));
}
{
std::string contents;
base::ReadFileToString(unzipped_path.Append(kResourceUnexpectedPath),
&contents);
EXPECT_EQ(ContentVerifyJob::NO_HASHES_FOR_FILE,
RunContentVerifyJob(*extension.get(), kResourceUnexpectedPath,
contents));
}
}
TEST_F(ContentVerifyJobWithoutSignedHashesUnittest, ExtensionWithoutHashes) {
TestExtensionDir temp_dir;
content_verifier_delegate()->SetVerifierSourceType(
ContentVerifierDelegate::VerifierSourceType::UNSIGNED_HASHES);
const base::FilePath kResourcePath(FILE_PATH_LITERAL("script-ok.js"));
scoped_refptr<Extension> extension =
CreateAndLoadTestExtensionToTempDir(&temp_dir, std::nullopt);
ASSERT_TRUE(extension);
base::FilePath unzipped_path = temp_dir.UnpackedPath();
const std::string kContents = "console.log('Nothing special');";
ASSERT_TRUE(base::WriteFile(unzipped_path.Append(kResourcePath), kContents));
{
std::string contents;
base::ReadFileToString(unzipped_path.Append(kResourcePath), &contents);
EXPECT_EQ(ContentVerifyJob::MISSING_ALL_HASHES,
RunContentVerifyJob(*extension.get(), kResourcePath, contents));
EXPECT_FALSE(
base::PathExists(file_util::GetComputedHashesPath(extension->path())));
}
}
class ContentMismatchUnittest
: public ContentVerifyJobUnittest,
public testing::WithParamInterface<ContentVerifyJobAsyncRunMode> {
public:
ContentMismatchUnittest() {}
ContentMismatchUnittest(const ContentMismatchUnittest&) = delete;
ContentMismatchUnittest& operator=(const ContentMismatchUnittest&) = delete;
protected:
void RunContentMismatchTest(const std::string& content_to_append_for_mismatch,
ContentVerifyJobAsyncRunMode run_mode) {
TestExtensionDir temp_dir;
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "with_verified_contents", "source_all.zip");
ASSERT_TRUE(extension.get());
base::FilePath unzipped_path = temp_dir.UnpackedPath();
const base::FilePath::CharType kResource[] =
FILE_PATH_LITERAL("background.js");
base::FilePath existent_resource_path(kResource);
{
std::string modified_contents;
base::ReadFileToString(unzipped_path.Append(existent_resource_path),
&modified_contents);
modified_contents.append(content_to_append_for_mismatch);
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH,
RunContentVerifyJob(*extension.get(), existent_resource_path,
modified_contents, run_mode));
}
}
};
INSTANTIATE_TEST_SUITE_P(ContentVerifyJobUnittest,
ContentMismatchUnittest,
testing::Values(kNone,
kContentReadBeforeHashesReady,
kHashesReadyBeforeContentRead));
TEST_P(ContentMismatchUnittest, ContentMismatch) {
RunContentMismatchTest("console.log('modified');", GetParam());
}
TEST_P(ContentMismatchUnittest, ContentMismatchWithLargeFile) {
std::string content_larger_than_block_size(
extension_misc::kContentVerificationDefaultBlockSize + 1, ';');
RunContentMismatchTest(content_larger_than_block_size, GetParam());
}
class ContentVerifyJobWithHashFetchUnittest : public ContentVerifyJobUnittest {
public:
ContentVerifyJobWithHashFetchUnittest()
: hash_fetch_interceptor_(base::BindRepeating(
&ContentVerifyJobWithHashFetchUnittest::InterceptHashFetch,
base::Unretained(this))) {}
ContentVerifyJobWithHashFetchUnittest(
const ContentVerifyJobWithHashFetchUnittest&) = delete;
ContentVerifyJobWithHashFetchUnittest& operator=(
const ContentVerifyJobWithHashFetchUnittest&) = delete;
protected:
void RespondToClientIfReady() {
DCHECK(verified_contents_);
if (!client_ || !ready_to_respond_) {
return;
}
content::URLLoaderInterceptor::WriteResponse(
std::string(), *verified_contents_, client_.get());
}
void ForceHashFetchOnNextResourceLoad(const Extension& extension) {
verified_contents_ = GetVerifiedContents(extension);
EXPECT_TRUE(base::DeletePathRecursively(
file_util::GetVerifiedContentsPath(extension.path())));
base::RunLoop run_loop;
content::GetIOThreadTaskRunner({})->PostTaskAndReply(
FROM_HERE,
base::BindOnce(
[](scoped_refptr<ContentVerifier> content_verifier) {
content_verifier->ClearCacheForTesting();
},
content_verifier()),
run_loop.QuitClosure());
run_loop.Run();
}
ContentVerifyJob::FailureReason RunContentVerifyJobBeforeHashReady(
scoped_refptr<Extension> extension,
const base::FilePath& resource_path,
MojoResult read_result) {
ForceHashFetchOnNextResourceLoad(*extension);
TestContentVerifySingleJobObserver observer(extension->id(), resource_path);
{
scoped_refptr<ContentVerifyJob> verify_job =
base::MakeRefCounted<ContentVerifyJob>(
extension->id(), extension->version(), extension->path(),
resource_path);
auto do_read_and_done =
[](scoped_refptr<ContentVerifyJob> job,
scoped_refptr<ContentVerifier> content_verifier,
scoped_refptr<Extension> extension, MojoResult read_result,
base::OnceClosure done_callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
job->Start(content_verifier.get(), extension->version(),
extension->manifest_version(), base::DoNothing());
job->BytesRead({}, read_result);
job->DoneReading();
std::move(done_callback).Run();
};
base::RunLoop run_loop;
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(do_read_and_done, verify_job, content_verifier(),
extension, read_result, run_loop.QuitClosure()));
run_loop.Run();
set_ready_to_respond();
RespondToClientIfReady();
}
return observer.WaitForJobFinished();
}
void set_ready_to_respond() { ready_to_respond_ = true; }
private:
bool InterceptHashFetch(
content::URLLoaderInterceptor::RequestParams* params) {
if (params->url_request.url.path() != "/getsignature") {
return false;
}
client_ = std::move(params->client);
RespondToClientIfReady();
return true;
}
content::URLLoaderInterceptor hash_fetch_interceptor_;
mojo::Remote<network::mojom::URLLoaderClient> client_;
bool ready_to_respond_ = false;
std::optional<std::string> verified_contents_;
};
TEST_F(ContentVerifyJobWithHashFetchUnittest, ReadErrorBeforeHashReady) {
TestExtensionDir temp_dir;
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "with_verified_contents", "source_all.zip");
ASSERT_TRUE(extension.get());
const base::FilePath::CharType kBackgroundJS[] =
FILE_PATH_LITERAL("background.js");
base::FilePath resource_path(kBackgroundJS);
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJobBeforeHashReady(extension, resource_path,
MOJO_RESULT_ABORTED));
}
TEST_F(ContentVerifyJobWithHashFetchUnittest,
ReadFileWithoutHashesBeforeHashReady) {
TestExtensionDir temp_dir;
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "with_verified_contents", "source_all.zip");
ASSERT_TRUE(extension.get());
base::FilePath unzipped_path = temp_dir.UnpackedPath();
const base::FilePath::CharType kUnexpectedResource[] =
FILE_PATH_LITERAL("foo.js");
base::FilePath unexpected_resource_path(kUnexpectedResource);
base::FilePath full_path = unzipped_path.Append(unexpected_resource_path);
EXPECT_TRUE(base::WriteFile(full_path, "42"));
EXPECT_EQ(ContentVerifyJob::NO_HASHES_FOR_FILE,
RunContentVerifyJobBeforeHashReady(
extension, unexpected_resource_path, MOJO_RESULT_OK));
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJobBeforeHashReady(
extension, unexpected_resource_path, MOJO_RESULT_ABORTED));
}
}