Commit d2ef8ba2 authored by Oleg Davydov's avatar Oleg Davydov Committed by Commit Bot

Add separate unit test for ContentHash

ContentHash class was previously tested indirectly: in a more
integration-like approach, for example, from ContentVerifyJobUnittest
and slightly from ContentHashFetcherTest. This commit adds its own test
for ContentHash.

This will be used in https://crbug.com/958794 to test ContentHash work
with unsigned hashes.

Bug: 958794, 796395
Change-Id: Ica75dac2a48250f12d5729126ecf397a8168061e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1844966
Commit-Queue: Oleg Davydov <burunduk@chromium.org>
Reviewed-by: default avatarDevlin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#706885}
parent 36f5c1a8
...@@ -615,6 +615,7 @@ source_set("unit_tests") { ...@@ -615,6 +615,7 @@ source_set("unit_tests") {
"computed_hashes_unittest.cc", "computed_hashes_unittest.cc",
"content_hash_fetcher_unittest.cc", "content_hash_fetcher_unittest.cc",
"content_hash_tree_unittest.cc", "content_hash_tree_unittest.cc",
"content_verifier/content_hash_unittest.cc",
"content_verifier_unittest.cc", "content_verifier_unittest.cc",
"content_verify_job_unittest.cc", "content_verify_job_unittest.cc",
"error_map_unittest.cc", "error_map_unittest.cc",
......
...@@ -79,10 +79,12 @@ class ContentHashFetcherTest : public ExtensionsTest { ...@@ -79,10 +79,12 @@ class ContentHashFetcherTest : public ExtensionsTest {
url_loader_factory_ptr.PassInterface(); url_loader_factory_ptr.PassInterface();
std::unique_ptr<ContentHashResult> result = std::unique_ptr<ContentHashResult> result =
ContentHashWaiter().CreateAndWaitForCallback(ContentHash::FetchKey( ContentHashWaiter().CreateAndWaitForCallback(
extension_->id(), extension_->path(), extension_->version(), ContentHash::FetchKey(extension_->id(), extension_->path(),
std::move(url_loader_factory_ptr_info), fetch_url_, extension_->version(),
delegate_->GetPublicKey())); std::move(url_loader_factory_ptr_info),
fetch_url_, delegate_->GetPublicKey()),
ContentVerifierDelegate::VerifierSourceType::SIGNED_HASHES);
delegate_.reset(); delegate_.reset();
......
...@@ -288,7 +288,9 @@ class ContentVerifier::HashHelper { ...@@ -288,7 +288,9 @@ class ContentVerifier::HashHelper {
const IsCancelledCallback& is_cancelled, const IsCancelledCallback& is_cancelled,
ContentHash::CreatedCallback created_callback) { ContentHash::CreatedCallback created_callback) {
ContentHash::Create( ContentHash::Create(
std::move(fetch_key), is_cancelled, std::move(fetch_key),
ContentVerifierDelegate::VerifierSourceType::SIGNED_HASHES,
is_cancelled,
base::BindOnce(&HashHelper::ForwardToIO, std::move(created_callback))); base::BindOnce(&HashHelper::ForwardToIO, std::move(created_callback)));
} }
......
...@@ -83,9 +83,14 @@ ContentHash::FetchKey& ContentHash::FetchKey::operator=( ...@@ -83,9 +83,14 @@ ContentHash::FetchKey& ContentHash::FetchKey::operator=(
ContentHash::FetchKey&& other) = default; ContentHash::FetchKey&& other) = default;
// static // static
void ContentHash::Create(FetchKey key, void ContentHash::Create(
const IsCancelledCallback& is_cancelled, FetchKey key,
CreatedCallback created_callback) { ContentVerifierDelegate::VerifierSourceType source_type,
const IsCancelledCallback& is_cancelled,
CreatedCallback created_callback) {
// TODO(https://crbug.com/958794): Add support for unsigned hashes.
DCHECK_EQ(ContentVerifierDelegate::VerifierSourceType::SIGNED_HASHES,
source_type);
// Step 1/2: verified_contents.json: // Step 1/2: verified_contents.json:
std::unique_ptr<VerifiedContents> verified_contents = GetVerifiedContents( std::unique_ptr<VerifiedContents> verified_contents = GetVerifiedContents(
key, key,
...@@ -136,6 +141,14 @@ const ComputedHashes::Reader& ContentHash::computed_hashes() const { ...@@ -136,6 +141,14 @@ const ComputedHashes::Reader& ContentHash::computed_hashes() const {
return *computed_hashes_; return *computed_hashes_;
} }
// static
std::string ContentHash::ComputeTreeHashForContent(const std::string& contents,
int block_size) {
std::vector<std::string> hashes;
ComputedHashes::ComputeHashesForContent(contents, block_size, &hashes);
return ComputeTreeHashRoot(hashes, block_size / crypto::kSHA256Length);
}
ContentHash::ContentHash( ContentHash::ContentHash(
const ExtensionId& id, const ExtensionId& id,
const base::FilePath& root, const base::FilePath& root,
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
#include "base/version.h" #include "base/version.h"
#include "extensions/browser/computed_hashes.h" #include "extensions/browser/computed_hashes.h"
#include "extensions/browser/content_verifier/content_verifier_key.h" #include "extensions/browser/content_verifier/content_verifier_key.h"
#include "extensions/browser/content_verifier_delegate.h"
#include "extensions/browser/verified_contents.h" #include "extensions/browser/verified_contents.h"
#include "extensions/common/constants.h" #include "extensions/common/constants.h"
#include "extensions/common/extension_id.h" #include "extensions/common/extension_id.h"
...@@ -103,6 +104,7 @@ class ContentHash : public base::RefCountedThreadSafe<ContentHash> { ...@@ -103,6 +104,7 @@ class ContentHash : public base::RefCountedThreadSafe<ContentHash> {
base::OnceCallback<void(scoped_refptr<ContentHash> hash, base::OnceCallback<void(scoped_refptr<ContentHash> hash,
bool was_cancelled)>; bool was_cancelled)>;
static void Create(FetchKey key, static void Create(FetchKey key,
ContentVerifierDelegate::VerifierSourceType source_type,
const IsCancelledCallback& is_cancelled, const IsCancelledCallback& is_cancelled,
CreatedCallback created_callback); CreatedCallback created_callback);
...@@ -140,6 +142,9 @@ class ContentHash : public base::RefCountedThreadSafe<ContentHash> { ...@@ -140,6 +142,9 @@ class ContentHash : public base::RefCountedThreadSafe<ContentHash> {
!did_attempt_creating_computed_hashes_; !did_attempt_creating_computed_hashes_;
} }
static std::string ComputeTreeHashForContent(const std::string& contents,
int block_size);
private: private:
friend class base::RefCountedThreadSafe<ContentHash>; friend class base::RefCountedThreadSafe<ContentHash>;
......
// Copyright 2019 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/content_verifier/content_hash.h"
#include "base/base64url.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/json/json_writer.h"
#include "crypto/rsa_private_key.h"
#include "crypto/sha2.h"
#include "crypto/signature_creator.h"
#include "extensions/browser/computed_hashes.h"
#include "extensions/browser/content_hash_tree.h"
#include "extensions/browser/content_verifier/test_utils.h"
#include "extensions/browser/content_verifier_delegate.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extensions_test.h"
#include "extensions/browser/verified_contents.h"
#include "extensions/common/constants.h"
#include "extensions/common/file_util.h"
#include "extensions/common/value_builder.h"
#include "extensions/test/test_extension_dir.h"
namespace extensions {
// Helper class to create directory with extension files, including signed
// hashes for content verification.
class TestExtensionBuilder {
public:
TestExtensionBuilder()
: test_content_verifier_key_(crypto::RSAPrivateKey::Create(2048)),
// We have to provide explicit extension id in verified_contents.json.
extension_id_(32, 'a') {
base::CreateDirectory(
extension_dir_.UnpackedPath().Append(kMetadataFolder));
}
void WriteManifest() {
extension_dir_.WriteManifest(DictionaryBuilder()
.Set("manifest_version", 2)
.Set("name", "Test extension")
.Set("version", "1.0")
.ToJSON());
}
void WriteResource(base::FilePath::StringType relative_path,
std::string contents) {
extension_dir_.WriteFile(relative_path, contents);
extension_resources_.emplace_back(base::FilePath(std::move(relative_path)),
std::move(contents));
}
void WriteComputedHashes() {
int block_size = extension_misc::kContentVerificationDefaultBlockSize;
ComputedHashes::Writer computed_hashes_writer;
for (const auto& resource : extension_resources_) {
std::vector<std::string> hashes;
ComputedHashes::ComputeHashesForContent(resource.contents, block_size,
&hashes);
computed_hashes_writer.AddHashes(resource.relative_path, block_size,
hashes);
}
ASSERT_TRUE(computed_hashes_writer.WriteToFile(
file_util::GetComputedHashesPath(extension_dir_.UnpackedPath())));
}
void WriteVerifiedContents() {
std::unique_ptr<base::Value> payload = CreateVerifiedContents();
std::string payload_value;
ASSERT_TRUE(base::JSONWriter::Write(*payload, &payload_value));
std::string payload_b64;
base::Base64UrlEncode(
payload_value, base::Base64UrlEncodePolicy::OMIT_PADDING, &payload_b64);
std::string signature_sha256 = crypto::SHA256HashString("." + payload_b64);
std::vector<uint8_t> signature_source(signature_sha256.begin(),
signature_sha256.end());
std::vector<uint8_t> signature_value;
ASSERT_TRUE(crypto::SignatureCreator::Sign(
test_content_verifier_key_.get(), crypto::SignatureCreator::SHA256,
signature_source.data(), signature_source.size(), &signature_value));
std::string signature_b64;
base::Base64UrlEncode(
std::string(signature_value.begin(), signature_value.end()),
base::Base64UrlEncodePolicy::OMIT_PADDING, &signature_b64);
std::unique_ptr<base::Value> signatures =
ListBuilder()
.Append(DictionaryBuilder()
.Set("header",
DictionaryBuilder().Set("kid", "webstore").Build())
.Set("protected", "")
.Set("signature", signature_b64)
.Build())
.Build();
std::unique_ptr<base::Value> verified_contents =
ListBuilder()
.Append(DictionaryBuilder()
.Set("description", "treehash per file")
.Set("signed_content",
DictionaryBuilder()
.Set("payload", payload_b64)
.Set("signatures", std::move(signatures))
.Build())
.Build())
.Build();
std::string json;
ASSERT_TRUE(base::JSONWriter::Write(*verified_contents, &json));
base::FilePath verified_contents_path =
file_util::GetVerifiedContentsPath(extension_dir_.UnpackedPath());
ASSERT_EQ(
static_cast<int>(json.size()),
base::WriteFile(verified_contents_path, json.data(), json.size()));
}
std::vector<uint8_t> GetTestContentVerifierPublicKey() {
std::vector<uint8_t> public_key;
test_content_verifier_key_->ExportPublicKey(&public_key);
return public_key;
}
base::FilePath extension_path() const {
return extension_dir_.UnpackedPath();
}
const ExtensionId& extension_id() const { return extension_id_; }
private:
struct ExtensionResource {
ExtensionResource(base::FilePath relative_path, std::string contents)
: relative_path(std::move(relative_path)),
contents(std::move(contents)) {}
base::FilePath relative_path;
std::string contents;
};
std::unique_ptr<base::Value> CreateVerifiedContents() {
int block_size = extension_misc::kContentVerificationDefaultBlockSize;
ListBuilder files;
for (const auto& resource : extension_resources_) {
base::FilePath::StringType path =
VerifiedContents::NormalizeResourcePath(resource.relative_path);
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(DictionaryBuilder()
.Set("path", path)
.Set("root_hash", tree_hash_b64)
.Build());
}
return DictionaryBuilder()
.Set("item_id", extension_id_)
.Set("item_version", "1.0")
.Set("content_hashes",
ListBuilder()
.Append(DictionaryBuilder()
.Set("format", "treehash")
.Set("block_size", block_size)
.Set("hash_block_size", block_size)
.Set("files", files.Build())
.Build())
.Build())
.Build();
}
std::unique_ptr<crypto::RSAPrivateKey> test_content_verifier_key_;
ExtensionId extension_id_;
std::vector<ExtensionResource> extension_resources_;
TestExtensionDir extension_dir_;
DISALLOW_COPY_AND_ASSIGN(TestExtensionBuilder);
};
class ContentHashUnittest : public ExtensionsTest {
protected:
ContentHashUnittest() = default;
std::unique_ptr<ContentHashResult> CreateContentHash(
Extension* extension,
ContentVerifierDelegate::VerifierSourceType source_type,
const std::vector<uint8_t>& content_verifier_public_key) {
ContentHash::FetchKey key(
extension->id(), extension->path(), extension->version(),
nullptr /* url_loader_factory_ptr_info */, GURL() /* fetch_url */,
content_verifier_public_key);
return ContentHashWaiter().CreateAndWaitForCallback(std::move(key),
source_type);
}
scoped_refptr<Extension> LoadExtension(const TestExtensionBuilder& builder) {
std::string error;
scoped_refptr<Extension> extension = file_util::LoadExtension(
builder.extension_path(), builder.extension_id(), Manifest::INTERNAL,
0 /* flags */, &error);
if (!extension)
ADD_FAILURE() << " error:'" << error << "'";
return extension;
}
};
TEST_F(ContentHashUnittest, ExtensionWithSignedHashes) {
TestExtensionBuilder builder;
builder.WriteManifest();
builder.WriteResource(FILE_PATH_LITERAL("background.js"),
"console.log('Nothing special');");
builder.WriteVerifiedContents();
scoped_refptr<Extension> extension = LoadExtension(builder);
ASSERT_NE(nullptr, extension);
std::unique_ptr<ContentHashResult> result = CreateContentHash(
extension.get(),
ContentVerifierDelegate::VerifierSourceType::SIGNED_HASHES,
builder.GetTestContentVerifierPublicKey());
DCHECK(result);
EXPECT_TRUE(result->success);
}
} // namespace extensions
...@@ -221,10 +221,12 @@ ContentHashWaiter::ContentHashWaiter() ...@@ -221,10 +221,12 @@ ContentHashWaiter::ContentHashWaiter()
ContentHashWaiter::~ContentHashWaiter() = default; ContentHashWaiter::~ContentHashWaiter() = default;
std::unique_ptr<ContentHashResult> ContentHashWaiter::CreateAndWaitForCallback( std::unique_ptr<ContentHashResult> ContentHashWaiter::CreateAndWaitForCallback(
ContentHash::FetchKey key) { ContentHash::FetchKey key,
ContentVerifierDelegate::VerifierSourceType source_type) {
GetExtensionFileTaskRunner()->PostTask( GetExtensionFileTaskRunner()->PostTask(
FROM_HERE, base::BindOnce(&ContentHashWaiter::CreateContentHash, FROM_HERE,
base::Unretained(this), std::move(key))); base::BindOnce(&ContentHashWaiter::CreateContentHash,
base::Unretained(this), std::move(key), source_type));
run_loop_.Run(); run_loop_.Run();
DCHECK(result_); DCHECK(result_);
return std::move(result_); return std::move(result_);
...@@ -248,8 +250,11 @@ void ContentHashWaiter::CreatedCallback(scoped_refptr<ContentHash> content_hash, ...@@ -248,8 +250,11 @@ void ContentHashWaiter::CreatedCallback(scoped_refptr<ContentHash> content_hash,
run_loop_.QuitWhenIdle(); run_loop_.QuitWhenIdle();
} }
void ContentHashWaiter::CreateContentHash(ContentHash::FetchKey key) { void ContentHashWaiter::CreateContentHash(
ContentHash::Create(std::move(key), ContentHash::IsCancelledCallback(), ContentHash::FetchKey key,
ContentVerifierDelegate::VerifierSourceType source_type) {
ContentHash::Create(std::move(key), source_type,
ContentHash::IsCancelledCallback(),
base::BindOnce(&ContentHashWaiter::CreatedCallback, base::BindOnce(&ContentHashWaiter::CreatedCallback,
base::Unretained(this))); base::Unretained(this)));
} }
......
...@@ -176,13 +176,16 @@ class ContentHashWaiter { ...@@ -176,13 +176,16 @@ class ContentHashWaiter {
~ContentHashWaiter(); ~ContentHashWaiter();
std::unique_ptr<ContentHashResult> CreateAndWaitForCallback( std::unique_ptr<ContentHashResult> CreateAndWaitForCallback(
ContentHash::FetchKey key); ContentHash::FetchKey key,
ContentVerifierDelegate::VerifierSourceType source_type);
private: private:
void CreatedCallback(scoped_refptr<ContentHash> content_hash, void CreatedCallback(scoped_refptr<ContentHash> content_hash,
bool was_cancelled); bool was_cancelled);
void CreateContentHash(ContentHash::FetchKey key); void CreateContentHash(
ContentHash::FetchKey key,
ContentVerifierDelegate::VerifierSourceType source_type);
scoped_refptr<base::SequencedTaskRunner> reply_task_runner_; scoped_refptr<base::SequencedTaskRunner> reply_task_runner_;
base::RunLoop run_loop_; base::RunLoop run_loop_;
......
...@@ -199,8 +199,7 @@ std::unique_ptr<VerifiedContents> VerifiedContents::Create( ...@@ -199,8 +199,7 @@ std::unique_ptr<VerifiedContents> VerifiedContents::Create(
bool VerifiedContents::HasTreeHashRoot( bool VerifiedContents::HasTreeHashRoot(
const base::FilePath& relative_path) const { const base::FilePath& relative_path) const {
base::FilePath::StringType path = base::ToLowerASCII( base::FilePath::StringType path = NormalizeResourcePath(relative_path);
relative_path.NormalizePathSeparatorsTo('/').value());
if (base::Contains(root_hashes_, path)) if (base::Contains(root_hashes_, path))
return true; return true;
...@@ -215,7 +214,7 @@ bool VerifiedContents::HasTreeHashRoot( ...@@ -215,7 +214,7 @@ bool VerifiedContents::HasTreeHashRoot(
bool VerifiedContents::TreeHashRootEquals(const base::FilePath& relative_path, bool VerifiedContents::TreeHashRootEquals(const base::FilePath& relative_path,
const std::string& expected) const { const std::string& expected) const {
base::FilePath::StringType normalized_relative_path = base::FilePath::StringType normalized_relative_path =
base::ToLowerASCII(relative_path.NormalizePathSeparatorsTo('/').value()); NormalizeResourcePath(relative_path);
if (TreeHashRootEqualsImpl(normalized_relative_path, expected)) if (TreeHashRootEqualsImpl(normalized_relative_path, expected))
return true; return true;
...@@ -229,6 +228,13 @@ bool VerifiedContents::TreeHashRootEquals(const base::FilePath& relative_path, ...@@ -229,6 +228,13 @@ bool VerifiedContents::TreeHashRootEquals(const base::FilePath& relative_path,
return false; return false;
} }
// static
base::FilePath::StringType VerifiedContents::NormalizeResourcePath(
const base::FilePath& relative_path) {
return base::ToLowerASCII(
relative_path.NormalizePathSeparatorsTo('/').value());
}
// We're loosely following the "JSON Web Signature" draft spec for signing // We're loosely following the "JSON Web Signature" draft spec for signing
// a JSON payload: // a JSON payload:
// //
......
...@@ -49,6 +49,9 @@ class VerifiedContents { ...@@ -49,6 +49,9 @@ class VerifiedContents {
// signature" mode, this can return false. // signature" mode, this can return false.
bool valid_signature() { return valid_signature_; } bool valid_signature() { return valid_signature_; }
static base::FilePath::StringType NormalizeResourcePath(
const base::FilePath& relative_path);
private: private:
// Note: the public_key must remain valid for the lifetime of this object. // Note: the public_key must remain valid for the lifetime of this object.
explicit VerifiedContents(base::span<const uint8_t> public_key); explicit VerifiedContents(base::span<const uint8_t> public_key);
......
...@@ -5,10 +5,7 @@ ...@@ -5,10 +5,7 @@
#include "extensions/test/test_extension_dir.h" #include "extensions/test/test_extension_dir.h"
#include "base/files/file_util.h" #include "base/files/file_util.h"
#include "base/json/json_writer.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_util.h" #include "base/strings/string_util.h"
#include "base/test/values_test_util.h"
#include "base/threading/thread_restrictions.h" #include "base/threading/thread_restrictions.h"
#include "extensions/browser/extension_creator.h" #include "extensions/browser/extension_creator.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
...@@ -71,7 +68,7 @@ base::FilePath TestExtensionDir::Pack() { ...@@ -71,7 +68,7 @@ base::FilePath TestExtensionDir::Pack() {
return crx_path; return crx_path;
} }
base::FilePath TestExtensionDir::UnpackedPath() { base::FilePath TestExtensionDir::UnpackedPath() const {
base::ScopedAllowBlockingForTesting allow_blocking; base::ScopedAllowBlockingForTesting allow_blocking;
// We make this absolute because it's possible that dir_ contains a symlink as // We make this absolute because it's possible that dir_ contains a symlink as
// part of it's path. When UnpackedInstaller::GetAbsolutePath() runs as part // part of it's path. When UnpackedInstaller::GetAbsolutePath() runs as part
......
...@@ -39,7 +39,7 @@ class TestExtensionDir { ...@@ -39,7 +39,7 @@ class TestExtensionDir {
base::FilePath Pack(); base::FilePath Pack();
// Returns the path to the unpacked directory. // Returns the path to the unpacked directory.
base::FilePath UnpackedPath(); base::FilePath UnpackedPath() const;
private: private:
// Stores files that make up the extension. // Stores files that make up the extension.
......
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