// Copyright 2014 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 #include #include #include "base/base64url.h" #include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/path_service.h" #include "base/stl_util.h" #include "build/build_config.h" #include "extensions/browser/content_verifier/content_verifier_utils.h" #include "extensions/browser/verified_contents.h" #include "extensions/common/extension.h" #include "extensions/common/extension_paths.h" #include "testing/gtest/include/gtest/gtest.h" namespace extensions { namespace { const char kContentVerifierDirectory[] = "content_verifier/"; const char kPublicKeyPem[] = "public_key.pem"; constexpr bool kIsFileAccessCaseInsensitive = !content_verifier_utils::IsFileAccessCaseSensitive(); constexpr bool kIsDotSpaceSuffixIgnored = content_verifier_utils::IsDotSpaceFilenameSuffixIgnored(); std::string DecodeBase64Url(const std::string& encoded) { std::string decoded; if (!base::Base64UrlDecode( encoded, base::Base64UrlDecodePolicy::IGNORE_PADDING, &decoded)) return std::string(); return decoded; } bool GetPublicKey(const base::FilePath& path, std::string* public_key) { std::string public_key_pem; if (!base::ReadFileToString(path, &public_key_pem)) return false; if (!Extension::ParsePEMKeyBytes(public_key_pem, public_key)) return false; return true; } base::FilePath GetTestDir(const char* sub_dir) { base::FilePath path; base::PathService::Get(DIR_TEST_DATA, &path); return path.AppendASCII(kContentVerifierDirectory).AppendASCII(sub_dir); } // Loads verified_contents file from a sub directory under // kContentVerifierDirectory. std::unique_ptr CreateTestVerifiedContents( const char* sub_dir, const char* verified_contents_filename) { // Figure out our test data directory. base::FilePath path = GetTestDir(sub_dir); // Initialize the VerifiedContents object. std::string public_key; if (!GetPublicKey(path.AppendASCII(kPublicKeyPem), &public_key)) return nullptr; base::FilePath verified_contents_path = path.AppendASCII(verified_contents_filename); return VerifiedContents::Create(base::as_bytes(base::make_span(public_key)), verified_contents_path); } } // namespace TEST(VerifiedContents, Simple) { std::unique_ptr verified_contents = CreateTestVerifiedContents("simple", "verified_contents.json"); ASSERT_TRUE(verified_contents); const VerifiedContents& contents = *verified_contents; // Make sure we get expected values. EXPECT_EQ(contents.block_size(), 4096); EXPECT_EQ(contents.extension_id(), "abcdefghijklmnopabcdefghijklmnop"); EXPECT_EQ("1.2.3", contents.version().GetString()); EXPECT_TRUE(contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("manifest.json"), DecodeBase64Url("-vyyIIn7iSCzg7X3ICUI5wZa3tG7w7vyiCckxZdJGfs"))); EXPECT_TRUE(contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("background.js"), DecodeBase64Url("txHiG5KQvNoPOSH5FbQo9Zb5gJ23j3oFB0Ru9DOnziw"))); base::FilePath foo_bar_html = base::FilePath(FILE_PATH_LITERAL("foo")).AppendASCII("bar.html"); EXPECT_FALSE(foo_bar_html.IsAbsolute()); EXPECT_TRUE(contents.TreeHashRootEquals( foo_bar_html, DecodeBase64Url("L37LFbT_hmtxRL7AfGZN9YTpW6yoz_ZiQ1opLJn1NZU"))); base::FilePath nonexistent = base::FilePath::FromUTF8Unsafe("nonexistent"); EXPECT_FALSE(contents.HasTreeHashRoot(nonexistent)); std::map hashes = { {"lowercase.html", "HpLotLGCmmOdKYvGQmD3OkXMKGs458dbanY4WcfAZI0"}, {"ALLCAPS.html", "bl-eV8ENowvtw6P14D4X1EP0mlcMoG-_aOx5o9C1364"}, {"MixedCase.Html", "zEAO9FwciigMNy3NtU2XNb-dS5TQMmVNx0T9h7WvXbQ"}, {"mIxedcAse.Html", "nKRqUcJg1_QZWAeCb4uFd5ouC0McuGavKp8TFDRqBgg"}, }; // Resource is "lowercase.html". EXPECT_TRUE(contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("lowercase.html"), DecodeBase64Url(hashes["lowercase.html"]))); // Only case-insensitive systems should be able to get hashes with incorrect // case. EXPECT_EQ(kIsFileAccessCaseInsensitive, contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("Lowercase.Html"), DecodeBase64Url(hashes["lowercase.html"]))); EXPECT_EQ(kIsFileAccessCaseInsensitive, contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("LOWERCASE.HTML"), DecodeBase64Url(hashes["lowercase.html"]))); // Resource is "ALLCAPS.HTML" EXPECT_TRUE(contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("ALLCAPS.HTML"), DecodeBase64Url(hashes["ALLCAPS.html"]))); // Only case-insensitive systems should be able to get hashes with incorrect // case. EXPECT_EQ(kIsFileAccessCaseInsensitive, contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("AllCaps.Html"), DecodeBase64Url(hashes["ALLCAPS.html"]))); EXPECT_EQ(kIsFileAccessCaseInsensitive, contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("allcaps.html"), DecodeBase64Url(hashes["ALLCAPS.html"]))); // Resources are "MixedCase.Html", "mIxedcAse.Html". EXPECT_TRUE(contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("MixedCase.Html"), DecodeBase64Url(hashes["MixedCase.Html"]))); EXPECT_TRUE(contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("mIxedcAse.Html"), DecodeBase64Url(hashes["mIxedcAse.Html"]))); // In case-sensitive systems, swapping hashes within MixedCase.Html and // mIxedcAse.Html always would mismatch hash, but it matches for // case-insensitive systems. // TODO(https:://crbug.com/1040702): Fix if this becomes a problem. EXPECT_EQ(kIsFileAccessCaseInsensitive, contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("mIxedcAse.Html"), DecodeBase64Url(hashes["MixedCase.Html"]))); EXPECT_EQ(kIsFileAccessCaseInsensitive, contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("MixedCase.Html"), DecodeBase64Url(hashes["mIxedcAse.Html"]))); // Continuing from above, in case-insensitive systems, there is non // deterministic behavior here, e.g. MIXEDCASE.HTML will match both hashes of // MixedCase.Html and mIxedcAse.Html. EXPECT_EQ(kIsFileAccessCaseInsensitive, contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("MIXEDCASE.HTML"), DecodeBase64Url(hashes["MixedCase.Html"]))); EXPECT_EQ(kIsFileAccessCaseInsensitive, contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("MIXEDCASE.HTML"), DecodeBase64Url(hashes["mIxedcAse.Html"]))); EXPECT_EQ(kIsFileAccessCaseInsensitive, contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("mixedcase.html"), DecodeBase64Url(hashes["MixedCase.Html"]))); EXPECT_EQ(kIsFileAccessCaseInsensitive, contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("mixedcase.html"), DecodeBase64Url(hashes["mIxedcAse.Html"]))); // Regression test for https://crbug.com/776609. EXPECT_FALSE(contents.TreeHashRootEquals( base::FilePath::FromUTF8Unsafe("allcaps.html"), // This is the hash of "mixedcase.html". DecodeBase64Url("zEAO9FwciigMNy3NtU2XNb-dS5TQMmVNx0T9h7WvXbQ"))); } TEST(VerifiedContents, FailsOnBase64) { // Accepting base64-encoded input where base64url-encoded input is expected // will be considered to be invalid data. Verify that it gets rejected. ASSERT_FALSE( CreateTestVerifiedContents("simple", "verified_contents_base64.json")); } // Tests behavior of verified contents with filenames that have "." and " " // suffixes appened to them. // Regression test for https://crbug.com/696208. TEST(VerifiedContents, DotSpaceSuffixedFiles) { std::unique_ptr contents = CreateTestVerifiedContents("dot_space_suffix", "verified_contents.json"); ASSERT_TRUE(contents); // Make sure we get expected values. EXPECT_EQ(contents->block_size(), 4096); EXPECT_EQ(contents->extension_id(), "abcdabcdabcdabcdabcdabcdabcdabcd"); EXPECT_EQ("1.2.3.4", contents->version().GetString()); auto has_tree_hash_root = [&contents](const std::string& file_path_str) { return contents->HasTreeHashRoot( base::FilePath::FromUTF8Unsafe(file_path_str)); }; auto tree_hash_root_equals = [&contents](const std::string& file_path_str, const char* expected_hash) { return contents->TreeHashRootEquals( base::FilePath::FromUTF8Unsafe(file_path_str), DecodeBase64Url(expected_hash)); }; // Non-existent files won't be found in tree hash. EXPECT_FALSE(has_tree_hash_root("non-existent.js")); EXPECT_FALSE(has_tree_hash_root("")); struct TestFileInfo { const char* filename; const char* root_hashes; }; std::vector info_list{ { "manifest.json", "ysCDJuQ1s7vWF4yUZTRB2_XDE6vfFyQcIPSmyvNvqEw", }, { "background.js", "uYeF7eHzVgKpiBg5fikv2NTctmJnxCfX1UhhlrizvNE", }, { "mixedcase.html", "S1lnRa4Yu1CM2dCwJoFYKfAqRkFC7SSI4tzyIOzO7hA", }, { "mixedCase.html", "FVncNmt1wBfFn3aZVTnMB9CFRTRIl0Z4YFqm14Wmrhs", }, { "doT.html.", "jEsJEk-0azFYx7G91rSUPuzPBXp95863lG4MDwZcSog", }, }; std::vector kSuffixes{ // Only spaces. " ", " ", " ", // Only dots. ".", "..", "...", // Mix of dots and spaces. ". ", ". ", ".. ", "... ", " .", " .", " .", " . ", " ..", " ...", " .. ", }; for (const TestFileInfo& info : info_list) { // The original filenames' hashes must exist. EXPECT_TRUE(has_tree_hash_root(info.filename)); EXPECT_TRUE(tree_hash_root_equals(info.filename, info.root_hashes)); // Verify that the discovery of tree hashes is also correct when the // filenames are appended with dot and space characters: // - they should still succeed on windows (kIsDotSpaceSuffixIgnored // = true). // - they should fail otherwise (kIsDotSpaceSuffixIgnored = false). for (const std::string& suffix : kSuffixes) { std::string path_with_suffix = std::string(info.filename).append(suffix); EXPECT_EQ(kIsDotSpaceSuffixIgnored, has_tree_hash_root(path_with_suffix)); } } // For background.js, additionally verify that reading the file with and // without the suffixes described above matches our expectations, taking // kIsDotSpaceSuffixIgnored into account. const char* kBackgroundJSFilename = "background.js"; const char* kBackgroundJSContents = "console.log('hello');\n"; base::FilePath test_dir = GetTestDir("dot_space_suffix"); { // Case 1/2: background.js without suffix. base::FilePath background_js_path = test_dir.AppendASCII(kBackgroundJSFilename); EXPECT_TRUE(base::PathExists(background_js_path)); std::string background_js_contents; EXPECT_TRUE( base::ReadFileToString(background_js_path, &background_js_contents)); EXPECT_EQ(kBackgroundJSContents, background_js_contents); } { // Case 2/2: background.js with dot/space suffixes. for (const std::string& suffix : kSuffixes) { base::FilePath background_js_suffix_path = test_dir.AppendASCII( std::string(kBackgroundJSFilename).append(suffix)); EXPECT_EQ(kIsDotSpaceSuffixIgnored, base::PathExists(background_js_suffix_path)); if (kIsDotSpaceSuffixIgnored) { std::string background_js_suffix_contents; EXPECT_TRUE(base::ReadFileToString(background_js_suffix_path, &background_js_suffix_contents)); EXPECT_EQ(kBackgroundJSContents, background_js_suffix_contents); } } } } // Tests behavior of verified_contents.json file containing keys already with // "." suffix. // Regression test for https://crbug.com/696208. TEST(VerifiedContents, VerifiedContentsFileContainsDotSuffixedFilename) { std::unique_ptr contents = CreateTestVerifiedContents("dot_space_suffix", "verified_contents.json"); ASSERT_TRUE(contents); // Make sure we get expected values. EXPECT_EQ(contents->block_size(), 4096); EXPECT_EQ(contents->extension_id(), "abcdabcdabcdabcdabcdabcdabcdabcd"); EXPECT_EQ("1.2.3.4", contents->version().GetString()); auto has_tree_hash_root = [&contents](const std::string& file_path_str) { return contents->HasTreeHashRoot( base::FilePath::FromUTF8Unsafe(file_path_str)); }; auto tree_hash_root_equals = [&contents](const std::string& file_path_str, const char* expected_hash) { return contents->TreeHashRootEquals( base::FilePath::FromUTF8Unsafe(file_path_str), DecodeBase64Url(expected_hash)); }; // The original key "doT.html." should always succeed. EXPECT_TRUE(has_tree_hash_root("doT.html.")); EXPECT_TRUE(tree_hash_root_equals( "doT.html.", "jEsJEk-0azFYx7G91rSUPuzPBXp95863lG4MDwZcSog")); // Its case variants would only succeed for case-insensitive system. EXPECT_EQ(kIsFileAccessCaseInsensitive, has_tree_hash_root("dot.html.")); EXPECT_EQ(kIsFileAccessCaseInsensitive, tree_hash_root_equals( "dot.html.", "jEsJEk-0azFYx7G91rSUPuzPBXp95863lG4MDwZcSog")); // Keys with dot stripped succeeds if kIsDotSpaceSuffixIgnored is true. { const char* kKey = "dot.html"; EXPECT_EQ(kIsDotSpaceSuffixIgnored, has_tree_hash_root(kKey)); EXPECT_EQ(kIsDotSpaceSuffixIgnored, tree_hash_root_equals( kKey, "jEsJEk-0azFYx7G91rSUPuzPBXp95863lG4MDwZcSog")); } // Also, adding (.| )+ suffix would succeed if kIsDotSpaceSuffixIgnored is // true. This is already part of VerifiedContents.DotSpaceSuffixedFiles test. } } // namespace extensions