blob: df4458aebaff889bcb1dea851f2c525421550b4c [file] [log] [blame]
// Copyright 2017 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 "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/ptr_util.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/version.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "extensions/browser/content_hash_reader.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 "testing/gtest/include/gtest/gtest.h"
#include "third_party/zlib/google/zip.h"
namespace extensions {
namespace {
scoped_refptr<ContentHashReader> CreateContentHashReader(
const Extension& extension,
const base::FilePath& extension_resource_path) {
return base::MakeRefCounted<ContentHashReader>(
extension.id(), extension.version(), extension.path(),
extension_resource_path,
ContentVerifierKey(kWebstoreSignaturesPublicKey,
kWebstoreSignaturesPublicKeySize));
}
void DoNothingWithReasonParam(ContentVerifyJob::FailureReason reason) {}
class JobTestObserver : public ContentVerifyJob::TestObserver {
public:
JobTestObserver(const std::string& extension_id,
const base::FilePath& relative_path)
: extension_id_(extension_id), relative_path_(relative_path) {
ContentVerifyJob::SetObserverForTests(this);
}
~JobTestObserver() { ContentVerifyJob::SetObserverForTests(nullptr); }
void JobStarted(const std::string& extension_id,
const base::FilePath& relative_path) override {}
void JobFinished(const std::string& extension_id,
const base::FilePath& relative_path,
ContentVerifyJob::FailureReason reason) override {
if (extension_id != extension_id_ || relative_path != relative_path_)
return;
failure_reason_ = reason;
run_loop_.Quit();
}
ContentVerifyJob::FailureReason WaitAndGetFailureReason() {
// Run() returns immediately if Quit() has already been called.
run_loop_.Run();
EXPECT_TRUE(failure_reason_.has_value());
return failure_reason_.value_or(ContentVerifyJob::FAILURE_REASON_MAX);
}
private:
base::RunLoop run_loop_;
std::string extension_id_;
base::FilePath relative_path_;
base::Optional<ContentVerifyJob::FailureReason> failure_reason_;
DISALLOW_COPY_AND_ASSIGN(JobTestObserver);
};
} // namespace
class ContentVerifyJobUnittest : public ExtensionsTest {
public:
ContentVerifyJobUnittest()
// The TestBrowserThreadBundle is needed for ContentVerifyJob::Start().
: ExtensionsTest(std::make_unique<content::TestBrowserThreadBundle>(
content::TestBrowserThreadBundle::REAL_IO_THREAD)) {}
~ContentVerifyJobUnittest() override {}
// Helper to get files from our subdirectory in the general extensions test
// data dir.
base::FilePath GetTestPath(const base::FilePath& relative_path) {
base::FilePath base_path;
EXPECT_TRUE(PathService::Get(DIR_TEST_DATA, &base_path));
base_path = base_path.AppendASCII("content_hash_fetcher");
return base_path.Append(relative_path);
}
// Unzips the extension source from |extension_zip| into a temporary
// directory and loads it. Returns the resuling Extension object.
// |destination| points to the path where the extension was extracted.
scoped_refptr<Extension> UnzipToTempDirAndLoad(
const base::FilePath& extension_zip,
base::FilePath* destination) {
EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
*destination = temp_dir_.GetPath();
EXPECT_TRUE(zip::Unzip(extension_zip, *destination));
std::string error;
scoped_refptr<Extension> extension = file_util::LoadExtension(
*destination, Manifest::INTERNAL, 0 /* flags */, &error);
EXPECT_NE(nullptr, extension.get()) << " error:'" << error << "'";
return extension;
}
protected:
ContentVerifyJob::FailureReason RunContentVerifyJob(
const Extension& extension,
const base::FilePath& resource_path,
std::string& resource_contents) {
JobTestObserver observer(extension.id(), resource_path);
scoped_refptr<ContentHashReader> content_hash_reader =
CreateContentHashReader(extension, resource_path);
scoped_refptr<ContentVerifyJob> verify_job = new ContentVerifyJob(
content_hash_reader.get(), base::Bind(&DoNothingWithReasonParam));
verify_job->Start();
{
// Simulate serving |resource_contents| from |resource_path|.
verify_job->BytesRead(resource_contents.size(),
base::string_as_array(&resource_contents));
verify_job->DoneReading();
}
return observer.WaitAndGetFailureReason();
}
private:
base::ScopedTempDir temp_dir_;
DISALLOW_COPY_AND_ASSIGN(ContentVerifyJobUnittest);
};
// Tests that deleted legitimate files trigger content verification failure.
// Also tests that non-existent file request does not trigger content
// verification failure.
TEST_F(ContentVerifyJobUnittest, DeletedAndMissingFiles) {
base::FilePath unzipped_path;
base::FilePath test_dir_base =
GetTestPath(base::FilePath(FILE_PATH_LITERAL("with_verified_contents")));
scoped_refptr<Extension> extension = UnzipToTempDirAndLoad(
test_dir_base.AppendASCII("source_all.zip"), &unzipped_path);
ASSERT_TRUE(extension.get());
// Make sure there is a verified_contents.json file there as this test cannot
// fetch it.
EXPECT_TRUE(
base::PathExists(file_util::GetVerifiedContentsPath(extension->path())));
const base::FilePath::CharType kExistentResource[] =
FILE_PATH_LITERAL("background.js");
base::FilePath existent_resource_path(kExistentResource);
{
// Make sure background.js passes verification correctly.
std::string contents;
base::ReadFileToString(
unzipped_path.Append(base::FilePath(kExistentResource)), &contents);
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), existent_resource_path,
contents));
}
{
// Once background.js is deleted, verification will result in HASH_MISMATCH.
// Delete the existent file first.
EXPECT_TRUE(base::DeleteFile(
unzipped_path.Append(base::FilePath(kExistentResource)), false));
// Deleted file will serve empty contents.
std::string empty_contents;
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH,
RunContentVerifyJob(*extension.get(), existent_resource_path,
empty_contents));
}
{
// Now ask for a non-existent resource non-existent.js. Verification should
// skip this file as it is not listed in our verified_contents.json file.
const base::FilePath::CharType kNonExistentResource[] =
FILE_PATH_LITERAL("non-existent.js");
base::FilePath non_existent_resource_path(kNonExistentResource);
// Non-existent file will serve empty contents.
std::string empty_contents;
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), non_existent_resource_path,
empty_contents));
}
{
// Now create a resource foo.js which exists on disk but is not in the
// extension's verified_contents.json. Verification should result in
// NO_HASHES_FOR_FILE since the extension is trying to load a file the
// extension should not have.
const base::FilePath::CharType kUnexpectedResource[] =
FILE_PATH_LITERAL("foo.js");
base::FilePath unexpected_resource_path(kUnexpectedResource);
base::FilePath full_path =
unzipped_path.Append(base::FilePath(unexpected_resource_path));
base::WriteFile(full_path, "42", sizeof("42"));
std::string contents;
base::ReadFileToString(full_path, &contents);
EXPECT_EQ(ContentVerifyJob::NO_HASHES_FOR_FILE,
RunContentVerifyJob(*extension.get(), unexpected_resource_path,
contents));
}
{
// Ask for the root path of the extension (i.e., chrome-extension://<id>/).
// Verification should skip this request as if the resource were
// non-existent. See https://crbug.com/791929.
base::FilePath empty_path_resource_path(FILE_PATH_LITERAL(""));
std::string empty_contents;
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), empty_path_resource_path,
empty_contents));
}
}
// Tests that content modification causes content verification failure.
TEST_F(ContentVerifyJobUnittest, ContentMismatch) {
base::FilePath unzipped_path;
base::FilePath test_dir_base =
GetTestPath(base::FilePath(FILE_PATH_LITERAL("with_verified_contents")));
scoped_refptr<Extension> extension = UnzipToTempDirAndLoad(
test_dir_base.AppendASCII("source_all.zip"), &unzipped_path);
ASSERT_TRUE(extension.get());
// Make sure there is a verified_contents.json file there as this test cannot
// fetch it.
EXPECT_TRUE(
base::PathExists(file_util::GetVerifiedContentsPath(extension->path())));
const base::FilePath::CharType kResource[] =
FILE_PATH_LITERAL("background.js");
base::FilePath existent_resource_path(kResource);
{
// Make sure modified background.js fails content verification.
std::string modified_contents;
base::ReadFileToString(unzipped_path.Append(base::FilePath(kResource)),
&modified_contents);
modified_contents.append("console.log('modified');");
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH,
RunContentVerifyJob(*extension.get(), existent_resource_path,
modified_contents));
}
}
// Tests that extension resources that are originally 0 byte behave correctly
// with content verification.
TEST_F(ContentVerifyJobUnittest, LegitimateZeroByteFile) {
base::FilePath unzipped_path;
base::FilePath test_dir_base =
GetTestPath(base::FilePath(FILE_PATH_LITERAL("zero_byte_file")));
// |extension| has a 0 byte background.js file in it.
scoped_refptr<Extension> extension = UnzipToTempDirAndLoad(
test_dir_base.AppendASCII("source.zip"), &unzipped_path);
ASSERT_TRUE(extension.get());
// Make sure there is a verified_contents.json file there as this test cannot
// fetch it.
EXPECT_TRUE(
base::PathExists(file_util::GetVerifiedContentsPath(extension->path())));
const base::FilePath::CharType kResource[] =
FILE_PATH_LITERAL("background.js");
base::FilePath resource_path(kResource);
{
// Make sure 0 byte background.js passes content verification.
std::string contents;
base::ReadFileToString(unzipped_path.Append(base::FilePath(kResource)),
&contents);
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), resource_path, contents));
}
{
// Make sure non-empty background.js fails content verification.
std::string modified_contents = "console.log('non empty');";
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH,
RunContentVerifyJob(*extension.get(), resource_path,
modified_contents));
}
}
// Tests that extension resources of different interesting sizes work properly.
// Regression test for https://crbug.com/720597, where content verification
// always failed for sizes multiple of content hash's block size (4096 bytes).
TEST_F(ContentVerifyJobUnittest, DifferentSizedFiles) {
base::FilePath test_dir_base =
GetTestPath(base::FilePath(FILE_PATH_LITERAL("different_sized_files")));
base::FilePath unzipped_path;
scoped_refptr<Extension> extension = UnzipToTempDirAndLoad(
test_dir_base.AppendASCII("source.zip"), &unzipped_path);
ASSERT_TRUE(extension.get());
// Make sure there is a verified_contents.json file there as this test cannot
// fetch it.
EXPECT_TRUE(
base::PathExists(file_util::GetVerifiedContentsPath(extension->path())));
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::FromUTF8Unsafe(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));
}
}
} // namespace extensions