| // Copyright 2018 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 <memory> |
| #include <string> |
| |
| #include "base/bind.h" |
| #include "base/macros.h" |
| #include "base/test/bind_test_util.h" |
| #include "chrome/browser/extensions/browsertest_util.h" |
| #include "chrome/browser/extensions/chrome_content_verifier_delegate.h" |
| #include "chrome/browser/extensions/extension_browsertest.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/public/test/url_loader_interceptor.h" |
| #include "extensions/browser/computed_hashes.h" |
| #include "extensions/browser/content_verifier/test_utils.h" |
| #include "extensions/browser/extension_file_task_runner.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/test_extension_registry_observer.h" |
| #include "extensions/common/file_util.h" |
| #include "services/network/public/cpp/features.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // Specifies the content verification mode. |
| enum ContentVerificationMode { |
| // Uses ContentVerifierDelegate::ENFORCE mode. |
| kEnforce, |
| // Uses ContentVerifierDelegate::ENFORCE_STRICT mode. |
| kEnforceStrict |
| }; |
| |
| } // namespace |
| |
| // Tests content verification's hash fetch behavior and its implication on |
| // verification failure in different verification modes (enforce and |
| // enforce_strict). |
| // TODO(lazyboy): Add assertions for checking verified_contents.json file's |
| // validity after running each test. |
| class ContentVerifierHashTest |
| : public ExtensionBrowserTest, |
| public testing::WithParamInterface<ContentVerificationMode> { |
| public: |
| ContentVerifierHashTest() = default; |
| ~ContentVerifierHashTest() override {} |
| |
| enum TamperResourceType { |
| kTamperRequestedResource, |
| kTamperNotRequestedResource |
| }; |
| |
| // ExtensionBrowserTest: |
| bool ShouldEnableContentVerification() override { return true; } |
| |
| void SetUp() override { |
| // Override content verification mode before ExtensionSystemImpl initializes |
| // ChromeContentVerifierDelegate. |
| ChromeContentVerifierDelegate::SetDefaultModeForTesting( |
| uses_enforce_strict_mode() ? ContentVerifierDelegate::ENFORCE_STRICT |
| : ContentVerifierDelegate::ENFORCE); |
| |
| ExtensionBrowserTest::SetUp(); |
| } |
| |
| void TearDown() override { |
| ExtensionBrowserTest::TearDown(); |
| ChromeContentVerifierDelegate::SetDefaultModeForTesting(base::nullopt); |
| } |
| |
| void TearDownOnMainThread() override { |
| url_loader_interceptor_.reset(); |
| ExtensionBrowserTest::TearDownOnMainThread(); |
| } |
| |
| bool uses_enforce_strict_mode() { |
| return GetParam() == ContentVerificationMode::kEnforceStrict; |
| } |
| bool uses_enforce_mode() { |
| return GetParam() == ContentVerificationMode::kEnforce; |
| } |
| |
| void DisableHashFetching() { hash_fetching_disabled_ = true; } |
| |
| testing::AssertionResult InstallDefaultResourceExtension() { |
| LOG(INFO) << "InstallDefaultResourceExtension"; |
| return InstallExtension(kHasDefaultResource); |
| } |
| testing::AssertionResult InstallNoDefaultResourceExtension() { |
| LOG(INFO) << "InstallNoDefaultResourceExtension"; |
| return InstallExtension(kDoesNotHaveDefaultResource); |
| } |
| |
| void DisableExtension() { ExtensionBrowserTest::DisableExtension(id()); } |
| |
| testing::AssertionResult DeleteVerifiedContents() { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| |
| base::FilePath verified_contents_path = |
| file_util::GetVerifiedContentsPath(info_->extension_root); |
| if (!base::PathExists(verified_contents_path)) { |
| return testing::AssertionFailure() |
| << "Could not find verified_contents.json."; |
| } |
| |
| // Delete verified_contents.json: |
| if (!base::DeleteFile(verified_contents_path, false /* recursive */)) { |
| return testing::AssertionFailure() |
| << "Could not delete verified_contents.json."; |
| } |
| return testing::AssertionSuccess(); |
| } |
| |
| bool HasComputedHashes() { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| return base::PathExists( |
| file_util::GetComputedHashesPath(info_->extension_root)); |
| } |
| |
| testing::AssertionResult DeleteComputedHashes() { |
| LOG(INFO) << "Deleting computed_hashes.json"; |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| if (!HasComputedHashes()) { |
| return testing::AssertionFailure() |
| << "Could not find computed_hashes.json for deletion. " |
| << "Make sure the previous steps created a " |
| << "computed_hashes.json, otherwise tests might fail/flake"; |
| } |
| base::FilePath computed_hashes_path = |
| file_util::GetComputedHashesPath(info_->extension_root); |
| if (!base::DeleteFile(computed_hashes_path, false /* recursive */)) { |
| return testing::AssertionFailure() |
| << "Error deleting computed_hashes.json."; |
| } |
| return testing::AssertionSuccess(); |
| } |
| |
| testing::AssertionResult TamperComputedHashes() { |
| LOG(INFO) << "Tampering computed_hashes.json"; |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| if (!HasComputedHashes()) { |
| return testing::AssertionFailure() |
| << "Could not find computed_hashes.json for tampering."; |
| } |
| base::FilePath computed_hashes_path = |
| file_util::GetComputedHashesPath(info_->extension_root); |
| std::string extra = R"({hello:"world"})"; |
| if (!base::AppendToFile(computed_hashes_path, extra.data(), extra.size())) { |
| return testing::AssertionFailure() |
| << "Could not tamper computed_hashes.json"; |
| } |
| return testing::AssertionSuccess(); |
| } |
| |
| testing::AssertionResult TamperResource(TamperResourceType type) { |
| const std::string resource_to_tamper = |
| type == kTamperRequestedResource ? "background.js" : "script.js"; |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| // Modify content of a resource if this test requested that, time the |
| // extension loads, hash fetch will discover content verification failure |
| // due to hash mismatch. |
| std::string extra = "some_extra_function_call();"; |
| base::FilePath real_path = |
| info_->extension_root.AppendASCII(resource_to_tamper); |
| if (!base::AppendToFile(real_path, extra.data(), extra.size())) { |
| return testing::AssertionFailure() |
| << "Could not tamper " << resource_to_tamper << "."; |
| } |
| return testing::AssertionSuccess(); |
| } |
| |
| const ExtensionId& id() const { return info_->extension_id; } |
| |
| void EnableExtensionAndWaitForCompletion(bool expect_disabled) { |
| LOG(INFO) << "EnableExtensionAndWaitForCompletion: expect_disabled = " |
| << expect_disabled; |
| // Only observe ContentVerifyJob when necessary. This is because |
| // ContentVerifyJob's callback and ContentVerifyJob::OnExtensionLoad's |
| // callbacks can be race-y. |
| std::unique_ptr<TestContentVerifyJobObserver> job_observer; |
| const bool needs_to_observe_content_verify_job = |
| // If the test wouldn't disable the extension, extensions with |
| // default resource(s) will always see at at least one ContentVerifyJob |
| // to a default resource (background.js). |
| info_->type == kHasDefaultResource && !expect_disabled; |
| |
| if (needs_to_observe_content_verify_job) { |
| LOG(INFO) << "Observing ContentVerifyJob"; |
| job_observer = std::make_unique<TestContentVerifyJobObserver>(); |
| using Result = TestContentVerifyJobObserver::Result; |
| job_observer->ExpectJobResult( |
| id(), base::FilePath(FILE_PATH_LITERAL("background.js")), |
| Result::SUCCESS); |
| } |
| |
| TestExtensionRegistryObserver registry_observer( |
| ExtensionRegistry::Get(profile()), id()); |
| VerifierObserver verifier_observer; |
| { |
| EnableExtension(id()); |
| registry_observer.WaitForExtensionLoaded(); |
| } |
| if (!base::ContainsKey(verifier_observer.completed_fetches(), id())) |
| verifier_observer.WaitForFetchComplete(id()); |
| LOG(INFO) << "Verifier observer has seen FetchComplete"; |
| |
| if (job_observer) { |
| LOG(INFO) << "ContentVerifyJobObserver, wait for expected job"; |
| job_observer->WaitForExpectedJobs(); |
| LOG(INFO) << "ContentVerifyJobObserver, completed expected job"; |
| } |
| } |
| |
| bool ExtensionIsDisabledForCorruption() { |
| const Extension* extension = extensions::ExtensionRegistry::Get(profile()) |
| ->disabled_extensions() |
| .GetByID(id()); |
| if (!extension) |
| return false; |
| |
| ExtensionPrefs* prefs = ExtensionPrefs::Get(profile()); |
| // Make sure the extension got disabled due to corruption (and only due to |
| // corruption). |
| int reasons = prefs->GetDisableReasons(id()); |
| return reasons == disable_reason::DISABLE_CORRUPTED; |
| } |
| |
| bool ExtensionIsEnabled() { |
| return extensions::ExtensionRegistry::Get(profile()) |
| ->enabled_extensions() |
| .Contains(id()); |
| } |
| |
| bool HasValidComputedHashes() { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| ComputedHashes::Reader reader; |
| return reader.InitFromFile( |
| file_util::GetComputedHashesPath(info_->extension_root)); |
| } |
| |
| bool HasValidVerifiedContents() { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::FilePath verified_contents_path = |
| file_util::GetVerifiedContentsPath(info_->extension_root); |
| if (!base::PathExists(verified_contents_path)) { |
| ADD_FAILURE() << "Could not find verified_contents.json."; |
| return false; |
| } |
| std::string contents; |
| if (!base::ReadFileToString(verified_contents_path, &contents)) { |
| ADD_FAILURE() << "Could not read verified_contents.json."; |
| return false; |
| } |
| return verified_contents_contents_ == contents; |
| } |
| |
| private: |
| enum ExtensionType { |
| // An extension (has_default_resource.crx) that by default requests a |
| // resource in it during ExtensionLoad. |
| kHasDefaultResource, |
| // An extension (no_default_resources.crx) that doesn't request any |
| // resource during ExtensionLoad. |
| kDoesNotHaveDefaultResource |
| }; |
| |
| struct ExtensionInfo { |
| ExtensionId extension_id; |
| base::FilePath extension_root; |
| base::Version version; |
| ExtensionType type; |
| |
| ExtensionInfo(const ExtensionId& extension_id, |
| const base::FilePath& extension_root, |
| const base::Version& version, |
| ExtensionType type) |
| : extension_id(extension_id), |
| extension_root(extension_root), |
| version(version), |
| type(type) {} |
| ExtensionInfo(const Extension* extension, ExtensionType type) |
| : ExtensionInfo(extension->id(), |
| extension->path(), |
| extension->version(), |
| type) {} |
| }; |
| |
| // Stores verified_contents.json into a temp file. |
| bool CopyVerifiedContents(base::FilePath* out_path) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| if (!temp_dir_.CreateUniqueTempDir()) { |
| ADD_FAILURE() << "Could not create temp dir for test."; |
| return false; |
| } |
| |
| base::FilePath verified_contents_path = |
| file_util::GetVerifiedContentsPath(info_->extension_root); |
| if (!base::PathExists(verified_contents_path)) { |
| ADD_FAILURE() << "Could not find verified_contents.json for copying."; |
| return false; |
| } |
| base::FilePath destination = temp_dir_.GetPath(); |
| *out_path = destination.Append(FILE_PATH_LITERAL("copy.json")); |
| if (!base::CopyFile(verified_contents_path, *out_path)) { |
| ADD_FAILURE() << "Could not copy verified_contents.json to a temp dir."; |
| return false; |
| } |
| if (!base::ReadFileToString(verified_contents_path, |
| &verified_contents_contents_)) { |
| ADD_FAILURE() << "Could not read verified_contents.json."; |
| return false; |
| } |
| return true; |
| } |
| |
| // Installs test extension that is copied from the webstore with actual |
| // signatures. |
| testing::AssertionResult InstallExtension(ExtensionType type) { |
| // This observer will make sure content hash read and computed_hashes.json |
| // writing is complete before we proceed. |
| VerifierObserver verifier_observer; |
| |
| const std::string crx_relative_path = |
| type == kHasDefaultResource |
| ? "content_verifier/has_default_resource.crx" |
| : "content_verifier/no_default_resources.crx"; |
| // These test extension is copied from the webstore that has actual |
| // signatures. |
| const Extension* extension = InstallExtensionFromWebstore( |
| test_data_dir_.AppendASCII(crx_relative_path), 1); |
| if (!extension) { |
| return testing::AssertionFailure() |
| << "Could not install extension: " << crx_relative_path; |
| } |
| |
| const ExtensionId& extension_id = extension->id(); |
| if (!base::ContainsKey(verifier_observer.completed_fetches(), extension_id)) |
| verifier_observer.WaitForFetchComplete(extension_id); |
| |
| info_ = std::make_unique<ExtensionInfo>(extension, type); |
| |
| // Set up the interceptor functor and data needed by it. |
| if (!hash_fetching_disabled_ && !InstallInterceptor()) |
| return testing::AssertionFailure() << "Failed to install interceptor."; |
| |
| return testing::AssertionSuccess(); |
| } |
| |
| bool InstallInterceptor() { |
| if (url_loader_interceptor_) { |
| testing::AssertionFailure() << "Already created interceptor."; |
| return false; |
| } |
| |
| SetUpInterceptorData(); |
| |
| auto interceptor_function = |
| [](GURL* fetch_url, base::FilePath* file_path, |
| content::URLLoaderInterceptor::RequestParams* params) { |
| GURL url = params->url_request.url; |
| if (url == *fetch_url) { |
| base::ScopedAllowBlockingForTesting allow_io; |
| std::string contents; |
| CHECK(base::ReadFileToString(*file_path, &contents)); |
| |
| content::URLLoaderInterceptor::WriteResponse( |
| std::string(), contents, params->client.get()); |
| return true; |
| } |
| return false; |
| }; |
| url_loader_interceptor_ = std::make_unique<content::URLLoaderInterceptor>( |
| base::BindRepeating(interceptor_function, &fetch_url_, |
| &copied_verified_contents_path_)); |
| return true; |
| } |
| |
| bool SetUpInterceptorData() { |
| // Use stored copy of verified_contents.json as hash fetch response. |
| if (!CopyVerifiedContents(&copied_verified_contents_path_)) { |
| ADD_FAILURE() << "Could not copy verified_contents.json."; |
| return false; |
| } |
| |
| ExtensionSystem* system = ExtensionSystem::Get(profile()); |
| fetch_url_ = system->content_verifier()->GetSignatureFetchUrlForTest( |
| id(), info_->version); |
| |
| return true; |
| } |
| |
| std::unique_ptr<content::URLLoaderInterceptor> url_loader_interceptor_; |
| base::ScopedTempDir temp_dir_; |
| |
| base::FilePath copied_verified_contents_path_; |
| GURL fetch_url_; |
| |
| // Information about the loaded extension. |
| std::unique_ptr<ExtensionInfo> info_; |
| |
| // Contents of verified_contents.json (if available). |
| std::string verified_contents_contents_; |
| |
| bool hash_fetching_disabled_ = false; |
| |
| DISALLOW_COPY_AND_ASSIGN(ContentVerifierHashTest); |
| }; |
| |
| // Tests that corruption of a requested extension resource always disables the |
| // extension. |
| IN_PROC_BROWSER_TEST_P(ContentVerifierHashTest, |
| TamperRequestedResourceKeepComputedHashes) { |
| ASSERT_TRUE(InstallDefaultResourceExtension()); |
| |
| DisableExtension(); |
| |
| // Delete verified_contents.json to force a hash fetch on next load. |
| EXPECT_TRUE(DeleteVerifiedContents()); |
| |
| // computed_hashes.json should remain valid. |
| EXPECT_TRUE(HasComputedHashes()); |
| |
| // Tamper an extension resource that will be requested on next load. |
| EXPECT_TRUE(TamperResource(kTamperRequestedResource)); |
| |
| TestExtensionRegistryObserver registry_observer( |
| ExtensionRegistry::Get(profile()), id()); |
| // Since we tampered with the resource, content verification should |
| // disable the extension, both in "enforce_strict" and "enforce" mode. |
| EnableExtensionAndWaitForCompletion(true /* expect_disabled */); |
| EXPECT_TRUE(registry_observer.WaitForExtensionUnloaded()); |
| |
| EXPECT_TRUE(ExtensionIsDisabledForCorruption()); |
| |
| // Expect a valid verified_contents.json file at the end. |
| EXPECT_TRUE(HasValidVerifiedContents()); |
| } |
| |
| // Tests that tampering a resource that will be requested by the extension and |
| // deleting computed_hashes.json will always disable the extension. |
| IN_PROC_BROWSER_TEST_P(ContentVerifierHashTest, |
| TamperRequestedResourceDeleteComputedHashes) { |
| ASSERT_TRUE(InstallDefaultResourceExtension()); |
| |
| DisableExtension(); |
| |
| // Delete verified_contents.json to force a hash fetch on next load. |
| EXPECT_TRUE(DeleteVerifiedContents()); |
| |
| // Delete computed_hashes.json. |
| EXPECT_TRUE(DeleteComputedHashes()); |
| |
| // Tamper an extension resource that will be requested on next load. |
| EXPECT_TRUE(TamperResource(kTamperRequestedResource)); |
| |
| TestExtensionRegistryObserver registry_observer( |
| ExtensionRegistry::Get(profile()), id()); |
| // Since we tampered with the resource, content verification should |
| // disable the extension, both in "enforce_strict" and "enforce" mode. |
| EnableExtensionAndWaitForCompletion(true /* expect_disabled */); |
| EXPECT_TRUE(registry_observer.WaitForExtensionUnloaded()); |
| |
| EXPECT_TRUE(ExtensionIsDisabledForCorruption()); |
| |
| // Expect a valid computed_hashes.json file at the end, the verification |
| // failure must have used this file to detect corruption. |
| EXPECT_TRUE(HasValidComputedHashes()); |
| |
| // Expect a valid verified_contents.json file at the end. |
| EXPECT_TRUE(HasValidVerifiedContents()); |
| } |
| |
| // Tests that tampering a resource that will be requested by the extension and |
| // tampering computed_hashes.json will always disable the extension. |
| IN_PROC_BROWSER_TEST_P(ContentVerifierHashTest, |
| TamperRequestedResourceTamperComputedHashes) { |
| ASSERT_TRUE(InstallDefaultResourceExtension()); |
| |
| DisableExtension(); |
| |
| // Delete verified_contents.json to force a hash fetch on next load. |
| EXPECT_TRUE(DeleteVerifiedContents()); |
| |
| // Tamper computed_hashes.json. |
| EXPECT_TRUE(TamperComputedHashes()); |
| |
| // Tamper an extension resource that will be requested on next load. |
| EXPECT_TRUE(TamperResource(kTamperRequestedResource)); |
| |
| TestExtensionRegistryObserver registry_observer( |
| ExtensionRegistry::Get(profile()), id()); |
| // Since we tampered with the resource, content verification should |
| // disable the extension, both in "enforce_strict" and "enforce" mode. |
| EnableExtensionAndWaitForCompletion(true /* expect_disabled */); |
| EXPECT_TRUE(registry_observer.WaitForExtensionUnloaded()); |
| |
| EXPECT_TRUE(ExtensionIsDisabledForCorruption()); |
| |
| // Expect a valid computed_hashes.json file at the end, the verification |
| // failure must have used this file to detect corruption. |
| EXPECT_TRUE(HasValidComputedHashes()); |
| |
| // Expect a valid verified_contents.json file at the end. |
| EXPECT_TRUE(HasValidVerifiedContents()); |
| } |
| |
| // Tests hash fetch failure scenario with an extension that requests resource(s) |
| // by default. |
| IN_PROC_BROWSER_TEST_P(ContentVerifierHashTest, |
| FetchFailureWithDefaultResourceExtension) { |
| // Do *not* install any hash fetch interceptor, so that hash fetch after |
| // reload fails. |
| DisableHashFetching(); |
| |
| ASSERT_TRUE(InstallDefaultResourceExtension()); |
| |
| DisableExtension(); |
| |
| // Delete verified_contents.json to force a hash fetch on next load. |
| EXPECT_TRUE(DeleteVerifiedContents()); |
| |
| // computed_hashes.json and extension resource(s) aren't touched since they do |
| // not matter as hash fetch will fail. |
| |
| // In "enforce_strict" mode, hash fetch failures will cause the extension to |
| // be disabled. Implementation-wise, this happens because the requested |
| // resource's ContentVerifyJob will result in failure, not because the hash |
| // fetch failed. See https://crbug.com/819818 for details. |
| // |
| // In "enforce" mode, the extension won't be disabled. However, since we |
| // request a resource (background.js), its corresponding ContentVerifyJob will |
| // attempt to fetch hash and that job will also fail. In order to achieve |
| // determinism in this case, also observe a ContentVerifyJob that will fail. |
| const bool expect_disabled = uses_enforce_strict_mode(); |
| |
| // Similar to EnableExtensionAndWaitForCompletion, but also forces a |
| // ContentVerifyJob observer in "enforce" mode. |
| // Instead of generalizing this oddball expectation into |
| // EnableExtensionAndWaitForCompletion, provide the implementation right here |
| // in the test body. |
| { |
| LOG(INFO) << "EnableExtensionAndWaitForCompletion: expect_disabled = " |
| << expect_disabled; |
| ExtensionId extension_id = id(); |
| |
| // Only observe ContentVerifyJob when necessary. This is because |
| // ContentVerifyJob's callback and ContentVerifyJob::OnExtensionLoad's |
| // callbacks can be race-y. |
| std::unique_ptr<TestContentVerifyJobObserver> job_observer; |
| if (uses_enforce_mode()) { |
| // In "enforce" mode, we expect to see a job completion (and its failure). |
| job_observer = std::make_unique<TestContentVerifyJobObserver>(); |
| using Result = TestContentVerifyJobObserver::Result; |
| job_observer->ExpectJobResult( |
| extension_id, |
| // This extension has default resource (background.js), so it must |
| // request it. |
| base::FilePath(FILE_PATH_LITERAL("background.js")), Result::FAILURE); |
| } |
| |
| TestExtensionRegistryObserver registry_observer( |
| ExtensionRegistry::Get(profile()), extension_id); |
| VerifierObserver verifier_observer; |
| { |
| EnableExtension(extension_id); |
| registry_observer.WaitForExtensionLoaded(); |
| } |
| if (!base::ContainsKey(verifier_observer.completed_fetches(), extension_id)) |
| verifier_observer.WaitForFetchComplete(extension_id); |
| LOG(INFO) << "Verifier observer has seen FetchComplete"; |
| |
| if (job_observer) { |
| LOG(INFO) << "ContentVerifyJobObserver, wait for expected job"; |
| job_observer->WaitForExpectedJobs(); |
| LOG(INFO) << "ContentVerifyJobObserver, completed expected job"; |
| } |
| |
| if (expect_disabled) |
| EXPECT_TRUE(registry_observer.WaitForExtensionUnloaded()); |
| } |
| |
| EXPECT_TRUE(expect_disabled ? ExtensionIsDisabledForCorruption() |
| : ExtensionIsEnabled()); |
| } |
| |
| // Tests that hash fetch failure for loading an extension that doesn't request |
| // any resource by default will not be disabled. |
| IN_PROC_BROWSER_TEST_P(ContentVerifierHashTest, |
| FetchFailureWithNoDefaultResourceDoesNotDisable) { |
| // Do *not* install any hash fetch interceptor, so that hash fetch after |
| // reload fails. |
| DisableHashFetching(); |
| |
| ASSERT_TRUE(InstallNoDefaultResourceExtension()); |
| |
| DisableExtension(); |
| |
| // Delete verified_contents.json to force a hash fetch on next load. |
| EXPECT_TRUE(DeleteVerifiedContents()); |
| |
| // computed_hashes.json and extension resource(s) aren't touched since they do |
| // not matter as hash fetch will fail. |
| |
| // If the extension didn't explicitly request any resources, then there will |
| // not be any content verification failures. |
| const bool expect_disabled = false; |
| // TODO(lazyboy): https://crbug.com/819818: "enforce_strict" mode should |
| // disable the extension. |
| // const bool expect_disabled = uses_enforce_strict_mode(); |
| EnableExtensionAndWaitForCompletion(expect_disabled); |
| |
| EXPECT_TRUE(expect_disabled ? ExtensionIsDisabledForCorruption() |
| : ExtensionIsEnabled()); |
| } |
| |
| // Tests the behavior of tampering an extension resource that is not requested |
| // by default and without modifying computed_hashes.json. |
| IN_PROC_BROWSER_TEST_P(ContentVerifierHashTest, |
| TamperNotRequestedResourceKeepComputedHashes) { |
| ASSERT_TRUE(InstallDefaultResourceExtension()); |
| |
| DisableExtension(); |
| |
| // Delete verified_contents.json to force a hash fetch on next load. |
| EXPECT_TRUE(DeleteVerifiedContents()); |
| |
| // computed_hashes.json should remain valid. |
| EXPECT_TRUE(HasComputedHashes()); |
| |
| // Tamper an extension resource that will *not* be requested on next load. |
| EXPECT_TRUE(TamperResource(kTamperNotRequestedResource)); |
| |
| // We tampered a resource that is not requested by the extension. Keeping |
| // computed_hashes.json will not compute new compute computed_hashes.json, and |
| // we will not discover the tampered hash. So the extension won't be disabled. |
| // |
| // TODO(lazyboy): http://crbug.com/819832: We fetched a new |
| // verified_contents.json in this case. However, if we had recomputed |
| // computed_hashes.json we would have discovered the tampered resource's hash |
| // mismatch. Fix. |
| const bool expect_disabled = false; |
| EnableExtensionAndWaitForCompletion(expect_disabled); |
| |
| EXPECT_TRUE(ExtensionIsEnabled()); |
| |
| // Expect a valid verified_contents.json file at the end. |
| EXPECT_TRUE(HasValidVerifiedContents()); |
| } |
| |
| // Tests the behavior of loading an extension without any default resource |
| // request and deleting its computed_hashes.json before fetching hashes. |
| IN_PROC_BROWSER_TEST_P(ContentVerifierHashTest, |
| TamperNoResourceExtensionDeleteComputedHashes) { |
| ASSERT_TRUE(InstallNoDefaultResourceExtension()); |
| |
| DisableExtension(); |
| |
| // Delete verified_contents.json to force a hash fetch on next load. |
| EXPECT_TRUE(DeleteVerifiedContents()); |
| |
| // Delete computed_hashes.json. |
| EXPECT_TRUE(DeleteComputedHashes()); |
| |
| // Tamper an extension resource that will *not* be requested on next load. |
| EXPECT_TRUE(TamperResource(kTamperNotRequestedResource)); |
| |
| TestExtensionRegistryObserver registry_observer( |
| ExtensionRegistry::Get(profile()), id()); |
| // The deletion of computed_hashes.json forces its recomputation and disables |
| // the extension. |
| EnableExtensionAndWaitForCompletion(true /* expect_disabled */); |
| EXPECT_TRUE(registry_observer.WaitForExtensionUnloaded()); |
| |
| EXPECT_TRUE(ExtensionIsDisabledForCorruption()); |
| |
| // Expect a valid computed_hashes.json file at the end, the verification |
| // failure must have used this file to detect corruption. |
| EXPECT_TRUE(HasValidComputedHashes()); |
| |
| // Expect a valid verified_contents.json file at the end. |
| EXPECT_TRUE(HasValidVerifiedContents()); |
| } |
| |
| // Tests the behavior of loading an extension without any default resource |
| // request and keeping its computed_hashes.json. |
| IN_PROC_BROWSER_TEST_P(ContentVerifierHashTest, |
| TamperNoResourceExtensionKeepComputedHashes) { |
| ASSERT_TRUE(InstallNoDefaultResourceExtension()); |
| |
| DisableExtension(); |
| |
| // Delete verified_contents.json to force a hash fetch on next load. |
| EXPECT_TRUE(DeleteVerifiedContents()); |
| |
| // computed_hashes.json should remain valid. |
| EXPECT_TRUE(HasComputedHashes()); |
| |
| // Tamper an extension resource that will *not* be requested on next load. |
| EXPECT_TRUE(TamperResource(kTamperNotRequestedResource)); |
| |
| // Not modifying computed_hashes.json will not trigger any hash computation |
| // at OnExtensionLoad, so we won't discover any hash mismatches. |
| EnableExtensionAndWaitForCompletion(false /* expect_disabled */); |
| |
| EXPECT_TRUE(ExtensionIsEnabled()); |
| |
| // Expect a valid verified_contents.json file at the end. |
| EXPECT_TRUE(HasValidVerifiedContents()); |
| } |
| |
| // Tests the behavior of loading an extension without any default resource |
| // request and tampering its computed_hashes.json. |
| IN_PROC_BROWSER_TEST_P(ContentVerifierHashTest, |
| TamperNoResourceExtensionTamperComputedHashes) { |
| ASSERT_TRUE(InstallNoDefaultResourceExtension()); |
| |
| DisableExtension(); |
| |
| // Delete verified_contents.json to force a hash fetch on next load. |
| EXPECT_TRUE(DeleteVerifiedContents()); |
| |
| // Tamper computed_hashes.json. |
| EXPECT_TRUE(TamperComputedHashes()); |
| |
| // Tamper an extension resource that will *not* be requested on next load. |
| EXPECT_TRUE(TamperResource(kTamperNotRequestedResource)); |
| |
| // Tampering computed_hashes.json will not trigger any hash computation |
| // at OnExtensionLoad, so we won't discover any hash mismatches. |
| // TODO(lazyboy): Consider fixing this, see http://crbug.com/819832 for |
| // details. |
| EnableExtensionAndWaitForCompletion(false /* expect_disabled */); |
| |
| EXPECT_TRUE(ExtensionIsEnabled()); |
| |
| // Because we didn't do any hash computation, expect computed_hashes.json to |
| // still remain invalid. |
| EXPECT_FALSE(HasValidComputedHashes()); |
| |
| // Expect a valid verified_contents.json file at the end. |
| EXPECT_TRUE(HasValidVerifiedContents()); |
| } |
| |
| // Tests the behavior of loading a default resource extension with tampering |
| // an extension resource that is not requested by default and without modifying |
| // computed_hashes.json. |
| IN_PROC_BROWSER_TEST_P( |
| ContentVerifierHashTest, |
| DefaultRequestExtensionTamperNotRequestedResourceKeepComputedHashes) { |
| ASSERT_TRUE(InstallDefaultResourceExtension()); |
| |
| DisableExtension(); |
| |
| // Delete verified_contents.json to force a hash fetch on next load. |
| EXPECT_TRUE(DeleteVerifiedContents()); |
| |
| // computed_hashes.json should remain valid. |
| EXPECT_TRUE(HasComputedHashes()); |
| |
| // Tamper an extension resource that will *not* be requested on next load. |
| EXPECT_TRUE(TamperResource(kTamperNotRequestedResource)); |
| |
| // TODO(lazyboy): Not modifying the computed_hashes.json file doesn't prompt |
| // a hash recomputation and the requested (not-tampered) resource's |
| // corresponding ContentVerifyJob succeeds because that resource's hash |
| // remains fine. Therefore, the extension remains enabled. Consider disabling |
| // the extension in this case: https://crbug.com/819832. |
| EnableExtensionAndWaitForCompletion(false /* expect_disabled */); |
| |
| EXPECT_TRUE(ExtensionIsEnabled()); |
| |
| // Expect a valid verified_contents.json file at the end. |
| EXPECT_TRUE(HasValidVerifiedContents()); |
| } |
| |
| // Tests the behavior of loading a default resource extension with tampering |
| // an extension resource that is not requested by default and tampering |
| // computed_hashes.json. |
| IN_PROC_BROWSER_TEST_P( |
| ContentVerifierHashTest, |
| DefaultRequestExtensionTamperNotRequestedResourceTamperComputedHashes) { |
| ASSERT_TRUE(InstallDefaultResourceExtension()); |
| |
| DisableExtension(); |
| |
| // Delete verified_contents.json to force a hash fetch on next load. |
| EXPECT_TRUE(DeleteVerifiedContents()); |
| |
| // Tamper computed_hashes.json. |
| EXPECT_TRUE(TamperComputedHashes()); |
| |
| // Tamper an extension resource that will *not* be requested on next load. |
| EXPECT_TRUE(TamperResource(kTamperNotRequestedResource)); |
| |
| TestExtensionRegistryObserver registry_observer( |
| ExtensionRegistry::Get(profile()), id()); |
| // Subtle: tampering computed_hashes.json (by itself) will not trigger any |
| // hash computation or failure during OnExtensionLoad. However, the default |
| // resource request (that isn't tampered) will prompt a hash read that will |
| // fail due to the tampered computed_hashes.json. |
| EnableExtensionAndWaitForCompletion(true /* expect_disabled */); |
| EXPECT_TRUE(registry_observer.WaitForExtensionUnloaded()); |
| |
| EXPECT_TRUE(ExtensionIsDisabledForCorruption()); |
| |
| // Expect a valid computed_hashes.json file at the end, the verification |
| // failure must have used this file to detect corruption. |
| EXPECT_TRUE(HasValidComputedHashes()); |
| |
| // Expect a valid verified_contents.json file at the end. |
| EXPECT_TRUE(HasValidVerifiedContents()); |
| } |
| |
| // Tests the behavior of loading a default resource extension with tampering |
| // an extension resource that is not requested by default and deleting |
| // computed_hashes.json. |
| IN_PROC_BROWSER_TEST_P( |
| ContentVerifierHashTest, |
| DefaultRequestExtensionTamperNotRequestedResourceDeleteComputedHashes) { |
| ASSERT_TRUE(InstallDefaultResourceExtension()); |
| |
| DisableExtension(); |
| |
| // Delete verified_contents.json to force a hash fetch on next load. |
| EXPECT_TRUE(DeleteVerifiedContents()); |
| |
| // Delete computed_hashes.json. |
| EXPECT_TRUE(DeleteComputedHashes()); |
| |
| // Tamper an extension resource that will *not* be requested on next load. |
| EXPECT_TRUE(TamperResource(kTamperNotRequestedResource)); |
| |
| TestExtensionRegistryObserver registry_observer( |
| ExtensionRegistry::Get(profile()), id()); |
| // The deletion of computed_hashes.json will trigger hash recomputation and |
| // the file's regeneration. This will discover the resource tampering and |
| // disable the extension. |
| EnableExtensionAndWaitForCompletion(true /* expect_disabled */); |
| EXPECT_TRUE(registry_observer.WaitForExtensionUnloaded()); |
| |
| EXPECT_TRUE(ExtensionIsDisabledForCorruption()); |
| |
| // Expect a valid computed_hashes.json file at the end, the verification |
| // failure must have used this file to detect corruption. |
| EXPECT_TRUE(HasValidComputedHashes()); |
| |
| // Expect a valid verified_contents.json file at the end. |
| EXPECT_TRUE(HasValidVerifiedContents()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(FetchBehaviorTests, |
| ContentVerifierHashTest, |
| testing::Values(kEnforce, kEnforceStrict)); |
| |
| } // namespace extensions |