blob: f877bdcecb28ea6f6b735867cb9fae95c9da3e4f [file] [log] [blame]
// 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 <stdint.h>
#include <string>
#include <vector>
#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<VerifiedContents> 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<VerifiedContents> 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<std::string, std::string> 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<VerifiedContents> 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<TestFileInfo> info_list{
{
"manifest.json", "ysCDJuQ1s7vWF4yUZTRB2_XDE6vfFyQcIPSmyvNvqEw",
},
{
"background.js", "uYeF7eHzVgKpiBg5fikv2NTctmJnxCfX1UhhlrizvNE",
},
{
"mixedcase.html", "S1lnRa4Yu1CM2dCwJoFYKfAqRkFC7SSI4tzyIOzO7hA",
},
{
"mixedCase.html", "FVncNmt1wBfFn3aZVTnMB9CFRTRIl0Z4YFqm14Wmrhs",
},
{
"doT.html.", "jEsJEk-0azFYx7G91rSUPuzPBXp95863lG4MDwZcSog",
},
};
std::vector<std::string> 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<VerifiedContents> 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