344 lines
9.6 KiB
C++
344 lines
9.6 KiB
C++
|
#include <openssl/evp.h>
|
||
|
#include <sparse/sparse.h>
|
||
|
|
||
|
#undef NDEBUG
|
||
|
|
||
|
#include <assert.h>
|
||
|
#include <errno.h>
|
||
|
#include <getopt.h>
|
||
|
#include <fcntl.h>
|
||
|
#include <inttypes.h>
|
||
|
#include <stdbool.h>
|
||
|
#include <stdio.h>
|
||
|
#include <stdlib.h>
|
||
|
#include <string.h>
|
||
|
#include <unistd.h>
|
||
|
|
||
|
struct sparse_hash_ctx {
|
||
|
unsigned char *hashes;
|
||
|
const unsigned char *salt;
|
||
|
uint64_t salt_size;
|
||
|
uint64_t hash_size;
|
||
|
uint64_t block_size;
|
||
|
const unsigned char *zero_block_hash;
|
||
|
const EVP_MD *md;
|
||
|
};
|
||
|
|
||
|
#define div_round_up(x,y) (((x) + (y) - 1)/(y))
|
||
|
|
||
|
#define round_up(x,y) (div_round_up(x,y)*(y))
|
||
|
|
||
|
#define FATAL(x...) { \
|
||
|
fprintf(stderr, x); \
|
||
|
exit(1); \
|
||
|
}
|
||
|
|
||
|
size_t verity_tree_blocks(uint64_t data_size, size_t block_size, size_t hash_size,
|
||
|
int level)
|
||
|
{
|
||
|
size_t level_blocks = div_round_up(data_size, block_size);
|
||
|
int hashes_per_block = div_round_up(block_size, hash_size);
|
||
|
|
||
|
do {
|
||
|
level_blocks = div_round_up(level_blocks, hashes_per_block);
|
||
|
} while (level--);
|
||
|
|
||
|
return level_blocks;
|
||
|
}
|
||
|
|
||
|
int hash_block(const EVP_MD *md,
|
||
|
const unsigned char *block, size_t len,
|
||
|
const unsigned char *salt, size_t salt_len,
|
||
|
unsigned char *out, size_t *out_size)
|
||
|
{
|
||
|
EVP_MD_CTX *mdctx;
|
||
|
unsigned int s;
|
||
|
int ret = 1;
|
||
|
|
||
|
mdctx = EVP_MD_CTX_create();
|
||
|
assert(mdctx);
|
||
|
ret &= EVP_DigestInit_ex(mdctx, md, NULL);
|
||
|
ret &= EVP_DigestUpdate(mdctx, salt, salt_len);
|
||
|
ret &= EVP_DigestUpdate(mdctx, block, len);
|
||
|
ret &= EVP_DigestFinal_ex(mdctx, out, &s);
|
||
|
EVP_MD_CTX_destroy(mdctx);
|
||
|
assert(ret == 1);
|
||
|
if (out_size) {
|
||
|
*out_size = s;
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
int hash_blocks(const EVP_MD *md,
|
||
|
const unsigned char *in, size_t in_size,
|
||
|
unsigned char *out, size_t *out_size,
|
||
|
const unsigned char *salt, size_t salt_size,
|
||
|
size_t block_size)
|
||
|
{
|
||
|
size_t s;
|
||
|
*out_size = 0;
|
||
|
for (size_t i = 0; i < in_size; i += block_size) {
|
||
|
hash_block(md, in + i, block_size, salt, salt_size, out, &s);
|
||
|
out += s;
|
||
|
*out_size += s;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
int hash_chunk(void *priv, const void *data, int len)
|
||
|
{
|
||
|
struct sparse_hash_ctx *ctx = (struct sparse_hash_ctx *)priv;
|
||
|
assert(len % ctx->block_size == 0);
|
||
|
if (data) {
|
||
|
size_t s;
|
||
|
hash_blocks(ctx->md, (const unsigned char *)data, len,
|
||
|
ctx->hashes, &s,
|
||
|
ctx->salt, ctx->salt_size, ctx->block_size);
|
||
|
ctx->hashes += s;
|
||
|
} else {
|
||
|
for (size_t i = 0; i < (size_t)len; i += ctx->block_size) {
|
||
|
memcpy(ctx->hashes, ctx->zero_block_hash, ctx->hash_size);
|
||
|
ctx->hashes += ctx->hash_size;
|
||
|
}
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
void usage(void)
|
||
|
{
|
||
|
printf("usage: build_verity_tree [ <options> ] -s <size> | <data> <verity>\n"
|
||
|
"options:\n"
|
||
|
" -a,--salt-str=<string> set salt to <string>\n"
|
||
|
" -A,--salt-hex=<hex digits> set salt to <hex digits>\n"
|
||
|
" -h show this help\n"
|
||
|
" -s,--verity-size=<data size> print the size of the verity tree\n"
|
||
|
" -S treat <data image> as a sparse file\n"
|
||
|
);
|
||
|
}
|
||
|
|
||
|
int main(int argc, char **argv)
|
||
|
{
|
||
|
char *data_filename;
|
||
|
char *verity_filename;
|
||
|
unsigned char *salt = NULL;
|
||
|
size_t salt_size = 0;
|
||
|
bool sparse = false;
|
||
|
size_t block_size = 4096;
|
||
|
size_t calculate_size = 0;
|
||
|
|
||
|
while (1) {
|
||
|
const static struct option long_options[] = {
|
||
|
{"salt-str", required_argument, 0, 'a'},
|
||
|
{"salt-hex", required_argument, 0, 'A'},
|
||
|
{"help", no_argument, 0, 'h'},
|
||
|
{"sparse", no_argument, 0, 'S'},
|
||
|
{"verity-size", required_argument, 0, 's'},
|
||
|
};
|
||
|
int c = getopt_long(argc, argv, "a:A:hSs:", long_options, NULL);
|
||
|
if (c < 0) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
switch (c) {
|
||
|
case 'a':
|
||
|
salt_size = strlen(optarg);
|
||
|
salt = new unsigned char[salt_size]();
|
||
|
if (salt == NULL) {
|
||
|
FATAL("failed to allocate memory for salt\n");
|
||
|
}
|
||
|
memcpy(salt, optarg, salt_size);
|
||
|
break;
|
||
|
case 'A': {
|
||
|
BIGNUM *bn = NULL;
|
||
|
if(!BN_hex2bn(&bn, optarg)) {
|
||
|
FATAL("failed to convert salt from hex\n");
|
||
|
}
|
||
|
salt_size = BN_num_bytes(bn);
|
||
|
salt = new unsigned char[salt_size]();
|
||
|
if (salt == NULL) {
|
||
|
FATAL("failed to allocate memory for salt\n");
|
||
|
}
|
||
|
if((size_t)BN_bn2bin(bn, salt) != salt_size) {
|
||
|
FATAL("failed to convert salt to bytes\n");
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 'h':
|
||
|
usage();
|
||
|
return 1;
|
||
|
case 'S':
|
||
|
sparse = true;
|
||
|
break;
|
||
|
case 's':
|
||
|
calculate_size = strtoul(optarg, NULL, 0);
|
||
|
break;
|
||
|
case '?':
|
||
|
usage();
|
||
|
return 1;
|
||
|
default:
|
||
|
abort();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
argc -= optind;
|
||
|
argv += optind;
|
||
|
|
||
|
const EVP_MD *md = EVP_sha256();
|
||
|
if (!md) {
|
||
|
FATAL("failed to get digest\n");
|
||
|
}
|
||
|
|
||
|
size_t hash_size = EVP_MD_size(md);
|
||
|
assert(hash_size * 2 < block_size);
|
||
|
|
||
|
if (!salt || !salt_size) {
|
||
|
salt_size = hash_size;
|
||
|
salt = new unsigned char[salt_size];
|
||
|
if (salt == NULL) {
|
||
|
FATAL("failed to allocate memory for salt\n");
|
||
|
}
|
||
|
|
||
|
int random_fd = open("/dev/urandom", O_RDONLY);
|
||
|
if (random_fd < 0) {
|
||
|
FATAL("failed to open /dev/urandom\n");
|
||
|
}
|
||
|
|
||
|
ssize_t ret = read(random_fd, salt, salt_size);
|
||
|
if (ret != (ssize_t)salt_size) {
|
||
|
FATAL("failed to read %zu bytes from /dev/urandom: %zd %d\n", salt_size, ret, errno);
|
||
|
}
|
||
|
close(random_fd);
|
||
|
}
|
||
|
|
||
|
if (calculate_size) {
|
||
|
if (argc != 0) {
|
||
|
usage();
|
||
|
return 1;
|
||
|
}
|
||
|
size_t verity_blocks = 0;
|
||
|
size_t level_blocks;
|
||
|
int levels = 0;
|
||
|
do {
|
||
|
level_blocks = verity_tree_blocks(calculate_size, block_size, hash_size, levels);
|
||
|
levels++;
|
||
|
verity_blocks += level_blocks;
|
||
|
} while (level_blocks > 1);
|
||
|
|
||
|
printf("%zu\n", verity_blocks * block_size);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
if (argc != 2) {
|
||
|
usage();
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
data_filename = argv[0];
|
||
|
verity_filename = argv[1];
|
||
|
|
||
|
int fd = open(data_filename, O_RDONLY);
|
||
|
if (fd < 0) {
|
||
|
FATAL("failed to open %s\n", data_filename);
|
||
|
}
|
||
|
|
||
|
struct sparse_file *file;
|
||
|
if (sparse) {
|
||
|
file = sparse_file_import(fd, false, false);
|
||
|
} else {
|
||
|
file = sparse_file_import_auto(fd, false);
|
||
|
}
|
||
|
|
||
|
if (!file) {
|
||
|
FATAL("failed to read file %s\n", data_filename);
|
||
|
}
|
||
|
|
||
|
int64_t len = sparse_file_len(file, false, false);
|
||
|
if (len % block_size != 0) {
|
||
|
FATAL("file size %" PRIu64 " is not a multiple of %zu bytes\n",
|
||
|
len, block_size);
|
||
|
}
|
||
|
|
||
|
int levels = 0;
|
||
|
size_t verity_blocks = 0;
|
||
|
size_t level_blocks;
|
||
|
|
||
|
do {
|
||
|
level_blocks = verity_tree_blocks(len, block_size, hash_size, levels);
|
||
|
levels++;
|
||
|
verity_blocks += level_blocks;
|
||
|
} while (level_blocks > 1);
|
||
|
|
||
|
unsigned char *verity_tree = new unsigned char[verity_blocks * block_size]();
|
||
|
unsigned char **verity_tree_levels = new unsigned char *[levels + 1]();
|
||
|
size_t *verity_tree_level_blocks = new size_t[levels]();
|
||
|
if (verity_tree == NULL || verity_tree_levels == NULL || verity_tree_level_blocks == NULL) {
|
||
|
FATAL("failed to allocate memory for verity tree\n");
|
||
|
}
|
||
|
|
||
|
unsigned char *ptr = verity_tree;
|
||
|
for (int i = levels - 1; i >= 0; i--) {
|
||
|
verity_tree_levels[i] = ptr;
|
||
|
verity_tree_level_blocks[i] = verity_tree_blocks(len, block_size, hash_size, i);
|
||
|
ptr += verity_tree_level_blocks[i] * block_size;
|
||
|
}
|
||
|
assert(ptr == verity_tree + verity_blocks * block_size);
|
||
|
assert(verity_tree_level_blocks[levels - 1] == 1);
|
||
|
|
||
|
unsigned char zero_block_hash[hash_size];
|
||
|
unsigned char zero_block[block_size];
|
||
|
memset(zero_block, 0, block_size);
|
||
|
hash_block(md, zero_block, block_size, salt, salt_size, zero_block_hash, NULL);
|
||
|
|
||
|
unsigned char root_hash[hash_size];
|
||
|
verity_tree_levels[levels] = root_hash;
|
||
|
|
||
|
struct sparse_hash_ctx ctx;
|
||
|
ctx.hashes = verity_tree_levels[0];
|
||
|
ctx.salt = salt;
|
||
|
ctx.salt_size = salt_size;
|
||
|
ctx.hash_size = hash_size;
|
||
|
ctx.block_size = block_size;
|
||
|
ctx.zero_block_hash = zero_block_hash;
|
||
|
ctx.md = md;
|
||
|
|
||
|
sparse_file_callback(file, false, false, hash_chunk, &ctx);
|
||
|
|
||
|
sparse_file_destroy(file);
|
||
|
close(fd);
|
||
|
|
||
|
for (int i = 0; i < levels; i++) {
|
||
|
size_t out_size;
|
||
|
hash_blocks(md,
|
||
|
verity_tree_levels[i], verity_tree_level_blocks[i] * block_size,
|
||
|
verity_tree_levels[i + 1], &out_size,
|
||
|
salt, salt_size, block_size);
|
||
|
if (i < levels - 1) {
|
||
|
assert(div_round_up(out_size, block_size) == verity_tree_level_blocks[i + 1]);
|
||
|
} else {
|
||
|
assert(out_size == hash_size);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for (size_t i = 0; i < hash_size; i++) {
|
||
|
printf("%02x", root_hash[i]);
|
||
|
}
|
||
|
printf(" ");
|
||
|
for (size_t i = 0; i < salt_size; i++) {
|
||
|
printf("%02x", salt[i]);
|
||
|
}
|
||
|
printf("\n");
|
||
|
|
||
|
fd = open(verity_filename, O_WRONLY|O_CREAT, 0666);
|
||
|
if (fd < 0) {
|
||
|
FATAL("failed to open output file '%s'\n", verity_filename);
|
||
|
}
|
||
|
write(fd, verity_tree, verity_blocks * block_size);
|
||
|
close(fd);
|
||
|
|
||
|
delete[] verity_tree_levels;
|
||
|
delete[] verity_tree_level_blocks;
|
||
|
delete[] verity_tree;
|
||
|
delete[] salt;
|
||
|
}
|