910e62b5创建于 1月15日历史提交
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "extensions/browser/content_verifier/test_utils.h"

#include "base/base64url.h"
#include "base/containers/contains.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/strings/stringprintf.h"
#include "base/task/sequenced_task_runner.h"
#include "base/values.h"
#include "content/public/browser/browser_thread.h"
#include "crypto/keypair.h"
#include "crypto/sign.h"
#include "crypto/test_support.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/file_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/zlib/google/zip.h"

namespace extensions {

// TestContentVerifySingleJobObserver ------------------------------------------
TestContentVerifySingleJobObserver::TestContentVerifySingleJobObserver(
    const ExtensionId& extension_id,
    const base::FilePath& relative_path)
    : client_(
          base::MakeRefCounted<ObserverClient>(extension_id, relative_path)) {
  ContentVerifyJob::SetObserverForTests(client_);
}

TestContentVerifySingleJobObserver::~TestContentVerifySingleJobObserver() {
  ContentVerifyJob::SetObserverForTests(nullptr);
}

ContentVerifyJob::FailureReason
TestContentVerifySingleJobObserver::WaitForJobFinished() {
  return client_->WaitForJobFinished();
}

ContentHashReader::InitStatus
TestContentVerifySingleJobObserver::WaitForOnHashesReady() {
  return client_->WaitForOnHashesReady();
}

TestContentVerifySingleJobObserver::ObserverClient::ObserverClient(
    const ExtensionId& extension_id,
    const base::FilePath& relative_path)
    : extension_id_(extension_id), relative_path_(relative_path) {
  EXPECT_TRUE(
      content::BrowserThread::GetCurrentThreadIdentifier(&creation_thread_));
}

TestContentVerifySingleJobObserver::ObserverClient::~ObserverClient() = default;

void TestContentVerifySingleJobObserver::ObserverClient::JobFinished(
    const ExtensionId& extension_id,
    const base::FilePath& relative_path,
    ContentVerifyJob::FailureReason reason) {
  if (!content::BrowserThread::CurrentlyOn(creation_thread_)) {
    content::BrowserThread::GetTaskRunnerForThread(creation_thread_)
        ->PostTask(FROM_HERE,
                   base::BindOnce(&TestContentVerifySingleJobObserver::
                                      ObserverClient::JobFinished,
                                  this, extension_id, relative_path, reason));
    return;
  }
  if (extension_id != extension_id_ || relative_path != relative_path_)
    return;
  EXPECT_FALSE(failure_reason_.has_value());
  failure_reason_ = reason;
  job_finished_run_loop_.Quit();
}

void TestContentVerifySingleJobObserver::ObserverClient::OnHashesReady(
    const ExtensionId& extension_id,
    const base::FilePath& relative_path,
    const ContentHashReader& hash_reader) {
  if (content::BrowserThread::CurrentlyOn(creation_thread_))
    OnHashesReadyOnCreationThread(extension_id, relative_path,
                                  hash_reader.status());
  else {
    content::BrowserThread::GetTaskRunnerForThread(creation_thread_)
        ->PostTask(
            FROM_HERE,
            base::BindOnce(&TestContentVerifySingleJobObserver::ObserverClient::
                               OnHashesReadyOnCreationThread,
                           this, extension_id, relative_path,
                           hash_reader.status()));
  }
}

void TestContentVerifySingleJobObserver::ObserverClient::
    OnHashesReadyOnCreationThread(const ExtensionId& extension_id,
                                  const base::FilePath& relative_path,
                                  ContentHashReader::InitStatus hashes_status) {
  if (extension_id != extension_id_ || relative_path != relative_path_)
    return;
  EXPECT_FALSE(seen_on_hashes_ready_);
  seen_on_hashes_ready_ = true;
  hashes_status_ = hashes_status;
  on_hashes_ready_run_loop_.Quit();
}

ContentVerifyJob::FailureReason
TestContentVerifySingleJobObserver::ObserverClient::WaitForJobFinished() {
  // Run() returns immediately if Quit() has already been called.
  job_finished_run_loop_.Run();
  EXPECT_TRUE(failure_reason_.has_value());
  return failure_reason_.value_or(ContentVerifyJob::FAILURE_REASON_MAX);
}

ContentHashReader::InitStatus
TestContentVerifySingleJobObserver::ObserverClient::WaitForOnHashesReady() {
  // Run() returns immediately if Quit() has already been called.
  on_hashes_ready_run_loop_.Run();
  return hashes_status_;
}

// TestContentVerifyJobObserver ------------------------------------------------
TestContentVerifyJobObserver::TestContentVerifyJobObserver()
    : client_(base::MakeRefCounted<ObserverClient>()) {
  ContentVerifyJob::SetObserverForTests(client_);
}

TestContentVerifyJobObserver::~TestContentVerifyJobObserver() {
  ContentVerifyJob::SetObserverForTests(nullptr);
}

void TestContentVerifyJobObserver::ExpectJobResult(
    const ExtensionId& extension_id,
    const base::FilePath& relative_path,
    Result expected_result) {
  client_->ExpectJobResult(extension_id, relative_path, expected_result);
}

bool TestContentVerifyJobObserver::WaitForExpectedJobs() {
  return client_->WaitForExpectedJobs();
}

TestContentVerifyJobObserver::ObserverClient::ObserverClient() {
  EXPECT_TRUE(
      content::BrowserThread::GetCurrentThreadIdentifier(&creation_thread_));
}

TestContentVerifyJobObserver::ObserverClient::~ObserverClient() = default;

void TestContentVerifyJobObserver::ObserverClient::JobFinished(
    const ExtensionId& extension_id,
    const base::FilePath& relative_path,
    ContentVerifyJob::FailureReason failure_reason) {
  if (!content::BrowserThread::CurrentlyOn(creation_thread_)) {
    content::BrowserThread::GetTaskRunnerForThread(creation_thread_)
        ->PostTask(
            FROM_HERE,
            base::BindOnce(
                &TestContentVerifyJobObserver::ObserverClient::JobFinished,
                this, extension_id, relative_path, failure_reason));
    return;
  }
  Result result = failure_reason == ContentVerifyJob::NONE ? Result::SUCCESS
                                                           : Result::FAILURE;
  bool found = false;
  for (auto i = expectations_.begin(); i != expectations_.end(); ++i) {
    if (i->extension_id == extension_id && i->path == relative_path &&
        i->result == result) {
      found = true;
      expectations_.erase(i);
      break;
    }
  }
  if (found) {
    if (expectations_.empty() && job_quit_closure_)
      std::move(job_quit_closure_).Run();
  } else {
    LOG(WARNING) << "Ignoring unexpected JobFinished " << extension_id << "/"
                 << relative_path.value()
                 << " failure_reason:" << failure_reason;
  }
}

void TestContentVerifyJobObserver::ObserverClient::ExpectJobResult(
    const ExtensionId& extension_id,
    const base::FilePath& relative_path,
    Result expected_result) {
  expectations_.push_back(
      ExpectedResult(extension_id, relative_path, expected_result));
}

bool TestContentVerifyJobObserver::ObserverClient::WaitForExpectedJobs() {
  EXPECT_TRUE(content::BrowserThread::CurrentlyOn(creation_thread_));
  if (!expectations_.empty()) {
    base::RunLoop run_loop;
    job_quit_closure_ = run_loop.QuitClosure();
    run_loop.Run();
  }
  return expectations_.empty();
}

// MockContentVerifierDelegate ------------------------------------------------
MockContentVerifierDelegate::MockContentVerifierDelegate()
    : verifier_key_(kWebstoreSignaturesPublicKey.begin(),
                    kWebstoreSignaturesPublicKey.end()) {}

MockContentVerifierDelegate::~MockContentVerifierDelegate() = default;

ContentVerifierDelegate::VerifierSourceType
MockContentVerifierDelegate::GetVerifierSourceType(const Extension& extension) {
  return verifier_source_type_;
}

ContentVerifierKey MockContentVerifierDelegate::GetPublicKey() {
  DCHECK_EQ(VerifierSourceType::SIGNED_HASHES, verifier_source_type_);
  return verifier_key_;
}

GURL MockContentVerifierDelegate::GetSignatureFetchUrl(
    const ExtensionId& extension_id,
    const base::Version& version) {
  DCHECK_EQ(VerifierSourceType::SIGNED_HASHES, verifier_source_type_);
  std::string url =
      base::StringPrintf("http://localhost/getsignature?id=%s&version=%s",
                         extension_id.c_str(), version.GetString().c_str());
  return GURL(url);
}

std::set<base::FilePath> MockContentVerifierDelegate::GetBrowserImagePaths(
    const extensions::Extension* extension) {
  return std::set<base::FilePath>();
}

void MockContentVerifierDelegate::VerifyFailed(
    const ExtensionId& extension_id,
    ContentVerifyJob::FailureReason reason) {
  ADD_FAILURE() << "Unexpected call for this test";
}

void MockContentVerifierDelegate::Shutdown() {}

void MockContentVerifierDelegate::SetVerifierSourceType(
    VerifierSourceType type) {
  verifier_source_type_ = type;
}

void MockContentVerifierDelegate::SetVerifierKey(std::vector<uint8_t> key) {
  verifier_key_ = std::move(key);
}

// VerifierObserver -----------------------------------------------------------
VerifierObserver::VerifierObserver() {
  EXPECT_TRUE(
      content::BrowserThread::GetCurrentThreadIdentifier(&creation_thread_));
  ContentVerifier::SetObserverForTests(this);
}

VerifierObserver::~VerifierObserver() {
  ContentVerifier::SetObserverForTests(nullptr);
}

void VerifierObserver::EnsureFetchCompleted(const ExtensionId& extension_id) {
  EXPECT_TRUE(content::BrowserThread::CurrentlyOn(creation_thread_));
  if (base::Contains(completed_fetches_, extension_id))
    return;
  EXPECT_TRUE(id_to_wait_for_.empty());
  EXPECT_EQ(loop_runner_.get(), nullptr);
  id_to_wait_for_ = extension_id;
  loop_runner_ = base::MakeRefCounted<content::MessageLoopRunner>();
  loop_runner_->Run();
  id_to_wait_for_.clear();
  loop_runner_ = nullptr;
}

void VerifierObserver::OnFetchComplete(
    const scoped_refptr<const ContentHash>& content_hash,
    bool did_hash_mismatch) {
  if (!content::BrowserThread::CurrentlyOn(creation_thread_)) {
    content::BrowserThread::GetTaskRunnerForThread(creation_thread_)
        ->PostTask(FROM_HERE, base::BindOnce(&VerifierObserver::OnFetchComplete,
                                             weak_ptr_factory_.GetWeakPtr(),
                                             content_hash, did_hash_mismatch));
    return;
  }
  const ExtensionId extension_id = content_hash->extension_id();
  completed_fetches_.insert(extension_id);
  content_hash_ = content_hash;
  did_hash_mismatch_ = did_hash_mismatch;
  if (extension_id == id_to_wait_for_) {
    DCHECK(loop_runner_);
    loop_runner_->Quit();
  }
}

// ContentHashResult ----------------------------------------------------------
ContentHashResult::ContentHashResult(
    const ExtensionId& extension_id,
    bool success,
    bool was_cancelled,
    const std::set<base::FilePath> mismatch_paths)
    : extension_id(extension_id),
      success(success),
      was_cancelled(was_cancelled),
      mismatch_paths(mismatch_paths) {}
ContentHashResult::~ContentHashResult() = default;

// ContentHashWaiter ----------------------------------------------------------
ContentHashWaiter::ContentHashWaiter()
    : reply_task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) {}
ContentHashWaiter::~ContentHashWaiter() = default;

std::unique_ptr<ContentHashResult> ContentHashWaiter::CreateAndWaitForCallback(
    ContentHash::FetchKey key,
    ContentVerifierDelegate::VerifierSourceType source_type) {
  GetExtensionFileTaskRunner()->PostTask(
      FROM_HERE,
      base::BindOnce(&ContentHashWaiter::CreateContentHash,
                     base::Unretained(this), std::move(key), source_type));
  run_loop_.Run();
  DCHECK(result_);
  return std::move(result_);
}

void ContentHashWaiter::CreatedCallback(scoped_refptr<ContentHash> content_hash,
                                        bool was_cancelled) {
  if (!reply_task_runner_->RunsTasksInCurrentSequence()) {
    reply_task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(&ContentHashWaiter::CreatedCallback,
                       base::Unretained(this), content_hash, was_cancelled));
    return;
  }

  DCHECK(content_hash);
  result_ = std::make_unique<ContentHashResult>(
      content_hash->extension_id(), content_hash->succeeded(), was_cancelled,
      content_hash->hash_mismatch_unix_paths());

  run_loop_.QuitWhenIdle();
}

void ContentHashWaiter::CreateContentHash(
    ContentHash::FetchKey key,
    ContentVerifierDelegate::VerifierSourceType source_type) {
  ContentHash::Create(std::move(key), source_type, IsCancelledCallback(),
                      base::BindOnce(&ContentHashWaiter::CreatedCallback,
                                     base::Unretained(this)));
}

namespace content_verifier_test_utils {

// TestExtensionBuilder -------------------------------------------------------
TestExtensionBuilder::TestExtensionBuilder()
    // We have to provide explicit extension id in verified_contents.json.
    : TestExtensionBuilder(ExtensionId(32, 'a')) {}

TestExtensionBuilder::TestExtensionBuilder(const ExtensionId& extension_id)
    : extension_id_(extension_id) {
  base::CreateDirectory(extension_dir_.UnpackedPath().Append(kMetadataFolder));
}

TestExtensionBuilder::~TestExtensionBuilder() = default;

void TestExtensionBuilder::WriteManifest() {
  extension_dir_.WriteManifest(base::Value::Dict()
                                   .Set("manifest_version", 2)
                                   .Set("name", "Test extension")
                                   .Set("version", "1.0"));
}

void TestExtensionBuilder::WriteResource(
    base::FilePath::StringType relative_path,
    std::string contents) {
  extension_dir_.WriteFile(relative_path, contents);
  AddResource(std::move(relative_path), std::move(contents));
}

void TestExtensionBuilder::AddResource(base::FilePath::StringType relative_path,
                                       std::string contents) {
  extension_resources_.emplace_back(base::FilePath(std::move(relative_path)),
                                    std::move(contents));
}

void TestExtensionBuilder::WriteComputedHashes() {
  int block_size = extension_misc::kContentVerificationDefaultBlockSize;
  ComputedHashes::Data computed_hashes_data;

  for (const auto& resource : extension_resources_) {
    std::vector<std::string> hashes =
        ComputedHashes::GetHashesForContent(resource.contents, block_size);
    computed_hashes_data.Add(resource.relative_path, block_size, hashes);
  }

  ASSERT_TRUE(ComputedHashes(std::move(computed_hashes_data))
                  .WriteToFile(file_util::GetComputedHashesPath(
                      extension_dir_.UnpackedPath())));
}

std::string TestExtensionBuilder::CreateVerifiedContents() const {
  std::unique_ptr<base::Value> payload = CreateVerifiedContentsPayload();
  std::string payload_value;
  if (!base::JSONWriter::Write(*payload, &payload_value))
    return "";

  std::string payload_b64;
  base::Base64UrlEncode(
      payload_value, base::Base64UrlEncodePolicy::OMIT_PADDING, &payload_b64);

  auto signature =
      crypto::sign::Sign(crypto::sign::SignatureKind::RSA_PKCS1_SHA256,
                         crypto::test::FixedRsa2048PrivateKeyForTesting(),
                         base::as_byte_span("." + payload_b64));

  std::string signature_b64;
  base::Base64UrlEncode(base::as_byte_span(signature),
                        base::Base64UrlEncodePolicy::OMIT_PADDING,
                        &signature_b64);

  base::Value::List signatures = base::Value::List().Append(
      base::Value::Dict()
          .Set("header", base::Value::Dict().Set("kid", "webstore"))
          .Set("protected", "")
          .Set("signature", signature_b64));
  base::Value::List verified_contents = base::Value::List().Append(
      base::Value::Dict()
          .Set("description", "treehash per file")
          .Set("signed_content",
               base::Value::Dict()
                   .Set("payload", payload_b64)
                   .Set("signatures", std::move(signatures))));

  std::string json;
  if (!base::JSONWriter::Write(verified_contents, &json)) {
    return "";
  }

  return json;
}

void TestExtensionBuilder::WriteVerifiedContents() {
  std::string verified_contents = CreateVerifiedContents();
  ASSERT_NE(verified_contents.size(), 0u);

  base::FilePath verified_contents_path =
      file_util::GetVerifiedContentsPath(extension_dir_.UnpackedPath());
  ASSERT_TRUE(base::WriteFile(verified_contents_path, verified_contents));
}

std::vector<uint8_t> TestExtensionBuilder::GetTestContentVerifierPublicKey()
    const {
  auto key = crypto::test::FixedRsa2048PrivateKeyForTesting();
  return key.ToSubjectPublicKeyInfo();
}

std::unique_ptr<base::Value>
TestExtensionBuilder::CreateVerifiedContentsPayload() const {
  int block_size = extension_misc::kContentVerificationDefaultBlockSize;

  base::Value::List files;
  for (const auto& resource : extension_resources_) {
    std::string path = base::FilePath(resource.relative_path).AsUTF8Unsafe();
    std::string tree_hash =
        ContentHash::ComputeTreeHashForContent(resource.contents, block_size);

    std::string tree_hash_b64;
    base::Base64UrlEncode(tree_hash, base::Base64UrlEncodePolicy::OMIT_PADDING,
                          &tree_hash_b64);

    files.Append(
        base::Value::Dict().Set("path", path).Set("root_hash", tree_hash_b64));
  }

  base::Value::Dict result =
      base::Value::Dict()
          .Set("item_id", extension_id_)
          .Set("item_version", "1.0")
          .Set("content_hashes", base::Value::List().Append(
                                     base::Value::Dict()
                                         .Set("format", "treehash")
                                         .Set("block_size", block_size)
                                         .Set("hash_block_size", block_size)
                                         .Set("files", std::move(files))));

  return std::make_unique<base::Value>(std::move(result));
}

// Other stuff ----------------------------------------------------------------
scoped_refptr<Extension> UnzipToDirAndLoadExtension(
    const base::FilePath& extension_zip,
    const base::FilePath& unzip_dir) {
  if (!zip::Unzip(extension_zip, unzip_dir)) {
    ADD_FAILURE() << "Failed to unzip path.";
    return nullptr;
  }
  std::u16string error;
  scoped_refptr<Extension> extension = file_util::LoadExtension(
      unzip_dir, mojom::ManifestLocation::kInternal, 0 /* flags */, &error);
  EXPECT_NE(nullptr, extension.get()) << " error:'" << error << "'";
  return extension;
}

}  // namespace content_verifier_test_utils

}  // namespace extensions