Merge "Add an updater function to compute hash tree"
This commit is contained in:
commit
dbe4420325
8 changed files with 216 additions and 18 deletions
|
@ -48,6 +48,7 @@ enum CauseCode : int {
|
|||
kRebootFailure,
|
||||
kPackageExtractFileFailure,
|
||||
kPatchApplicationFailure,
|
||||
kHashTreeComputationFailure,
|
||||
kVendorFailure = 200
|
||||
};
|
||||
|
||||
|
|
|
@ -137,6 +137,7 @@ libupdater_static_libraries := \
|
|||
libext4_utils \
|
||||
libfec \
|
||||
libfec_rs \
|
||||
libverity_tree \
|
||||
libfs_mgr \
|
||||
libgtest_prod \
|
||||
liblog \
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
#include <brotli/encode.h>
|
||||
#include <bsdiff/bsdiff.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <verity/hash_tree_builder.h>
|
||||
#include <ziparchive/zip_archive.h>
|
||||
#include <ziparchive/zip_writer.h>
|
||||
|
||||
|
@ -389,6 +390,86 @@ TEST_F(UpdaterTest, read_file) {
|
|||
expect("", script, kNoCause);
|
||||
}
|
||||
|
||||
TEST_F(UpdaterTest, compute_hash_tree_smoke) {
|
||||
std::string data;
|
||||
for (unsigned char i = 0; i < 128; i++) {
|
||||
data += std::string(4096, i);
|
||||
}
|
||||
// Appends an additional block for verity data.
|
||||
data += std::string(4096, 0);
|
||||
ASSERT_EQ(129 * 4096, data.size());
|
||||
ASSERT_TRUE(android::base::WriteStringToFile(data, image_file_));
|
||||
|
||||
std::string salt = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7";
|
||||
std::string expected_root_hash =
|
||||
"7e0a8d8747f54384014ab996f5b2dc4eb7ff00c630eede7134c9e3f05c0dd8ca";
|
||||
// hash_tree_ranges, source_ranges, hash_algorithm, salt_hex, root_hash
|
||||
std::vector<std::string> tokens{ "compute_hash_tree", "2,128,129", "2,0,128", "sha256", salt,
|
||||
expected_root_hash };
|
||||
std::string hash_tree_command = android::base::Join(tokens, " ");
|
||||
|
||||
std::vector<std::string> transfer_list{
|
||||
"4", "2", "0", "2", hash_tree_command,
|
||||
};
|
||||
|
||||
PackageEntries entries{
|
||||
{ "new_data", "" },
|
||||
{ "patch_data", "" },
|
||||
{ "transfer_list", android::base::Join(transfer_list, "\n") },
|
||||
};
|
||||
|
||||
RunBlockImageUpdate(false, entries, image_file_, "t");
|
||||
|
||||
std::string updated;
|
||||
ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated));
|
||||
ASSERT_EQ(129 * 4096, updated.size());
|
||||
ASSERT_EQ(data.substr(0, 128 * 4096), updated.substr(0, 128 * 4096));
|
||||
|
||||
// Computes the SHA256 of the salt + hash_tree_data and expects the result to match with the
|
||||
// root_hash.
|
||||
std::vector<unsigned char> salt_bytes;
|
||||
ASSERT_TRUE(HashTreeBuilder::ParseBytesArrayFromString(salt, &salt_bytes));
|
||||
std::vector<unsigned char> hash_tree = std::move(salt_bytes);
|
||||
hash_tree.insert(hash_tree.end(), updated.begin() + 128 * 4096, updated.end());
|
||||
|
||||
std::vector<unsigned char> digest(SHA256_DIGEST_LENGTH);
|
||||
SHA256(hash_tree.data(), hash_tree.size(), digest.data());
|
||||
ASSERT_EQ(expected_root_hash, HashTreeBuilder::BytesArrayToString(digest));
|
||||
}
|
||||
|
||||
TEST_F(UpdaterTest, compute_hash_tree_root_mismatch) {
|
||||
std::string data;
|
||||
for (size_t i = 0; i < 128; i++) {
|
||||
data += std::string(4096, i);
|
||||
}
|
||||
// Appends an additional block for verity data.
|
||||
data += std::string(4096, 0);
|
||||
ASSERT_EQ(129 * 4096, data.size());
|
||||
// Corrupts one bit
|
||||
data[4096] = 'A';
|
||||
ASSERT_TRUE(android::base::WriteStringToFile(data, image_file_));
|
||||
|
||||
std::string salt = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7";
|
||||
std::string expected_root_hash =
|
||||
"7e0a8d8747f54384014ab996f5b2dc4eb7ff00c630eede7134c9e3f05c0dd8ca";
|
||||
// hash_tree_ranges, source_ranges, hash_algorithm, salt_hex, root_hash
|
||||
std::vector<std::string> tokens{ "compute_hash_tree", "2,128,129", "2,0,128", "sha256", salt,
|
||||
expected_root_hash };
|
||||
std::string hash_tree_command = android::base::Join(tokens, " ");
|
||||
|
||||
std::vector<std::string> transfer_list{
|
||||
"4", "2", "0", "2", hash_tree_command,
|
||||
};
|
||||
|
||||
PackageEntries entries{
|
||||
{ "new_data", "" },
|
||||
{ "patch_data", "" },
|
||||
{ "transfer_list", android::base::Join(transfer_list, "\n") },
|
||||
};
|
||||
|
||||
RunBlockImageUpdate(false, entries, image_file_, "", kHashTreeComputationFailure);
|
||||
}
|
||||
|
||||
TEST_F(UpdaterTest, write_value) {
|
||||
// write_value() expects two arguments.
|
||||
expect(nullptr, "write_value()", kArgsParsingFailure);
|
||||
|
|
|
@ -30,6 +30,7 @@ TEST(CommandsTest, ParseType) {
|
|||
ASSERT_EQ(Command::Type::IMGDIFF, Command::ParseType("imgdiff"));
|
||||
ASSERT_EQ(Command::Type::STASH, Command::ParseType("stash"));
|
||||
ASSERT_EQ(Command::Type::FREE, Command::ParseType("free"));
|
||||
ASSERT_EQ(Command::Type::COMPUTE_HASH_TREE, Command::ParseType("compute_hash_tree"));
|
||||
}
|
||||
|
||||
TEST(CommandsTest, ParseType_InvalidCommand) {
|
||||
|
|
|
@ -34,6 +34,7 @@ updater_common_static_libraries := \
|
|||
libext4_utils \
|
||||
libfec \
|
||||
libfec_rs \
|
||||
libverity_tree \
|
||||
libfs_mgr \
|
||||
libgtest_prod \
|
||||
liblog \
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
#include <fec/io.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <private/android_filesystem_config.h>
|
||||
#include <verity/hash_tree_builder.h>
|
||||
#include <ziparchive/zip_archive.h>
|
||||
|
||||
#include "edify/expr.h"
|
||||
|
@ -1495,6 +1496,105 @@ static int PerformCommandAbort(CommandParameters&) {
|
|||
return -1;
|
||||
}
|
||||
|
||||
// Computes the hash_tree bytes based on the parameters, checks if the root hash of the tree
|
||||
// matches the expected hash and writes the result to the specified range on the block_device.
|
||||
// Hash_tree computation arguments:
|
||||
// hash_tree_ranges
|
||||
// source_ranges
|
||||
// hash_algorithm
|
||||
// salt_hex
|
||||
// root_hash
|
||||
static int PerformCommandComputeHashTree(CommandParameters& params) {
|
||||
if (params.cpos + 5 != params.tokens.size()) {
|
||||
LOG(ERROR) << "Invaild arguments count in hash computation " << params.cmdline;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Expects the hash_tree data to be contiguous.
|
||||
RangeSet hash_tree_ranges = RangeSet::Parse(params.tokens[params.cpos++]);
|
||||
if (!hash_tree_ranges || hash_tree_ranges.size() != 1) {
|
||||
LOG(ERROR) << "Invalid hash tree ranges in " << params.cmdline;
|
||||
return -1;
|
||||
}
|
||||
|
||||
RangeSet source_ranges = RangeSet::Parse(params.tokens[params.cpos++]);
|
||||
if (!source_ranges) {
|
||||
LOG(ERROR) << "Invalid source ranges in " << params.cmdline;
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto hash_function = HashTreeBuilder::HashFunction(params.tokens[params.cpos++]);
|
||||
if (hash_function == nullptr) {
|
||||
LOG(ERROR) << "Invalid hash algorithm in " << params.cmdline;
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> salt;
|
||||
std::string salt_hex = params.tokens[params.cpos++];
|
||||
if (salt_hex.empty() || !HashTreeBuilder::ParseBytesArrayFromString(salt_hex, &salt)) {
|
||||
LOG(ERROR) << "Failed to parse salt in " << params.cmdline;
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string expected_root_hash = params.tokens[params.cpos++];
|
||||
if (expected_root_hash.empty()) {
|
||||
LOG(ERROR) << "Invalid root hash in " << params.cmdline;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Starts the hash_tree computation.
|
||||
HashTreeBuilder builder(BLOCKSIZE, hash_function);
|
||||
if (!builder.Initialize(source_ranges.blocks() * BLOCKSIZE, salt)) {
|
||||
LOG(ERROR) << "Failed to initialize hash tree computation, source " << source_ranges.ToString()
|
||||
<< ", salt " << salt_hex;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Iterates through every block in the source_ranges and updates the hash tree structure
|
||||
// accordingly.
|
||||
for (const auto& range : source_ranges) {
|
||||
uint8_t buffer[BLOCKSIZE];
|
||||
if (!check_lseek(params.fd, static_cast<off64_t>(range.first) * BLOCKSIZE, SEEK_SET)) {
|
||||
PLOG(ERROR) << "Failed to seek to block: " << range.first;
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t i = range.first; i < range.second; i++) {
|
||||
if (read_all(params.fd, buffer, BLOCKSIZE) == -1) {
|
||||
LOG(ERROR) << "Failed to read data in " << range.first << ":" << range.second;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!builder.Update(reinterpret_cast<unsigned char*>(buffer), BLOCKSIZE)) {
|
||||
LOG(ERROR) << "Failed to update hash tree builder";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!builder.BuildHashTree()) {
|
||||
LOG(ERROR) << "Failed to build hash tree";
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string root_hash_hex = HashTreeBuilder::BytesArrayToString(builder.root_hash());
|
||||
if (root_hash_hex != expected_root_hash) {
|
||||
LOG(ERROR) << "Root hash of the verity hash tree doesn't match the expected value. Expected: "
|
||||
<< expected_root_hash << ", actual: " << root_hash_hex;
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t write_offset = static_cast<uint64_t>(hash_tree_ranges.GetBlockNumber(0)) * BLOCKSIZE;
|
||||
if (params.canwrite && !builder.WriteHashTreeToFd(params.fd, write_offset)) {
|
||||
LOG(ERROR) << "Failed to write hash tree to output";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// TODO(xunchang) validates the written bytes
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
using CommandFunction = std::function<int(CommandParameters&)>;
|
||||
|
||||
using CommandMap = std::unordered_map<Command::Type, CommandFunction>;
|
||||
|
@ -1737,6 +1837,9 @@ static Value* PerformBlockImageUpdate(const char* name, State* state,
|
|||
|
||||
if (performer(params) == -1) {
|
||||
LOG(ERROR) << "failed to execute command [" << line << "]";
|
||||
if (cmd_type == Command::Type::COMPUTE_HASH_TREE && failure_type == kNoCause) {
|
||||
failure_type = kHashTreeComputationFailure;
|
||||
}
|
||||
goto pbiudone;
|
||||
}
|
||||
|
||||
|
@ -1894,15 +1997,16 @@ Value* BlockImageVerifyFn(const char* name, State* state,
|
|||
// Commands which are not allowed are set to nullptr to skip them completely.
|
||||
const CommandMap command_map{
|
||||
// clang-format off
|
||||
{ Command::Type::ABORT, PerformCommandAbort },
|
||||
{ Command::Type::BSDIFF, PerformCommandDiff },
|
||||
{ Command::Type::ERASE, nullptr },
|
||||
{ Command::Type::FREE, PerformCommandFree },
|
||||
{ Command::Type::IMGDIFF, PerformCommandDiff },
|
||||
{ Command::Type::MOVE, PerformCommandMove },
|
||||
{ Command::Type::NEW, nullptr },
|
||||
{ Command::Type::STASH, PerformCommandStash },
|
||||
{ Command::Type::ZERO, nullptr },
|
||||
{ Command::Type::ABORT, PerformCommandAbort },
|
||||
{ Command::Type::BSDIFF, PerformCommandDiff },
|
||||
{ Command::Type::COMPUTE_HASH_TREE, PerformCommandComputeHashTree },
|
||||
{ Command::Type::ERASE, nullptr },
|
||||
{ Command::Type::FREE, PerformCommandFree },
|
||||
{ Command::Type::IMGDIFF, PerformCommandDiff },
|
||||
{ Command::Type::MOVE, PerformCommandMove },
|
||||
{ Command::Type::NEW, nullptr },
|
||||
{ Command::Type::STASH, PerformCommandStash },
|
||||
{ Command::Type::ZERO, nullptr },
|
||||
// clang-format on
|
||||
};
|
||||
CHECK_EQ(static_cast<size_t>(Command::Type::LAST), command_map.size());
|
||||
|
@ -1915,15 +2019,16 @@ Value* BlockImageUpdateFn(const char* name, State* state,
|
|||
const std::vector<std::unique_ptr<Expr>>& argv) {
|
||||
const CommandMap command_map{
|
||||
// clang-format off
|
||||
{ Command::Type::ABORT, PerformCommandAbort },
|
||||
{ Command::Type::BSDIFF, PerformCommandDiff },
|
||||
{ Command::Type::ERASE, PerformCommandErase },
|
||||
{ Command::Type::FREE, PerformCommandFree },
|
||||
{ Command::Type::IMGDIFF, PerformCommandDiff },
|
||||
{ Command::Type::MOVE, PerformCommandMove },
|
||||
{ Command::Type::NEW, PerformCommandNew },
|
||||
{ Command::Type::STASH, PerformCommandStash },
|
||||
{ Command::Type::ZERO, PerformCommandZero },
|
||||
{ Command::Type::ABORT, PerformCommandAbort },
|
||||
{ Command::Type::BSDIFF, PerformCommandDiff },
|
||||
{ Command::Type::COMPUTE_HASH_TREE, PerformCommandComputeHashTree },
|
||||
{ Command::Type::ERASE, PerformCommandErase },
|
||||
{ Command::Type::FREE, PerformCommandFree },
|
||||
{ Command::Type::IMGDIFF, PerformCommandDiff },
|
||||
{ Command::Type::MOVE, PerformCommandMove },
|
||||
{ Command::Type::NEW, PerformCommandNew },
|
||||
{ Command::Type::STASH, PerformCommandStash },
|
||||
{ Command::Type::ZERO, PerformCommandZero },
|
||||
// clang-format on
|
||||
};
|
||||
CHECK_EQ(static_cast<size_t>(Command::Type::LAST), command_map.size());
|
||||
|
|
|
@ -40,6 +40,8 @@ Command::Type Command::ParseType(const std::string& type_str) {
|
|||
return Type::ABORT;
|
||||
} else if (type_str == "bsdiff") {
|
||||
return Type::BSDIFF;
|
||||
} else if (type_str == "compute_hash_tree") {
|
||||
return Type::COMPUTE_HASH_TREE;
|
||||
} else if (type_str == "erase") {
|
||||
return Type::ERASE;
|
||||
} else if (type_str == "free") {
|
||||
|
@ -175,6 +177,7 @@ Command Command::Parse(const std::string& line, size_t index, std::string* err)
|
|||
SourceInfo source_info;
|
||||
StashInfo stash_info;
|
||||
|
||||
// TODO(xunchang) add the parse code of compute_hash_tree
|
||||
if (op == Type::ZERO || op == Type::NEW || op == Type::ERASE) {
|
||||
// zero/new/erase <rangeset>
|
||||
if (pos + 1 != tokens.size()) {
|
||||
|
|
|
@ -213,6 +213,10 @@ class PatchInfo {
|
|||
// - Free the given stash data.
|
||||
// - Meaningful args: StashInfo
|
||||
//
|
||||
// compute_hash_tree <hash_tree_ranges> <source_ranges> <hash_algorithm> <salt_hex> <root_hash>
|
||||
// - Computes the hash_tree bytes and writes the result to the specified range on the
|
||||
// block_device.
|
||||
//
|
||||
// abort
|
||||
// - Abort the current update. Allowed for testing code only.
|
||||
//
|
||||
|
@ -221,6 +225,7 @@ class Command {
|
|||
enum class Type {
|
||||
ABORT,
|
||||
BSDIFF,
|
||||
COMPUTE_HASH_TREE,
|
||||
ERASE,
|
||||
FREE,
|
||||
IMGDIFF,
|
||||
|
|
Loading…
Reference in a new issue