| // 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; |
| } |