// SPDX-License-Identifier: GPL-2.0+
/*
* The 'fsverity setup' command
*
* Copyright (C) 2018 Google LLC
*
* Written by Eric Biggers.
*/
#include <fcntl.h>
#include <getopt.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "commands.h"
#include "fsverity_uapi.h"
#include "fsveritysetup.h"
#include "hash_algs.h"
enum {
OPT_HASH,
OPT_SALT,
OPT_BLOCKSIZE,
OPT_SIGNING_KEY,
OPT_SIGNING_CERT,
OPT_SIGNATURE,
OPT_ELIDE,
OPT_PATCH,
};
static const struct option longopts[] = {
{"hash", required_argument, NULL, OPT_HASH},
{"salt", required_argument, NULL, OPT_SALT},
{"blocksize", required_argument, NULL, OPT_BLOCKSIZE},
{"signing-key", required_argument, NULL, OPT_SIGNING_KEY},
{"signing-cert", required_argument, NULL, OPT_SIGNING_CERT},
{"signature", required_argument, NULL, OPT_SIGNATURE},
{"elide", required_argument, NULL, OPT_ELIDE},
{"patch", required_argument, NULL, OPT_PATCH},
{NULL, 0, NULL, 0}
};
/* Parse the --blocksize=BLOCKSIZE option */
static bool parse_blocksize_option(const char *opt, int *blocksize_ret)
{
char *end;
unsigned long n = strtoul(opt, &end, 10);
if (n <= 0 || n >= INT32_MAX || *end || !is_power_of_2(n)) {
error_msg("Invalid block size: %s. Must be power of 2", opt);
return false;
}
*blocksize_ret = n;
return true;
}
#define FS_VERITY_MAX_LEVELS 64
/*
* Calculate the depth of the Merkle tree, then create a map from level to the
* block offset at which that level's hash blocks start. Level 'depth - 1' is
* the root and is stored first in the file, in the first block following the
* original data. Level 0 is the "leaf" level: it's directly "above" the data
* blocks and is stored last in the file.
*/
static void compute_tree_layout(u64 data_size, u64 tree_offset, int blockbits,
unsigned int hashes_per_block,
u64 hash_lvl_region_idx[FS_VERITY_MAX_LEVELS],
int *depth_ret, u64 *tree_end_ret)
{
u64 blocks = data_size >> blockbits;
u64 offset = tree_offset >> blockbits;
int depth = 0;
int i;
ASSERT(data_size > 0);
ASSERT(data_size % (1 << blockbits) == 0);
ASSERT(tree_offset % (1 << blockbits) == 0);
ASSERT(hashes_per_block >= 2);
while (blocks > 1) {
ASSERT(depth < FS_VERITY_MAX_LEVELS);
blocks = DIV_ROUND_UP(blocks, hashes_per_block);
hash_lvl_region_idx[depth++] = blocks;
}
for (i = depth - 1; i >= 0; i--) {
u64 next_count = hash_lvl_region_idx[i];
hash_lvl_region_idx[i] = offset;
offset += next_count;
}
*depth_ret = depth;
*tree_end_ret = offset << blockbits;
}
/*
* Build a Merkle tree (hash tree) over the data of a file.
*
* @params: Block size, hashes per block, and salt
* @hash: Handle for the hash algorithm
* @data_file: input data file
* @data_size: size of data file in bytes; must be aligned to ->blocksize
* @tree_file: output tree file
* @tree_offset: byte offset in tree file at which to write the tree;
* must be aligned to ->blocksize
* @tree_end_ret: On success, the byte offset in the tree file of the end of the
* tree is written here
* @root_hash_ret: On success, the Merkle tree root hash is written here
*
* Return: exit status code (0 on success, nonzero on failure)
*/
static int build_merkle_tree(const struct fsveritysetup_params *params,
struct hash_ctx *hash,
struct filedes *data_file, u64 data_size,
struct filedes *tree_file, u64 tree_offset,
u64 *tree_end_ret, u8 *root_hash_ret)
{
const unsigned int digest_size = hash->alg->digest_size;
int depth;
u64 hash_lvl_region_idx[FS_VERITY_MAX_LEVELS];
u8 *data_to_hash = NULL;
u8 *pending_hashes = NULL;
unsigned int pending_hash_bytes;
u64 nr_hashes_at_this_lvl;
int lvl;
int status;
compute_tree_layout(data_size, tree_offset, params->blockbits,
params->hashes_per_block, hash_lvl_region_idx,
&depth, tree_end_ret);
/* Allocate block buffers */
data_to_hash = xmalloc(params->blocksize);
pending_hashes = xmalloc(params->blocksize);
pending_hash_bytes = 0;
nr_hashes_at_this_lvl = data_size >> params->blockbits;
/*
* Generate each level of the Merkle tree, starting at the leaf level
* ('lvl == 0') and ascending to the root node ('lvl == depth - 1').
* Then at the end ('lvl == depth'), calculate the root node's hash.
*/
for (lvl = 0; lvl <= depth; lvl++) {
u64 i;
for (i = 0; i < nr_hashes_at_this_lvl; i++) {
struct filedes *file;
u64 blk_idx;
hash_init(hash);
hash_update(hash, params->salt, params->saltlen);
if (lvl == 0) {
/* Leaf: hashing a data block */
file = data_file;
blk_idx = i;
} else {
/* Non-leaf: hashing a hash block */
file = tree_file;
blk_idx = hash_lvl_region_idx[lvl - 1] + i;
}
if (!full_pread(file, data_to_hash, params->blocksize,
blk_idx << params->blockbits))
goto out_err;
hash_update(hash, data_to_hash, params->blocksize);
hash_final(hash, &pending_hashes[pending_hash_bytes]);
pending_hash_bytes += digest_size;
if (lvl == depth) {
/* Root hash */
ASSERT(nr_hashes_at_this_lvl == 1);
ASSERT(pending_hash_bytes == digest_size);
memcpy(root_hash_ret, pending_hashes,
digest_size);
status = 0;
goto out;
}
if (pending_hash_bytes + digest_size > params->blocksize
|| i + 1 == nr_hashes_at_this_lvl) {
/* Flush the pending hash block */
memset(&pending_hashes[pending_hash_bytes], 0,
params->blocksize - pending_hash_bytes);
blk_idx = hash_lvl_region_idx[lvl] +
(i / params->hashes_per_block);
if (!full_pwrite(tree_file,
pending_hashes,
params->blocksize,
blk_idx << params->blockbits))
goto out_err;
pending_hash_bytes = 0;
}
}
nr_hashes_at_this_lvl = DIV_ROUND_UP(nr_hashes_at_this_lvl,
params->hashes_per_block);
}
ASSERT(0); /* unreachable; should exit via "Root hash" case above */
out_err:
status = 1;
out:
free(data_to_hash);
free(pending_hashes);
return status;
}
/*
* Append to the buffer @*buf_p an extension (variable-length metadata) item of
* type @type, containing the data @ext of length @extlen bytes.
*/
void fsverity_append_extension(void **buf_p, int type,
const void *ext, size_t extlen)
{
void *buf = *buf_p;
struct fsverity_extension *hdr = buf;
hdr->type = cpu_to_le16(type);
hdr->length = cpu_to_le32(sizeof(*hdr) + extlen);
hdr->reserved = 0;
buf += sizeof(*hdr);
memcpy(buf, ext, extlen);
buf += extlen;
memset(buf, 0, -extlen & 7);
buf += -extlen & 7;
ASSERT(buf - *buf_p == FSVERITY_EXTLEN(extlen));
*buf_p = buf;
}
/*
* Append the authenticated portion of the fs-verity descriptor to 'out', in the
* process updating 'hash' with the data written.
*/
static int append_fsverity_descriptor(const struct fsveritysetup_params *params,
u64 filesize, const u8 *root_hash,
struct filedes *out,
struct hash_ctx *hash)
{
size_t desc_auth_len;
void *buf;
struct fsverity_descriptor *desc;
u16 auth_ext_count;
int status;
desc_auth_len = sizeof(*desc);
desc_auth_len += FSVERITY_EXTLEN(params->hash_alg->digest_size);
if (params->saltlen)
desc_auth_len += FSVERITY_EXTLEN(params->saltlen);
desc_auth_len += total_elide_patch_ext_length(params);
desc = buf = xzalloc(desc_auth_len);
memcpy(desc->magic, FS_VERITY_MAGIC, sizeof(desc->magic));
desc->major_version = 1;
desc->minor_version = 0;
desc->log_data_blocksize = params->blockbits;
desc->log_tree_blocksize = params->blockbits;
desc->data_algorithm = cpu_to_le16(params->hash_alg -
fsverity_hash_algs);
desc->tree_algorithm = desc->data_algorithm;
desc->orig_file_size = cpu_to_le64(filesize);
auth_ext_count = 1; /* root hash */
if (params->saltlen)
auth_ext_count++;
auth_ext_count += params->num_elisions_and_patches;
desc->auth_ext_count = cpu_to_le16(auth_ext_count);
buf += sizeof(*desc);
fsverity_append_extension(&buf, FS_VERITY_EXT_ROOT_HASH,
root_hash, params->hash_alg->digest_size);
if (params->saltlen)
fsverity_append_extension(&buf, FS_VERITY_EXT_SALT,
params->salt, params->saltlen);
append_elide_patch_exts(&buf, params);
ASSERT(buf - (void *)desc == desc_auth_len);
hash_update(hash, desc, desc_auth_len);
if (!full_write(out, desc, desc_auth_len))
goto out_err;
status = 0;
out:
free(desc);
return status;
out_err:
status = 1;
goto out;
}
/*
* Append any needed unauthenticated extension items: currently, just possibly a
* PKCS7_SIGNATURE item containing the signed file measurement.
*/
static int
append_unauthenticated_extensions(struct filedes *out,
const struct fsveritysetup_params *params,
const u8 *measurement)
{
u16 unauth_ext_count = 0;
struct {
__le16 unauth_ext_count;
__le16 pad[3];
} hdr;
bool have_sig = params->signing_key_file || params->signature_file;
if (have_sig)
unauth_ext_count++;
ASSERT(sizeof(hdr) % 8 == 0);
memset(&hdr, 0, sizeof(hdr));
hdr.unauth_ext_count = cpu_to_le16(unauth_ext_count);
if (!full_write(out, &hdr, sizeof(hdr)))
return 1;
if (have_sig)
return append_signed_measurement(out, params, measurement);
return 0;
}
static int append_footer(struct filedes *out, u64 desc_offset)
{
struct fsverity_footer ftr;
u32 offset = (out->pos + sizeof(ftr)) - desc_offset;
ftr.desc_reverse_offset = cpu_to_le32(offset);
memcpy(ftr.magic, FS_VERITY_MAGIC, sizeof(ftr.magic));
if (!full_write(out, &ftr, sizeof(ftr)))
return 1;
return 0;
}
static int fsveritysetup(const char *infile, const char *outfile,
const struct fsveritysetup_params *params)
{
struct filedes _in = { .fd = -1 };
struct filedes _out = { .fd = -1 };
struct filedes _tmp = { .fd = -1 };
struct hash_ctx *hash = NULL;
struct filedes *in = &_in, *out = &_out, *src;
u64 filesize;
u64 aligned_filesize;
u64 src_filesize;
u64 tree_end_offset;
u8 root_hash[FS_VERITY_MAX_DIGEST_SIZE];
u8 measurement[FS_VERITY_MAX_DIGEST_SIZE];
char hash_hex[FS_VERITY_MAX_DIGEST_SIZE * 2 + 1];
int status;
if (!open_file(in, infile, (infile == outfile ? O_RDWR : O_RDONLY), 0))
goto out_err;
if (!get_file_size(in, &filesize))
goto out_err;
if (filesize <= 0) {
error_msg("input file is empty: '%s'", infile);
goto out_err;
}
if (infile == outfile) {
/*
* Invoked with one file argument: we're appending verity
* metadata to an existing file.
*/
out = in;
if (!filedes_seek(out, filesize, SEEK_SET))
goto out_err;
} else {
/*
* Invoked with two file arguments: we're copying the first file
* to the second file, then appending verity metadata to it.
*/
if (!open_file(out, outfile, O_RDWR|O_CREAT|O_TRUNC, 0644))
goto out_err;
if (!copy_file_data(in, out, filesize))
goto out_err;
}
/* Zero-pad the output file to the next block boundary */
aligned_filesize = ALIGN(filesize, params->blocksize);
if (!write_zeroes(out, aligned_filesize - filesize))
goto out_err;
if (params->num_elisions_and_patches) {
/* Merkle tree is built over temporary elided/patched file */
src = &_tmp;
if (!apply_elisions_and_patches(params, in, filesize,
src, &src_filesize))
goto out_err;
} else {
/* Merkle tree is built over original file */
src = out;
src_filesize = aligned_filesize;
}
hash = hash_create(params->hash_alg);
/* Build the file's Merkle tree and calculate its root hash */
status = build_merkle_tree(params, hash, src, src_filesize,
out, aligned_filesize,
&tree_end_offset, root_hash);
if (status)
goto out;
if (!filedes_seek(out, tree_end_offset, SEEK_SET))
goto out_err;
/* Append the additional needed metadata */
hash_init(hash);
status = append_fsverity_descriptor(params, filesize, root_hash,
out, hash);
if (status)
goto out;
hash_final(hash, measurement);
status = append_unauthenticated_extensions(out, params, measurement);
if (status)
goto out;
status = append_footer(out, tree_end_offset);
if (status)
goto out;
bin2hex(measurement, params->hash_alg->digest_size, hash_hex);
printf("File measurement: %s:%s\n", params->hash_alg->name, hash_hex);
status = 0;
out:
hash_free(hash);
if (status != 0 && out->fd >= 0) {
/* Error occurred; undo what we wrote */
if (in == out)
(void)ftruncate(out->fd, filesize);
else
out->autodelete = true;
}
filedes_close(&_in);
filedes_close(&_tmp);
if (!filedes_close(&_out) && status == 0)
status = 1;
return status;
out_err:
status = 1;
goto out;
}
int fsverity_cmd_setup(const struct fsverity_command *cmd,
int argc, char *argv[])
{
struct fsveritysetup_params params = {
.hash_alg = DEFAULT_HASH_ALG,
};
STRING_LIST(elide_opts);
STRING_LIST(patch_opts);
int c;
int status;
while ((c = getopt_long(argc, argv, "", longopts, NULL)) != -1) {
switch (c) {
case OPT_HASH:
params.hash_alg = find_hash_alg_by_name(optarg);
if (!params.hash_alg)
goto out_usage;
break;
case OPT_SALT:
if (params.salt) {
error_msg("--salt can only be specified once");
goto out_usage;
}
params.saltlen = strlen(optarg) / 2;
params.salt = xmalloc(params.saltlen);
if (!hex2bin(optarg, params.salt, params.saltlen)) {
error_msg("salt is not a valid hex string");
goto out_usage;
}
break;
case OPT_BLOCKSIZE:
if (!parse_blocksize_option(optarg, ¶ms.blocksize))
goto out_usage;
break;
case OPT_SIGNING_KEY:
params.signing_key_file = optarg;
break;
case OPT_SIGNING_CERT:
params.signing_cert_file = optarg;
break;
case OPT_SIGNATURE:
params.signature_file = optarg;
break;
case OPT_ELIDE:
string_list_append(&elide_opts, optarg);
break;
case OPT_PATCH:
string_list_append(&patch_opts, optarg);
break;
default:
goto out_usage;
}
}
argv += optind;
argc -= optind;
if (argc != 1 && argc != 2)
goto out_usage;
ASSERT(params.hash_alg->digest_size <= FS_VERITY_MAX_DIGEST_SIZE);
if (params.blocksize == 0) {
params.blocksize = sysconf(_SC_PAGESIZE);
if (params.blocksize <= 0 || !is_power_of_2(params.blocksize)) {
fprintf(stderr,
"Warning: invalid _SC_PAGESIZE (%d). Assuming 4K blocks.\n",
params.blocksize);
params.blocksize = 4096;
}
}
params.blockbits = ilog2(params.blocksize);
params.hashes_per_block = params.blocksize /
params.hash_alg->digest_size;
if (params.hashes_per_block < 2) {
error_msg("block size of %d bytes is too small for %s hash",
params.blocksize, params.hash_alg->name);
goto out_err;
}
if (params.signing_cert_file && !params.signing_key_file) {
error_msg("--signing-cert was given, but --signing-key was not.\n"
" You must provide the certificate's private key file using --signing-key.");
goto out_err;
}
if ((params.signing_key_file || params.signature_file) &&
!params.hash_alg->cryptographic) {
error_msg("Signing a file using '%s' checksums does not make sense\n"
" because '%s' is not a cryptographically secure hash algorithm.",
params.hash_alg->name, params.hash_alg->name);
goto out_err;
}
if (!load_elisions_and_patches(&elide_opts, &patch_opts, ¶ms))
goto out_err;
status = fsveritysetup(argv[0], argv[argc - 1], ¶ms);
out:
free(params.salt);
free_elisions_and_patches(¶ms);
string_list_destroy(&elide_opts);
string_list_destroy(&patch_opts);
return status;
out_err:
status = 1;
goto out;
out_usage:
usage(cmd, stderr);
status = 2;
goto out;
}