diff options
author | Eric Biggers <ebiggers@google.com> | 2018-06-27 15:01:06 -0700 |
---|---|---|
committer | Eric Biggers <ebiggers@google.com> | 2018-06-27 15:01:06 -0700 |
commit | 431c67bd2e5c420dd7026966185a685bd6d04a19 (patch) | |
tree | d7ea578c2ec52ba1c73cf1f40747c571c8493558 | |
parent | 3b40b2e3a101b63784752eeb4d56fec0d3f43e23 (diff) | |
download | fsverity-utils-431c67bd2e5c420dd7026966185a685bd6d04a19.tar.gz |
Rewrite fsveritysetup in C
Make fsveritysetup a subcommand 'setup' of the 'fsverity' program which
previously had just the 'enable' and 'set_measurement' commands.
When signing the file measurement, use libcrypto directly instead of
invoking the 'openssl' binary.
Similarly, build the Merkle tree in C code (using libcrypto for SHA-256,
or zlib for CRC-32) rather than invoking the 'veritysetup' binary.
Other improvements over the original Python script are included as well.
Signed-off-by: Eric Biggers <ebiggers@google.com>
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 17 | ||||
-rw-r--r-- | cmd_enable.c | 37 | ||||
-rw-r--r-- | cmd_set_measurement.c | 87 | ||||
-rw-r--r-- | cmd_setup.c | 534 | ||||
-rw-r--r-- | commands.h | 20 | ||||
-rw-r--r-- | debian/control | 6 | ||||
-rw-r--r-- | debian/copyright | 2 | ||||
-rwxr-xr-x | debian/rules | 2 | ||||
-rw-r--r-- | elide_patch.c | 307 | ||||
-rw-r--r-- | fsverity.c | 268 | ||||
-rw-r--r-- | fsverity_api.h | 24 | ||||
-rw-r--r-- | fsverity_sys_decls.h | 91 | ||||
-rwxr-xr-x | fsveritysetup | 704 | ||||
-rw-r--r-- | fsveritysetup.h | 43 | ||||
-rw-r--r-- | hash_algs.c | 181 | ||||
-rw-r--r-- | hash_algs.h | 63 | ||||
-rwxr-xr-x | mkfsverity.sh | 2 | ||||
-rw-r--r-- | sign.c | 388 | ||||
-rw-r--r-- | util.c | 362 | ||||
-rw-r--r-- | util.h | 149 |
21 files changed, 2388 insertions, 900 deletions
@@ -1,4 +1,5 @@ fsverity +*.o tags cscope.* ncscope.* @@ -1,14 +1,23 @@ -CFLAGS := -O2 -Wall EXE := fsverity +CFLAGS := -O2 -Wall +CPPFLAGS := -D_FILE_OFFSET_BITS=64 +LDLIBS := -lcrypto -lz DESTDIR := /usr/local +SRC := $(wildcard *.c) +OBJ := $(SRC:.c=.o) +HDRS := $(wildcard *.h) all:$(EXE) +$(EXE):$(OBJ) + +$(OBJ): %.o: %.c $(HDRS) + clean: - rm -f $(EXE) + rm -f $(EXE) $(OBJ) install:all - install -Dm755 -t $(DESTDIR)/bin $(EXE) fsveritysetup \ + install -Dm755 -t $(DESTDIR)/bin $(EXE) \ mkfsverity.sh full-run-fsverity.sh -.PHONY: all clean +.PHONY: all clean install diff --git a/cmd_enable.c b/cmd_enable.c new file mode 100644 index 0000000..6d28297 --- /dev/null +++ b/cmd_enable.c @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * The 'fsverity enable' command + * + * Copyright (C) 2018 Google, Inc. + * + * Written by Eric Biggers, 2018. + */ + +#include <fcntl.h> +#include <sys/ioctl.h> + +#include "commands.h" +#include "fsverity_sys_decls.h" + +int fsverity_cmd_enable(const struct fsverity_command *cmd, + int argc, char *argv[]) +{ + struct filedes file; + + if (argc != 2) { + usage(cmd, stderr); + return 2; + } + + if (!open_file(&file, argv[1], O_RDONLY, 0)) + return 1; + if (ioctl(file.fd, FS_IOC_ENABLE_VERITY, NULL) != 0) { + error_msg_errno("FS_IOC_ENABLE_VERITY failed on '%s'", + file.name); + filedes_close(&file); + return 1; + } + if (!filedes_close(&file)) + return 1; + return 0; +} diff --git a/cmd_set_measurement.c b/cmd_set_measurement.c new file mode 100644 index 0000000..db237b8 --- /dev/null +++ b/cmd_set_measurement.c @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * The 'fsverity set_measurement' command + * + * Copyright (C) 2018 Google, Inc. + * + * Written by Eric Biggers, 2018. + */ + +#include <fcntl.h> +#include <getopt.h> +#include <stdlib.h> +#include <sys/ioctl.h> + +#include "commands.h" +#include "fsverity_sys_decls.h" +#include "hash_algs.h" + +enum { + OPT_HASH, +}; + +static const struct option longopts[] = { + {"hash", required_argument, NULL, OPT_HASH}, + {NULL, 0, NULL, 0} +}; + +int fsverity_cmd_set_measurement(const struct fsverity_command *cmd, + int argc, char *argv[]) +{ + const struct fsverity_hash_alg *alg = DEFAULT_HASH_ALG; + struct fsverity_measurement *measurement = NULL; + struct filedes file; + int c; + int status; + + while ((c = getopt_long(argc, argv, "", longopts, NULL)) != -1) { + switch (c) { + case OPT_HASH: + alg = find_hash_alg(optarg); + if (!alg) + goto out_usage; + break; + default: + goto out_usage; + } + } + argv += optind; + argc -= optind; + + if (argc != 2) + goto out_usage; + + measurement = xzalloc(sizeof(*measurement) + alg->digest_size); + measurement->digest_algorithm = alg - fsverity_hash_algs; + measurement->digest_size = alg->digest_size; + if (!hex2bin(argv[1], measurement->digest, alg->digest_size)) { + error_msg("Invalid EXPECTED_MEASUREMENT hex string.\n" + " Expected %u-character hex string for hash algorithm '%s'.", + alg->digest_size * 2, alg->name); + goto out_usage; + } + + if (!open_file(&file, argv[0], O_RDONLY, 0)) + goto out_err; + if (ioctl(file.fd, FS_IOC_SET_VERITY_MEASUREMENT, measurement) != 0) { + error_msg_errno("FS_IOC_SET_VERITY_MEASUREMENT failed on '%s'", + file.name); + filedes_close(&file); + goto out_err; + } + if (!filedes_close(&file)) + goto out_err; + status = 0; +out: + free(measurement); + return status; + +out_err: + status = 1; + goto out; + +out_usage: + usage(cmd, stderr); + status = 2; + goto out; +} diff --git a/cmd_setup.c b/cmd_setup.c new file mode 100644 index 0000000..02ecf59 --- /dev/null +++ b/cmd_setup.c @@ -0,0 +1,534 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * The 'fsverity setup' command + * + * Copyright (C) 2018 Google, Inc. + * + * Written by Eric Biggers, 2018. + */ + +#include <fcntl.h> +#include <getopt.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "commands.h" +#include "fsverity_sys_decls.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, just before the fs-verity footer. + */ +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 footer to 'out', in the + * process updating 'hash' with the data written. + */ +static int append_auth_footer(const struct fsveritysetup_params *params, + u64 filesize, struct filedes *out, + struct hash_ctx *hash) +{ + size_t ftr_auth_len; + void *buf; + struct fsverity_footer *ftr; + int status; + + ftr_auth_len = sizeof(*ftr); + if (params->saltlen) + ftr_auth_len += FSVERITY_EXTLEN(params->saltlen); + ftr_auth_len += total_elide_patch_ext_length(params); + ftr = buf = xzalloc(ftr_auth_len); + + memcpy(ftr->magic, FS_VERITY_MAGIC, sizeof(ftr->magic)); + ftr->major_version = FS_VERITY_MAJOR; + ftr->minor_version = FS_VERITY_MINOR; + ftr->log_blocksize = params->blockbits; + /* TODO: should we be storing 'log_hash_blocksize' instead? */ + if (!is_power_of_2(params->hashes_per_block)) { + error_msg("Unsupported hashes_per_block (%u); must be a power of 2", + params->hashes_per_block); + goto out_err; + } + ftr->log_arity = ilog2(params->hashes_per_block); + ftr->meta_algorithm = cpu_to_le16(params->hash_alg - + fsverity_hash_algs); + ftr->data_algorithm = ftr->meta_algorithm; + ftr->size = cpu_to_le64(filesize); + + ftr->authenticated_ext_count = params->num_elisions_and_patches; + if (params->saltlen) + ftr->authenticated_ext_count++; + + ftr->unauthenticated_ext_count = 0; + if (params->signing_key_file || params->signature_file) + ftr->unauthenticated_ext_count++; + + buf += sizeof(*ftr); + if (params->saltlen) + fsverity_append_extension(&buf, FS_VERITY_EXT_SALT, + params->salt, params->saltlen); + append_elide_patch_exts(&buf, params); + ASSERT(buf - (void *)ftr == ftr_auth_len); + + hash_update(hash, ftr, ftr_auth_len); + if (!full_write(out, ftr, ftr_auth_len)) + goto out_err; + status = 0; +out: + free(ftr); + return status; + +out_err: + status = 1; + goto out; +} + +static int append_ftr_reverse_offset(struct filedes *out, u64 ftr_offset) +{ + __le32 offs; + + offs = cpu_to_le32(out->pos + sizeof(offs) - ftr_offset); + if (!full_write(out, &offs, sizeof(offs))) + 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, *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. + */ + out = &_out; + 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; + + /* + * Append the fixed-size portion of the footer and any authenticated + * extensions, then calculate the file measurement: the hash of the + * authenticated footer portion and the Merkle tree root hash. + */ + if (!filedes_seek(out, tree_end_offset, SEEK_SET)) + goto out_err; + hash_init(hash); + status = append_auth_footer(params, filesize, out, hash); + if (status) + goto out; + hash_update(hash, root_hash, params->hash_alg->digest_size); + hash_final(hash, measurement); + + /* If requested, append the signed file measurement */ + status = append_signed_measurement(out, params, measurement); + if (status) + goto out; + + /* Finish by appending the 'ftr_reverse_offset' field */ + status = append_ftr_reverse_offset(out, tree_end_offset); + if (status) + goto out; + + bin2hex(root_hash, params->hash_alg->digest_size, hash_hex); + printf("Merkle root hash: %s\n", hash_hex); + bin2hex(measurement, params->hash_alg->digest_size, hash_hex); + printf("fs-verity measurement: %s\n", hash_hex); + status = 0; +out: + hash_free(hash); + 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(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; +} diff --git a/commands.h b/commands.h new file mode 100644 index 0000000..4999ba3 --- /dev/null +++ b/commands.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#ifndef COMMANDS_H +#define COMMANDS_H + +#include <stdio.h> + +#include "util.h" + +struct fsverity_command; + +void usage(const struct fsverity_command *cmd, FILE *fp); + +int fsverity_cmd_enable(const struct fsverity_command *cmd, + int argc, char *argv[]); +int fsverity_cmd_setup(const struct fsverity_command *cmd, + int argc, char *argv[]); +int fsverity_cmd_set_measurement(const struct fsverity_command *cmd, + int argc, char *argv[]); + +#endif /* COMMANDS_H */ diff --git a/debian/control b/debian/control index a0bde47..d93f4f3 100644 --- a/debian/control +++ b/debian/control @@ -1,14 +1,14 @@ Source: fsverity-utils Priority: optional Maintainer: Eric Biggers <ebiggers@google.com> -Build-Depends: debhelper (>= 10), python +Build-Depends: debhelper (>= 10), libssl-dev (>= 1.0.0), libz-dev Standards-Version: 4.0.0 Vcs-Git: git://git.kernel.org/pub/scm/linux/kernel/git/mhalcrow/fsverity Package: fsverity-utils Section: utils Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}, cryptsetup-bin -Description: fs-verity userspace utilities. These programs allow you to set up +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: fs-verity userspace utility. This program allows you to set up read-only, integrity and/or authenticity-protected files when supported by the underlying filesystem. diff --git a/debian/copyright b/debian/copyright index 105dc6f..a16863c 100644 --- a/debian/copyright +++ b/debian/copyright @@ -4,4 +4,4 @@ Source: git://git.kernel.org/pub/scm/linux/kernel/git/mhalcrow/fsverity Files: * Copyright: 2018 Google, Inc -License: GPL-2 +License: GPL-2+ diff --git a/debian/rules b/debian/rules index ebd8ecb..9b694a1 100755 --- a/debian/rules +++ b/debian/rules @@ -5,4 +5,4 @@ export DH_VERBOSE=1 include /usr/share/dpkg/default.mk %: - dh $@ --with python2 + dh $@ diff --git a/elide_patch.c b/elide_patch.c new file mode 100644 index 0000000..5198d36 --- /dev/null +++ b/elide_patch.c @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Elide and patch handling for 'fsverity setup' + * + * Copyright (C) 2018 Google, Inc. + * + * Written by Eric Biggers, 2018. + */ + +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "fsverity_sys_decls.h" +#include "fsveritysetup.h" + +/* An elision or a patch */ +struct fsverity_elide_patch { + u64 offset; /* byte offset within the original data */ + u64 length; /* length in bytes */ + bool patch; /* false if elision, true if patch */ + u8 data[]; /* replacement data (if patch=true) */ +}; + +/* Maximum supported patch size, in bytes */ +#define FS_VERITY_MAX_PATCH_SIZE 255 + +/* Parse an --elide=OFFSET,LENGTH option */ +static struct fsverity_elide_patch *parse_elide_option(const char *optarg) +{ + struct fsverity_elide_patch *ext = NULL; + char *sep, *end; + unsigned long long offset; + unsigned long long length; + + sep = strchr(optarg, ','); + if (!sep || sep == optarg) + goto invalid; + errno = 0; + *sep = '\0'; + offset = strtoull(optarg, &end, 10); + *sep = ','; + if (errno || end != sep) + goto invalid; + length = strtoull(sep + 1, &end, 10); + if (errno || *end) + goto invalid; + if (length <= 0 || length > UINT64_MAX - offset) { + error_msg("Invalid length in '--elide=%s'", optarg); + return NULL; + } + ext = xzalloc(sizeof(*ext)); + ext->offset = offset; + ext->length = length; + ext->patch = false; + return ext; + +invalid: + error_msg("Invalid --elide option: '%s'. Must be formatted as OFFSET,LENGTH", + optarg); + return NULL; +} + +/* Parse a --patch=OFFSET,PATCHFILE option */ +static struct fsverity_elide_patch *parse_patch_option(const char *optarg) +{ + struct fsverity_elide_patch *ext = NULL; + struct filedes patchfile = { .fd = -1 }; + char *sep, *end; + unsigned long long offset; + u64 length; + + sep = strchr(optarg, ','); + if (!sep || sep == optarg) + goto invalid; + errno = 0; + *sep = '\0'; + offset = strtoull(optarg, &end, 10); + *sep = ','; + if (errno || end != sep) + goto invalid; + if (!open_file(&patchfile, sep + 1, O_RDONLY, 0)) + goto out; + if (!get_file_size(&patchfile, &length)) + goto out; + if (length <= 0) { + error_msg("patch file '%s' is empty", patchfile.name); + goto out; + } + if (length > FS_VERITY_MAX_PATCH_SIZE) { + error_msg("Patch file '%s' is too long. Max patch size is %d bytes.", + patchfile.name, FS_VERITY_MAX_PATCH_SIZE); + goto out; + } + ext = xzalloc(sizeof(*ext) + length); + ext->offset = offset; + ext->length = length; + ext->patch = true; + if (!full_read(&patchfile, ext->data, length)) { + free(ext); + ext = NULL; + } +out: + filedes_close(&patchfile); + return ext; + +invalid: + error_msg("Invalid --patch option: '%s'. Must be formatted as OFFSET,PATCHFILE", + optarg); + goto out; +} + +/* Sort by increasing offset */ +static int cmp_elide_patch_exts(const void *_p1, const void *_p2) +{ + const struct fsverity_elide_patch *ext1, *ext2; + + ext1 = *(const struct fsverity_elide_patch **)_p1; + ext2 = *(const struct fsverity_elide_patch **)_p2; + + if (ext1->offset > ext2->offset) + return 1; + if (ext1->offset < ext2->offset) + return -1; + return 0; +} + +/* + * Given the lists of --elide and --patch options, validate and load the + * elisions and patches into @params. + */ +bool load_elisions_and_patches(const struct string_list *elide_opts, + const struct string_list *patch_opts, + struct fsveritysetup_params *params) +{ + const size_t num_exts = elide_opts->length + patch_opts->length; + struct fsverity_elide_patch **exts; + size_t i, j; + + if (num_exts == 0) /* Normal case: no elisions or patches */ + return true; + params->num_elisions_and_patches = num_exts; + exts = xzalloc(num_exts * sizeof(exts[0])); + params->elisions_and_patches = exts; + j = 0; + + /* Parse the --elide options */ + for (i = 0; i < elide_opts->length; i++) { + exts[j] = parse_elide_option(elide_opts->strings[i]); + if (!exts[j++]) + return false; + } + + /* Parse the --patch options */ + for (i = 0; i < patch_opts->length; i++) { + exts[j] = parse_patch_option(patch_opts->strings[i]); + if (!exts[j++]) + return false; + } + + /* Sort the elisions and patches by increasing offset */ + qsort(exts, num_exts, sizeof(exts[0]), cmp_elide_patch_exts); + + /* Verify that no elisions or patches overlap */ + for (j = 1; j < num_exts; j++) { + if (exts[j]->offset < + exts[j - 1]->offset + exts[j - 1]->length) { + error_msg("%s at [%"PRIu64", %"PRIu64") overlaps " + "%s at [%"PRIu64", %"PRIu64")", + exts[j - 1]->patch ? "Patch" : "Elision", + exts[j - 1]->offset, + exts[j - 1]->offset + exts[j - 1]->length, + exts[j]->patch ? "patch" : "elision", + exts[j]->offset, + exts[j]->offset + exts[j]->length); + return false; + } + } + return true; +} + +void free_elisions_and_patches(struct fsveritysetup_params *params) +{ + size_t i; + + for (i = 0; i < params->num_elisions_and_patches; i++) + free(params->elisions_and_patches[i]); + free(params->elisions_and_patches); +} + +/* + * Given the original file @in of length @in_length bytes, create a temporary + * file @out_ret and write to it the data with the elisions and patches applied, + * with the end zero-padded to the next block boundary. Returns in + * @out_length_ret the length of the elided/patched file in bytes. + */ +bool apply_elisions_and_patches(const struct fsveritysetup_params *params, + struct filedes *in, u64 in_length, + struct filedes *out_ret, u64 *out_length_ret) +{ + struct fsverity_elide_patch **exts = params->elisions_and_patches; + struct filedes *out = out_ret; + size_t i; + + for (i = 0; i < params->num_elisions_and_patches; i++) { + if (exts[i]->offset + exts[i]->length > in_length) { + error_msg("%s at [%"PRIu64", %"PRIu64") extends beyond end of input file", + exts[i]->patch ? "Patch" : "Elision", + exts[i]->offset, + exts[i]->offset + exts[i]->length); + return false; + } + } + + if (!filedes_seek(in, 0, SEEK_SET)) + return false; + + if (!open_tempfile(out)) + return false; + + for (i = 0; i < params->num_elisions_and_patches; i++) { + printf("Applying %s: offset=%"PRIu64", length=%"PRIu64"\n", + exts[i]->patch ? "patch" : "elision", + exts[i]->offset, exts[i]->length); + + if (!copy_file_data(in, out, exts[i]->offset - in->pos)) + return false; + + if (exts[i]->patch && + !full_write(out, exts[i]->data, exts[i]->length)) + return false; + + if (!filedes_seek(in, exts[i]->length, SEEK_CUR)) + return false; + } + if (!copy_file_data(in, out, in_length - in->pos)) + return false; + if (!write_zeroes(out, ALIGN(out->pos, params->blocksize) - out->pos)) + return false; + *out_length_ret = out->pos; + return true; +} + +/* Calculate the size the elisions and patches will take up when serialized */ +size_t total_elide_patch_ext_length(const struct fsveritysetup_params *params) +{ + size_t total = 0; + size_t i; + + for (i = 0; i < params->num_elisions_and_patches; i++) { + const struct fsverity_elide_patch *ext = + params->elisions_and_patches[i]; + size_t inner_len; + + if (ext->patch) { + inner_len = sizeof(struct fsverity_extension_patch) + + ext->length; + } else { + inner_len = sizeof(struct fsverity_extension_elide); + } + total += FSVERITY_EXTLEN(inner_len); + } + return total; +} + +/* + * Append the elide and patch extensions (if any) to the given buffer. + * The buffer must have enough space; call total_elide_patch_ext_length() first. + */ +void append_elide_patch_exts(void **buf_p, + const struct fsveritysetup_params *params) +{ + void *buf = *buf_p; + size_t i; + union { + struct { + struct fsverity_extension_patch hdr; + u8 data[FS_VERITY_MAX_PATCH_SIZE]; + } patch; + struct fsverity_extension_elide elide; + } u; + + for (i = 0; i < params->num_elisions_and_patches; i++) { + const struct fsverity_elide_patch *ext = + params->elisions_and_patches[i]; + int type; + size_t extlen; + + if (ext->patch) { + type = FS_VERITY_EXT_PATCH; + u.patch.hdr.offset = cpu_to_le64(ext->offset); + ASSERT(ext->length <= sizeof(u.patch.data)); + memcpy(u.patch.data, ext->data, ext->length); + extlen = sizeof(u.patch.hdr) + ext->length; + } else { + type = FS_VERITY_EXT_ELIDE; + u.elide.offset = cpu_to_le64(ext->offset), + u.elide.length = cpu_to_le64(ext->length); + extlen = sizeof(u.elide); + } + fsverity_append_extension(&buf, type, &u, extlen); + } + + *buf_p = buf; +} @@ -1,204 +1,148 @@ -// SPDX-License-Identifier: GPL-2.0 +// SPDX-License-Identifier: GPL-2.0+ /* * fs-verity userspace tool * - * Copyright (C) 2018, Google, Inc. + * Copyright (C) 2018 Google, Inc. + * + * Written by Eric Biggers, 2018. */ -#include <fcntl.h> -#include <getopt.h> -#include <stdbool.h> -#include <stdio.h> #include <stdlib.h> #include <string.h> -#include <sys/ioctl.h> -#include <unistd.h> - -#include "fsverity_api.h" -#define ARRAY_SIZE(A) (sizeof(A) / sizeof((A)[0])) +#include "commands.h" +#include "hash_algs.h" -static const struct fsverity_hash_alg { +static const struct fsverity_command { const char *name; - int digest_size; -} fsverity_hash_algs[] = { - [FS_VERITY_ALG_SHA256] = { - .name = "sha256", - .digest_size = 32, - }, - [FS_VERITY_ALG_CRC32] = { - .name = "crc32", - .digest_size = 4, + int (*func)(const struct fsverity_command *cmd, int argc, char *argv[]); + const char *short_desc; + const char *usage_str; +} fsverity_commands[] = { + { + .name = "enable", + .func = fsverity_cmd_enable, + .short_desc = +"Enable fs-verity on a file with verity metadata", + .usage_str = +" fsverity enable FILE\n" + }, { + .name = "setup", + .func = fsverity_cmd_setup, + .short_desc = "Create the verity metadata for a file", + .usage_str = +" fsverity setup INFILE [OUTFILE]\n" +" [--hash=HASH_ALG] [--salt=SALT] [--signing-key=KEYFILE]\n" +" [--signing-cert=CERTFILE] [--signature=SIGFILE]\n" +" [--patch=OFFSET,PATCHFILE] [--elide=OFFSET,LENGTH]\n" + }, { + .name = "set_measurement", + .func = fsverity_cmd_set_measurement, + .short_desc = +"Set the trusted file measurement for the given fs-verity file", + .usage_str = +" fsverity set_measurement FILE EXPECTED_MEASUREMENT [--hash=HASH_ALG]\n" }, }; -static void show_hash_algs(void) +static void usage_all(FILE *fp) { - size_t i; - - fprintf(stderr, "Available hash algorithms:"); - for (i = 0; i < ARRAY_SIZE(fsverity_hash_algs); i++) { - if (fsverity_hash_algs[i].name) - fprintf(stderr, " %s", fsverity_hash_algs[i].name); - } - fprintf(stderr, "\n"); -} - -static const struct fsverity_hash_alg *find_hash_alg(const char *name) -{ - size_t i; - - for (i = 0; i < ARRAY_SIZE(fsverity_hash_algs); i++) { - if (fsverity_hash_algs[i].name && - !strcmp(name, fsverity_hash_algs[i].name)) - return &fsverity_hash_algs[i]; - } - return NULL; + int i; + + fputs("Usage:\n", fp); + for (i = 0; i < ARRAY_SIZE(fsverity_commands); i++) + fprintf(fp, " %s:\n%s\n", fsverity_commands[i].short_desc, + fsverity_commands[i].usage_str); + fputs( +" Standard options:\n" +" fsverity --help\n" +" fsverity --version\n" +"\n" +"Available hash algorithms: ", fp); + show_all_hash_algs(fp); + fputs("\nSee `man fsverity` for more details.\n", fp); } -static int hex2bin_char(char c) +static void usage_cmd(const struct fsverity_command *cmd, FILE *fp) { - if (c >= 'a' && c <= 'f') - return 10 + c - 'a'; - if (c >= 'A' && c <= 'F') - return 10 + c - 'A'; - if (c >= '0' && c <= '9') - return c - '0'; - return -1; + fprintf(fp, "Usage:\n%s", cmd->usage_str); } -static bool parse_hex_digest(const char *hex, __u8 *bin, size_t bin_len) +void usage(const struct fsverity_command *cmd, FILE *fp) { - size_t i; - - if (strlen(hex) != 2 * bin_len) - return false; - - for (i = 0; i < bin_len; i++) { - int hi = hex2bin_char(hex[i * 2]); - int lo = hex2bin_char(hex[i * 2 + 1]); - - if (hi < 0 || lo < 0) - return false; - bin[i] = (hi << 4) | lo; - } - return true; + if (cmd) + usage_cmd(cmd, fp); + else + usage_all(fp); } -enum { - OPT_HASH, -}; +#define PACKAGE_VERSION "v0.0-alpha" +#define PACKAGE_BUGREPORT "linux-fscrypt@vger.kernel.org" -static void usage(void) +static void show_version(void) { - const char * const usage_str = -"Usage: fsverity enable FILE\n" -" fsverity set_measurement [--hash=HASH] FILE EXPECTED_MEASUREMENT\n" + static const char * const str = +"fsverity " PACKAGE_VERSION "\n" +"Copyright (C) 2018 Google, Inc.\n" +"License GPLv2+: GNU GPL version 2 or later <http://gnu.org/licenses/gpl.html>.\n" +"This is free software: you are free to change and redistribute it.\n" +"There is NO WARRANTY, to the extent permitted by law.\n" "\n" -"EXPECTED_MEASUREMENT must be given as a hex string.\n" -"The default HASH algorithm is sha256.\n" - ; - fputs(usage_str, stderr); - show_hash_algs(); - exit(2); +"Report bugs to " PACKAGE_BUGREPORT ".\n"; + fputs(str, stdout); } -static int fsverity_enable(int argc, char *argv[]) +static void handle_common_options(int argc, char *argv[], + const struct fsverity_command *cmd) { - int fd; - - if (argc != 2) - usage(); - - fd = open(argv[1], O_RDONLY); - if (fd < 0) { - fprintf(stderr, "Can't open %s: %m\n", argv[1]); - return 1; - } - if (ioctl(fd, FS_IOC_ENABLE_VERITY, NULL)) { - fprintf(stderr, "FS_IOC_ENABLE_VERITY: %m\n"); - return 1; + int i; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (*arg++ != '-') + continue; + if (*arg++ != '-') + continue; + if (!strcmp(arg, "help")) { + usage(cmd, stdout); + exit(0); + } else if (!strcmp(arg, "version")) { + show_version(); + exit(0); + } else if (!*arg) /* reached "--", no more options */ + return; } - close(fd); - return 0; } -static int fsverity_set_measurement(int argc, char *argv[]) +static const struct fsverity_command *find_command(const char *name) { - static const struct option longopts[] = { - {"hash", required_argument, NULL, OPT_HASH}, - {NULL, 0, NULL, 0}, - }; - const struct fsverity_hash_alg *alg = - &fsverity_hash_algs[FS_VERITY_ALG_SHA256]; - int c; - int fd; - struct fsverity_measurement *measurement; - - while ((c = getopt_long(argc, argv, "", longopts, NULL)) != -1) { - switch (c) { - case OPT_HASH: - alg = find_hash_alg(optarg); - if (!alg) { - fprintf(stderr, - "Unknown hash algorithm: '%s'\n", - optarg); - show_hash_algs(); - return 2; - } - break; - default: - usage(); - } - } - argv += optind; - argc -= optind; + int i; - if (argc != 2) - usage(); + for (i = 0; i < ARRAY_SIZE(fsverity_commands); i++) + if (!strcmp(name, fsverity_commands[i].name)) + return &fsverity_commands[i]; + return NULL; +} - fd = open(argv[0], O_RDONLY); - if (fd < 0) { - fprintf(stderr, "Can't open %s: %m\n", argv[0]); - return 1; - } +int main(int argc, char *argv[]) +{ + const struct fsverity_command *cmd; - measurement = calloc(1, sizeof(*measurement) + alg->digest_size); - measurement->digest_algorithm = alg - &fsverity_hash_algs[0]; - measurement->digest_size = alg->digest_size; - if (!parse_hex_digest(argv[1], measurement->digest, alg->digest_size)) { - fprintf(stderr, - "Invalid EXPECTED_MEASUREMENT hex string. Expected %u-character hex string for hash algorithm '%s'\n", - alg->digest_size * 2, alg->name); + if (argc < 2) { + error_msg("no command specified"); + usage_all(stderr); return 2; } - if (ioctl(fd, FS_IOC_SET_VERITY_MEASUREMENT, measurement)) { - fprintf(stderr, "FS_IOC_SET_VERITY_MEASUREMENT: %m\n"); - return 1; - } - close(fd); - return 0; -} - -static const struct { - const char *name; - int (*func)(int argc, char *argv[]); -} commands[] = { - { "enable", fsverity_enable }, - { "set_measurement", fsverity_set_measurement }, -}; + cmd = find_command(argv[1]); -int main(int argc, char *argv[]) -{ - size_t i; + handle_common_options(argc, argv, cmd); - if (argc < 2) - usage(); - - for (i = 0; i < ARRAY_SIZE(commands); i++) { - if (!strcmp(argv[1], commands[i].name)) - return commands[i].func(argc - 1, argv + 1); + if (!cmd) { + error_msg("unrecognized command: '%s'", argv[1]); + usage_all(stderr); + return 2; } - usage(); + return cmd->func(cmd, argc - 1, argv + 1); } diff --git a/fsverity_api.h b/fsverity_api.h deleted file mode 100644 index e4cc960..0000000 --- a/fsverity_api.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef _FSVERITY_API_H -#define _FSVERITY_API_H - -#include <linux/limits.h> -#include <linux/ioctl.h> -#include <linux/types.h> - -/* file-based verity support */ - -#define FS_VERITY_ALG_SHA256 1 -#define FS_VERITY_ALG_CRC32 2 - -struct fsverity_measurement { - __u16 digest_algorithm; - __u16 digest_size; - __u32 reserved1; - __u64 reserved2[3]; - __u8 digest[]; -}; - -#define FS_IOC_ENABLE_VERITY _IO('f', 133) -#define FS_IOC_SET_VERITY_MEASUREMENT _IOW('f', 134, struct fsverity_measurement) - -#endif /* _FSVERITY_API_H */ diff --git a/fsverity_sys_decls.h b/fsverity_sys_decls.h new file mode 100644 index 0000000..122f4a2 --- /dev/null +++ b/fsverity_sys_decls.h @@ -0,0 +1,91 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#ifndef FSVERITY_SYS_DECLS_H +#define FSVERITY_SYS_DECLS_H + +#include <linux/limits.h> +#include <linux/ioctl.h> +#include <linux/types.h> + +/* ========== Ioctls ========== */ + +#define FS_VERITY_ALG_SHA256 1 +#define FS_VERITY_ALG_CRC32 2 + +/* Same as 'struct fsverity_signed_measurement', but with native endianness */ +struct fsverity_measurement { + __u16 digest_algorithm; + __u16 digest_size; + __u32 reserved1; + __u64 reserved2[3]; + __u8 digest[]; +}; + +#define FS_IOC_ENABLE_VERITY _IO('f', 133) +#define FS_IOC_SET_VERITY_MEASUREMENT _IOW('f', 134, struct fsverity_measurement) + +/* ========== On-disk footer format ========== */ + +#define FS_VERITY_MAGIC "TrueBrew" +#define FS_VERITY_MAJOR 1 +#define FS_VERITY_MINOR 0 + +/* Fixed-length portion of footer (begins after Merkle tree) */ +struct fsverity_footer { + __u8 magic[8]; /* must be FS_VERITY_MAGIC */ + __u8 major_version; /* must be FS_VERITY_MAJOR */ + __u8 minor_version; /* must be FS_VERITY_MINOR */ + __u8 log_blocksize; /* log2(data-bytes-per-hash), e.g. 12 for 4KB */ + __u8 log_arity; /* log2(leaves-per-node), e.g. 7 for SHA-256 */ + __le16 meta_algorithm; /* hash algorithm for tree blocks */ + __le16 data_algorithm; /* hash algorithm for data blocks */ + __le32 flags; /* flags */ + __le32 reserved1; /* must be 0 */ + __le64 size; /* size of the original, unpadded data */ + __u8 authenticated_ext_count; /* number of authenticated extensions */ + __u8 unauthenticated_ext_count; /* number of unauthenticated extensions */ + __u8 reserved2[30]; /* must be 0 */ + /* This structure is 64 bytes long */ +}; /* followed by zero or more extensions (struct fsverity_extension) */ + +#define FS_VERITY_FLAG_INTEGRITY_ONLY 0x00000001 + +/* extension types */ +#define FS_VERITY_EXT_ELIDE 1 +#define FS_VERITY_EXT_PATCH 2 +#define FS_VERITY_EXT_SALT 3 +#define FS_VERITY_EXT_PKCS7_SIGNATURE 4 + +/* Header of each variable-length metadata item following the fsverity_footer */ +struct fsverity_extension { + /* + * Length of this extension in bytes, including this header. Must be + * rounded up to an 8-byte boundary when advancing to the next + * extension. + */ + __le32 length; + __le16 type; /* Type of this extension (see codes above) */ + __le16 reserved; /* Reserved, must be 0 */ +}; + +struct fsverity_extension_elide { + __le64 offset; + __le64 length; +}; + +struct fsverity_extension_patch { + __le64 offset; +}; /* followed by variable-length replacement data */ + +/* + * Same as 'struct fsverity_measurement', but with fixed endianness, so it can + * be stored on-disk in the file footer. + */ +struct fsverity_signed_measurement { + __le16 digest_algorithm; + __le16 digest_size; + __le32 reserved1; + __le64 reserved2[3]; + __u8 digest[]; +}; + +#endif /* FSVERITY_SYS_DECLS_H */ diff --git a/fsveritysetup b/fsveritysetup deleted file mode 100755 index af158b7..0000000 --- a/fsveritysetup +++ /dev/null @@ -1,704 +0,0 @@ -#!/usr/bin/python -"""Sets up a file for fs-verity.""" - -from __future__ import print_function - -import argparse -import binascii -import ctypes -import hashlib -import io -import math -import os -import subprocess -import sys -import tempfile -import zlib - -DATA_BLOCK_SIZE = 4096 -HASH_BLOCK_SIZE = 4096 -FS_VERITY_MAGIC = b'TrueBrew' - -FS_VERITY_EXT_ELIDE = 1 -FS_VERITY_EXT_PATCH = 2 -FS_VERITY_EXT_SALT = 3 -FS_VERITY_EXT_PKCS7_SIGNATURE = 4 - -FS_VERITY_ALG_SHA256 = 1 -FS_VERITY_ALG_CRC32 = 2 - - -class CRC32Hash(object): - """hashlib-compatible wrapper for zlib.crc32().""" - - digest_size = 4 - - # Big endian, to be compatible with veritysetup --hash=crc32, which uses - # libgcrypt, which uses big endian CRC-32. - class Digest(ctypes.BigEndianStructure): - _fields_ = [('remainder', ctypes.c_uint32)] - - def __init__(self, remainder=0): - self.remainder = remainder - - def update(self, string): - self.remainder = zlib.crc32(bytes(string), self.remainder) - - def digest(self): - digest = CRC32Hash.Digest() - digest.remainder = self.remainder - return serialize_struct(digest) - - def hexdigest(self): - return binascii.hexlify(self.digest()).decode('ascii') - - def copy(self): - return CRC32Hash(self.remainder) - - -class HashAlgorithm(object): - """A hash algorithm supported by fs-verity""" - - def __init__(self, code, name, digest_size): - self.code = code - self.name = name - self.digest_size = digest_size - - def create(self): - if self.name == 'crc32': - return CRC32Hash() - else: - return hashlib.new(self.name) - - -HASH_ALGORITHMS = [ - HashAlgorithm(FS_VERITY_ALG_SHA256, 'sha256', 32), - HashAlgorithm(FS_VERITY_ALG_CRC32, 'crc32', 4), -] - - -class fsverity_footer(ctypes.LittleEndianStructure): - _fields_ = [ - ('magic', ctypes.c_char * 8), # - ('major_version', ctypes.c_uint8), - ('minor_version', ctypes.c_uint8), - ('log_blocksize', ctypes.c_uint8), - ('log_arity', ctypes.c_uint8), - ('meta_algorithm', ctypes.c_uint16), - ('data_algorithm', ctypes.c_uint16), - ('flags', ctypes.c_uint32), - ('reserved1', ctypes.c_uint32), - ('size', ctypes.c_uint64), - ('authenticated_ext_count', ctypes.c_uint8), - ('unauthenticated_ext_count', ctypes.c_uint8), - ('reserved2', ctypes.c_char * 30) - ] - - -class fsverity_extension(ctypes.LittleEndianStructure): - _fields_ = [ - ('length', ctypes.c_uint32), # - ('type', ctypes.c_uint16), - ('reserved', ctypes.c_uint16) - ] - - -class fsverity_extension_patch(ctypes.LittleEndianStructure): - _fields_ = [ - ('offset', ctypes.c_uint64), # - # followed by variable-length 'databytes' - ] - - -class fsverity_extension_elide(ctypes.LittleEndianStructure): - _fields_ = [ - ('offset', ctypes.c_uint64), # - ('length', ctypes.c_uint64) - ] - - -class fsverity_measurement(ctypes.LittleEndianStructure): - _fields_ = [ - ('digest_algorithm', ctypes.c_uint16), # - ('digest_size', ctypes.c_uint16), - ('reserved1', ctypes.c_uint32), - ('reserved2', ctypes.c_uint64 * 3) - # followed by variable-length 'digest' - ] - - -class FooterOffset(ctypes.LittleEndianStructure): - _fields_ = [('ftr_offset', ctypes.c_uint32)] - - -def copy_bytes(src, dst, n): - """Copies 'n' bytes from the 'src' file to the 'dst' file.""" - if n < 0: - raise ValueError('Negative copy count: {}'.format(n)) - while n > 0: - buf = src.read(min(n, io.DEFAULT_BUFFER_SIZE)) - if not buf: - raise EOFError('Unexpected end of src file') - dst.write(buf) - n -= len(buf) - - -def copy(src, dst): - """Copies from the 'src' file to the 'dst' file until EOF on 'src'.""" - buf = src.read(io.DEFAULT_BUFFER_SIZE) - while buf: - dst.write(buf) - buf = src.read(io.DEFAULT_BUFFER_SIZE) - - -def pad_to_block_boundary(f): - """Pads the file with zeroes to the next data block boundary.""" - f.write(b'\0' * (-f.tell() % DATA_BLOCK_SIZE)) - - -def ilog2(n): - l = int(math.log(n, 2)) - if n != 1 << l: - raise ValueError('{} is not a power of 2'.format(n)) - return l - - -def serialize_struct(struct): - """Serializes a ctypes.Structure to a byte array.""" - return bytes(ctypes.string_at(ctypes.pointer(struct), ctypes.sizeof(struct))) - - -def veritysetup(data_filename, tree_filename, salt, algorithm): - """Built-in Merkle tree generation algorithm.""" - salted_hash = algorithm.create() - salted_hash.update(salt) - hashes_per_block = HASH_BLOCK_SIZE // salted_hash.digest_size - level_blocks = [os.stat(data_filename).st_size // DATA_BLOCK_SIZE] - while level_blocks[-1] > 1: - level_blocks.append( - (level_blocks[-1] + hashes_per_block - 1) // hashes_per_block) - hash_block_offset = sum(level_blocks) - level_blocks[0] - with open(data_filename, 'rb') as datafile: - with open(tree_filename, 'r+b') as hashfile: - for level, blockcount in enumerate(level_blocks): - (i, pending) = (0, bytearray()) - for j in range(blockcount): - h = salted_hash.copy() - if level == 0: - datafile.seek(j * DATA_BLOCK_SIZE) - h.update(datafile.read(DATA_BLOCK_SIZE)) - else: - hashfile.seek((hash_block_offset + j) * HASH_BLOCK_SIZE) - h.update(hashfile.read(HASH_BLOCK_SIZE)) - pending += h.digest() - if level + 1 == len(level_blocks): - assert len(pending) == salted_hash.digest_size - return binascii.hexlify(pending).decode('ascii') - if len(pending) == HASH_BLOCK_SIZE or j + 1 == blockcount: - pending += b'\0' * (HASH_BLOCK_SIZE - len(pending)) - hashfile.seek((hash_block_offset - level_blocks[level + 1] + i) * - HASH_BLOCK_SIZE) - hashfile.write(pending) - (i, pending) = (i + 1, bytearray()) - hash_block_offset -= level_blocks[level + 1] - - -class Extension(object): - """An fs-verity extension item.""" - - def serialize(self): - type_buf = self._serialize_impl() - hdr = fsverity_extension() - hdr.length = ctypes.sizeof(hdr) + len(type_buf) - hdr.type = self.TYPE_CODE - pad = -len(type_buf) % 8 - return serialize_struct(hdr) + type_buf + (b'\0' * pad) - - -class SimpleExtension(Extension): - - def __init__(self, raw_data): - self.raw_data = raw_data - - def _serialize_impl(self): - return self.raw_data - - -class SaltExtension(SimpleExtension): - - TYPE_CODE = FS_VERITY_EXT_SALT - - -class PKCS7SignatureExtension(SimpleExtension): - - TYPE_CODE = FS_VERITY_EXT_PKCS7_SIGNATURE - - -class DataExtension(Extension): - """An fs-verity patch or elide extension.""" - - def __init__(self, offset, length): - self.offset = offset - self.length = length - if self.length < self.MIN_LENGTH: - raise ValueError('length too small (got {}, need >= {})'.format( - self.length, self.MIN_LENGTH)) - if self.length > self.MAX_LENGTH: - raise ValueError('length too large (got {}, need <= {})'.format( - self.length, self.MAX_LENGTH)) - if self.offset < 0: - raise ValueError('offset cannot be negative (got {})'.format(self.offset)) - - def __str__(self): - return '{}(offset {}, length {})'.format(self.__class__.__name__, - self.offset, self.length) - - -class ElideExtension(DataExtension): - """An fs-verity elide extension.""" - - TYPE_CODE = FS_VERITY_EXT_ELIDE - MIN_LENGTH = 1 - MAX_LENGTH = (1 << 64) - 1 - - def __init__(self, offset, length): - DataExtension.__init__(self, offset, length) - - def apply(self, out_file): - pass - - def _serialize_impl(self): - ext = fsverity_extension_elide() - ext.offset = self.offset - ext.length = self.length - return serialize_struct(ext) - - -class PatchExtension(DataExtension): - """An fs-verity patch extension.""" - - TYPE_CODE = FS_VERITY_EXT_PATCH - MIN_LENGTH = 1 - MAX_LENGTH = 255 - - def __init__(self, offset, data): - DataExtension.__init__(self, offset, len(data)) - self.data = data - - def apply(self, dst): - dst.write(self.data) - - def _serialize_impl(self): - ext = fsverity_extension_patch() - ext.offset = self.offset - return serialize_struct(ext) + self.data - - -class BadPatchOrElisionError(Exception): - pass - - -class FSVerityGenerator(object): - """Sets up a file for fs-verity.""" - - def __init__(self, in_filename, out_filename, algorithm, **kwargs): - self.in_filename = in_filename - self.original_size = os.stat(in_filename).st_size - self.out_filename = out_filename - self.algorithm = algorithm - - self.salt = kwargs.get('salt') - if self.salt is None: - self.salt = bytes() - - self.patches_and_elisions = kwargs.get('patches_and_elisions') - if self.patches_and_elisions is None: - self.patches_and_elisions = [] - - self.external_veritysetup = kwargs.get('external_veritysetup') - if self.external_veritysetup is None: - self.external_veritysetup = False - - self.signing_key_file = kwargs.get('signing_key_file') - self.signature_file = kwargs.get('signature_file') - - self.tmp_filenames = [] - - # Patches and elisions must be within the file size and must not overlap. - self.patches_and_elisions = sorted( - self.patches_and_elisions, key=lambda ext: ext.offset) - for i, ext in enumerate(self.patches_and_elisions): - ext_end = ext.offset + ext.length - if ext_end > self.original_size: - raise BadPatchOrElisionError( - '{} extends beyond end of file!'.format(ext)) - if i + 1 < len(self.patches_and_elisions - ) and ext_end > self.patches_and_elisions[i + 1].offset: - raise BadPatchOrElisionError('{} overlaps {}!'.format( - ext, self.patches_and_elisions[i + 1])) - - def _open_tmpfile(self, mode): - f = tempfile.NamedTemporaryFile(mode, delete=False) - self.tmp_filenames.append(f.name) - return f - - def _delete_tmpfiles(self): - for filename in self.tmp_filenames: - os.unlink(filename) - - def _apply_patch_elide_extensions(self, data_filename): - """Apply patch and elide extensions.""" - with open(data_filename, 'rb') as src: - with self._open_tmpfile('wb') as dst: - src_pos = 0 - for ext in self.patches_and_elisions: - print('Applying {}'.format(ext)) - copy_bytes(src, dst, ext.offset - src_pos) - ext.apply(dst) - src_pos = ext.offset + ext.length - src.seek(src_pos) - copy(src, dst) - return dst.name - - def _generate_merkle_tree(self, data_filename): - """Generates a file's Merkle tree for fs-verity. - - Args: - data_filename: file for which to generate the tree. Patches and/or - elisions may need to be applied on top of it. - - Returns: - (root hash as hex, name of the file containing the Merkle tree). - - Raises: - OSError: A problem occurred when executing the 'veritysetup' - program to generate the Merkle tree. - """ - - # If there are any patch or elide extensions, apply them to a temporary file - # and use that to build the Merkle tree instead of the original data. - if self.patches_and_elisions: - data_filename = self._apply_patch_elide_extensions(data_filename) - - # Pad to a data block boundary before building the Merkle tree. - # Note: elisions may result in padding being needed, even if the original - # file was block-aligned! - with open(data_filename, 'ab') as f: - pad_to_block_boundary(f) - - # File to which we'll output the Merkle tree - with self._open_tmpfile('wb') as f: - tree_filename = f.name - - if self.external_veritysetup: - # Delegate to 'veritysetup' to actually build the Merkle tree. - cmd = [ - 'veritysetup', - 'format', - data_filename, - tree_filename, - '--salt=' + binascii.hexlify(self.salt).decode('ascii'), - '--no-superblock', - '--hash={}'.format(self.algorithm.name), - '--data-block-size={}'.format(DATA_BLOCK_SIZE), - '--hash-block-size={}'.format(HASH_BLOCK_SIZE), - ] - print(' '.join(cmd)) - output = subprocess.check_output(cmd, universal_newlines=True) - - # Extract the root hash from veritysetup's output. - root_hash = None - for line in output.splitlines(): - if line.startswith('Root hash'): - root_hash = line.split(':')[1].strip() - break - if root_hash is None: - raise OSError('Root hash not found in veritysetup output!') - else: # builtin veritysetup - root_hash = veritysetup(data_filename, tree_filename, self.salt, - self.algorithm) - return root_hash, tree_filename - - def _generate_footer(self): - """Generates the fixed-size portion of the fs-verity footer.""" - footer = fsverity_footer() - assert ctypes.sizeof(footer) == 64 - footer.magic = FS_VERITY_MAGIC - footer.major_version = 1 - footer.minor_version = 0 - footer.log_blocksize = ilog2(DATA_BLOCK_SIZE) - footer.log_arity = ilog2(DATA_BLOCK_SIZE / self.algorithm.digest_size) - footer.meta_algorithm = self.algorithm.code - footer.data_algorithm = self.algorithm.code - footer.size = self.original_size - footer.authenticated_ext_count = len(self.patches_and_elisions) - if self.salt: - footer.authenticated_ext_count += 1 - footer.unauthenticated_ext_count = 0 - if self.signing_key_file or self.signature_file: - footer.unauthenticated_ext_count += 1 - footer.salt = self.salt - return serialize_struct(footer) - - def _sign_measurement(self, measurement): - """Sign the file's measurement using the given signing_key_file.""" - m = fsverity_measurement() - m.digest_algorithm = self.algorithm.code - m.digest_size = self.algorithm.digest_size - data_to_sign = serialize_struct(m) + binascii.unhexlify(measurement) - - with self._open_tmpfile('wb') as f: - f.write(data_to_sign) - data_to_sign_file = f.name - - with self._open_tmpfile('wb') as f: - pkcs7_msg_file = f.name - - cmd = [ - 'openssl', # - 'smime', - '-sign', - '-in', - data_to_sign_file, - '-signer', - self.signing_key_file, - '-inform', - 'pem', - '-md', - self.algorithm.name, - '-out', - pkcs7_msg_file, - '-outform', - 'der', - '-binary', - '-nodetach', - '-noattr' - ] - - print(' '.join(cmd)) - subprocess.check_call(cmd) - - with open(pkcs7_msg_file, 'rb') as f: - pkcs7_msg = f.read() - - return pkcs7_msg - - def generate(self): - """Sets up a file for fs-verity. - - The input file will be copied to the output file, then have the fs-verity - metadata appended to it. - - Returns: - (fs-verity measurement, Merkle tree root hash), both as hex. - - Raises: - IOError: Problem reading/writing the files. - """ - - # Copy the input file to the output file. - with open(self.in_filename, 'rb') as infile: - with open(self.out_filename, 'wb') as outfile: - copy(infile, outfile) - if outfile.tell() != self.original_size: - raise IOError('{}: size changed!'.format(self.in_filename)) - - try: - # Generate the file's Merkle tree and calculate its root hash. - (root_hash, tree_filename) = self._generate_merkle_tree(self.out_filename) - - with open(self.out_filename, 'ab') as outfile: - - # Pad to a block boundary and append the Merkle tree. - pad_to_block_boundary(outfile) - with open(tree_filename, 'rb') as treefile: - copy(treefile, outfile) - - # Generate the fixed-size portion of the fs-verity footer. - footer = self._generate_footer() - - # Generate authenticated extension items, if any. - auth_extensions = bytearray() - for ext in self.patches_and_elisions: - auth_extensions += ext.serialize() - if self.salt: - auth_extensions += SaltExtension(self.salt).serialize() - - # Compute the fs-verity measurement. - measurement = self.algorithm.create() - measurement.update(footer) - measurement.update(auth_extensions) - measurement.update(binascii.unhexlify(root_hash)) - measurement = measurement.hexdigest() - - # Generate unauthenticated extension items, if any. - unauth_extensions = bytearray() - - pkcs7_msg = None - if self.signing_key_file: - pkcs7_msg = self._sign_measurement(measurement) - if self.signature_file: - with open(self.signature_file, 'wb') as f: - f.write(pkcs7_msg) - print('Wrote signed file measurement to "{}"'.format( - self.signature_file)) - elif self.signature_file: - with open(self.signature_file, 'rb') as f: - pkcs7_msg = f.read() - if pkcs7_msg: - unauth_extensions += PKCS7SignatureExtension(pkcs7_msg).serialize() - - # Write the footer to the output file. - outfile.write(footer) - outfile.write(auth_extensions) - outfile.write(unauth_extensions) - ftr_offset = FooterOffset() - ftr_offset.ftr_offset = len(footer) + len(auth_extensions) + len( - unauth_extensions) + ctypes.sizeof(ftr_offset) - outfile.write(serialize_struct(ftr_offset)) - - finally: - self._delete_tmpfiles() - - return (measurement, root_hash) - - -def convert_hash_argument(argstring): - for alg in HASH_ALGORITHMS: - if alg.name == argstring: - return alg - raise argparse.ArgumentTypeError( - 'Unrecognized algorithm: "{}". Choices are: {}'.format( - argstring, [alg.name for alg in HASH_ALGORITHMS])) - - -def convert_salt_argument(argstring): - try: - return binascii.unhexlify(argstring) - except (ValueError, TypeError): - raise argparse.ArgumentTypeError( - 'Must be a hex string. (Got "{}")'.format(argstring)) - - -def convert_patch_argument(argstring): - """Parse a --patch argument into a PatchExtension.""" - try: - (offset, patchfile) = argstring.split(',') - offset = int(offset) - except ValueError: - raise argparse.ArgumentTypeError( - 'Must be formatted as <offset,patchfile>. (Got "{}")'.format( - argstring)) - try: - with open(patchfile, 'rb') as f: - data = f.read() - return PatchExtension(int(offset), data) - except (IOError, ValueError) as e: - raise argparse.ArgumentTypeError(e) - - -def convert_elide_argument(argstring): - """Parse an --elide argument into an ElideExtension.""" - try: - (offset, length) = argstring.split(',') - offset = int(offset) - length = int(length) - except ValueError: - raise argparse.ArgumentTypeError( - 'Must be formatted as <offset,length>. (Got "{}")'.format(argstring)) - try: - return ElideExtension(offset, length) - except ValueError as e: - raise argparse.ArgumentTypeError(e) - - -def parse_args(): - """Parses the command-line arguments.""" - parser = argparse.ArgumentParser( - description='Sets up a file for fs-verity (file-based integrity)') - parser.add_argument( - 'in_filename', - metavar='<input_file>', - type=str, - help='Original content input file') - parser.add_argument( - 'out_filename', - metavar='<output_file>', - type=str, - help='Output file formatted for fs-verity') - parser.add_argument( - '--salt', - metavar='<hex_string>', - type=convert_salt_argument, - help='Salt, given as a hex string. Default is no salt.') - parser.add_argument( - '--hash', - type=convert_hash_argument, - default='sha256', - help="""Hash algorithm to use. Available algorithms: {}. - Default is sha256.""".format([alg.name for alg in HASH_ALGORITHMS])) - parser.add_argument( - '--patch', - metavar='<offset,patchfile>', - type=convert_patch_argument, - action='append', - dest='patches_and_elisions', - help="""Add a patch extension (not recommended). Data in the region - beginning at <offset> in the original file and continuing for - filesize(<patchfile>) bytes will be replaced with the contents of - <patchfile> for verification purposes, but reads will return the original - data.""") - parser.add_argument( - '--elide', - metavar='<offset,length>', - type=convert_elide_argument, - action='append', - dest='patches_and_elisions', - help="""Add an elide extension (not recommended). Data in the region - beginning at <offset> in the original file and continuing for <length> - bytes will not be verified.""") - parser.add_argument( - '--external-veritysetup', - action='store_const', - const=True, - help="""Invoke the external veritysetup program rather than using the - built-in Merkle tree generation algorithm. They should produce the same - result.""") - parser.add_argument( - '--signing-key', - metavar='<signing_key_file>', - type=str, - help='File containing signing key in PEM format') - parser.add_argument( - '--signature', - metavar='<signature_file>', - type=str, - help="""File containing signed measurement in PKCS#7 DER format. This is - an output file if --signing-key is given, or an input file otherwise.""") - return parser.parse_args() - - -def main(): - args = parse_args() - try: - generator = FSVerityGenerator( - args.in_filename, - args.out_filename, - args.hash, - salt=args.salt, - patches_and_elisions=args.patches_and_elisions, - external_veritysetup=args.external_veritysetup, - signing_key_file=args.signing_key, - signature_file=args.signature) - except BadPatchOrElisionError as e: - sys.stderr.write('ERROR: {}\n'.format(e)) - sys.exit(1) - - (measurement, root_hash) = generator.generate() - - print('Merkle root hash: {}'.format(root_hash)) - print('fs-verity measurement: {}'.format(measurement)) - - -if __name__ == '__main__': - main() diff --git a/fsveritysetup.h b/fsveritysetup.h new file mode 100644 index 0000000..282aabd --- /dev/null +++ b/fsveritysetup.h @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#ifndef FSVERITYSETUP_H +#define FSVERITYSETUP_H + +#include "util.h" + +struct fsveritysetup_params { + const struct fsverity_hash_alg *hash_alg; + u8 *salt; + size_t saltlen; + int blocksize; + int blockbits; /* ilog2(blocksize) */ + unsigned int hashes_per_block; /* blocksize / digest_size */ + const char *signing_key_file; + const char *signing_cert_file; + const char *signature_file; + struct fsverity_elide_patch **elisions_and_patches; + size_t num_elisions_and_patches; +}; + +void fsverity_append_extension(void **buf_p, int type, + const void *ext, size_t extlen); + +#define FSVERITY_EXTLEN(inner_len) \ + ALIGN(sizeof(struct fsverity_extension) + (inner_len), 8) + +/* elide_patch.c */ +bool load_elisions_and_patches(const struct string_list *elide_opts, + const struct string_list *patch_opts, + struct fsveritysetup_params *params); +void free_elisions_and_patches(struct fsveritysetup_params *params); +bool apply_elisions_and_patches(const struct fsveritysetup_params *params, + struct filedes *in, u64 in_length, + struct filedes *out_ret, u64 *out_length_ret); +size_t total_elide_patch_ext_length(const struct fsveritysetup_params *params); +void append_elide_patch_exts(void **buf_p, + const struct fsveritysetup_params *params); +/* sign.c */ +int append_signed_measurement(struct filedes *out, + const struct fsveritysetup_params *params, + const u8 *measurement); + +#endif /* FSVERITYSETUP_H */ diff --git a/hash_algs.c b/hash_algs.c new file mode 100644 index 0000000..3080213 --- /dev/null +++ b/hash_algs.c @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * fs-verity hash algorithms + * + * Copyright (C) 2018 Google, Inc. + * + * Written by Eric Biggers, 2018. + */ + +#include <string.h> +#include <openssl/evp.h> +#include <zlib.h> /* for crc32() */ + +#include "fsverity_sys_decls.h" +#include "hash_algs.h" + +static void free_hash_ctx(struct hash_ctx *ctx) +{ + free(ctx); +} + +/* ========== libcrypto (OpenSSL) wrappers ========== */ + +struct openssl_hash_ctx { + struct hash_ctx base; /* must be first */ + EVP_MD_CTX *md_ctx; + const EVP_MD *md; +}; + +static void openssl_digest_init(struct hash_ctx *_ctx) +{ + struct openssl_hash_ctx *ctx = (void *)_ctx; + + if (EVP_DigestInit_ex(ctx->md_ctx, ctx->md, NULL) != 1) + fatal_error("EVP_DigestInit_ex() failed for algorithm '%s'", + ctx->base.alg->name); +} + +static void openssl_digest_update(struct hash_ctx *_ctx, + const void *data, size_t size) +{ + struct openssl_hash_ctx *ctx = (void *)_ctx; + + if (EVP_DigestUpdate(ctx->md_ctx, data, size) != 1) + fatal_error("EVP_DigestUpdate() failed for algorithm '%s'", + ctx->base.alg->name); +} + +static void openssl_digest_final(struct hash_ctx *_ctx, u8 *digest) +{ + struct openssl_hash_ctx *ctx = (void *)_ctx; + + if (EVP_DigestFinal_ex(ctx->md_ctx, digest, NULL) != 1) + fatal_error("EVP_DigestFinal_ex() failed for algorithm '%s'", + ctx->base.alg->name); +} + +static void openssl_digest_ctx_free(struct hash_ctx *_ctx) +{ + struct openssl_hash_ctx *ctx = (void *)_ctx; + + EVP_MD_CTX_free(ctx->md_ctx); + free(ctx); +} + +static struct hash_ctx * +openssl_digest_ctx_create(const struct fsverity_hash_alg *alg, const EVP_MD *md) +{ + struct openssl_hash_ctx *ctx; + + ctx = xzalloc(sizeof(*ctx)); + ctx->base.alg = alg; + ctx->base.init = openssl_digest_init; + ctx->base.update = openssl_digest_update; + ctx->base.final = openssl_digest_final; + ctx->base.free = openssl_digest_ctx_free; + + ctx->md_ctx = EVP_MD_CTX_new(); + if (!ctx->md_ctx) + fatal_error("out of memory"); + + ctx->md = md; + ASSERT(EVP_MD_size(md) == alg->digest_size); + + return &ctx->base; +} + +static struct hash_ctx *create_sha256_ctx(const struct fsverity_hash_alg *alg) +{ + return openssl_digest_ctx_create(alg, EVP_sha256()); +} + +/* ========== zlib wrapper for CRC-32 ========== */ + +struct crc32_hash_ctx { + struct hash_ctx base; /* must be first */ + u32 remainder; +}; + +static void crc32_init(struct hash_ctx *_ctx) +{ + struct crc32_hash_ctx *ctx = (void *)_ctx; + + ctx->remainder = 0; +} + +static void crc32_update(struct hash_ctx *_ctx, const void *data, size_t size) +{ + struct crc32_hash_ctx *ctx = (void *)_ctx; + + ctx->remainder = crc32(ctx->remainder, data, size); +} + +/* + * Big endian, to be compatible with `veritysetup --hash=crc32`, which uses + * libgcrypt, which uses big endian CRC-32. + */ +static void crc32_final(struct hash_ctx *_ctx, u8 *digest) +{ + struct crc32_hash_ctx *ctx = (void *)_ctx; + __be32 remainder = cpu_to_be32(ctx->remainder); + + memcpy(digest, &remainder, sizeof(remainder)); +} + +static struct hash_ctx *create_crc32_ctx(const struct fsverity_hash_alg *alg) +{ + struct crc32_hash_ctx *ctx = xzalloc(sizeof(*ctx)); + + ctx->base.alg = alg; + ctx->base.init = crc32_init; + ctx->base.update = crc32_update; + ctx->base.final = crc32_final; + ctx->base.free = free_hash_ctx; + return &ctx->base; +} + +/* ========== Hash algorithm definitions ========== */ + +const struct fsverity_hash_alg fsverity_hash_algs[] = { + [FS_VERITY_ALG_SHA256] = { + .name = "sha256", + .digest_size = 32, + .cryptographic = true, + .create_ctx = create_sha256_ctx, + }, + [FS_VERITY_ALG_CRC32] = { + .name = "crc32", + .digest_size = 4, + .create_ctx = create_crc32_ctx, + }, +}; + +const struct fsverity_hash_alg *find_hash_alg(const char *name) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(fsverity_hash_algs); i++) { + if (fsverity_hash_algs[i].name && + !strcmp(name, fsverity_hash_algs[i].name)) + return &fsverity_hash_algs[i]; + } + error_msg("unknown hash algorithm: '%s'", name); + fputs("Available hash algorithms: ", stderr); + show_all_hash_algs(stderr); + putc('\n', stderr); + return NULL; +} + +void show_all_hash_algs(FILE *fp) +{ + int i; + const char *sep = ""; + + for (i = 0; i < ARRAY_SIZE(fsverity_hash_algs); i++) { + if (fsverity_hash_algs[i].name) { + fprintf(fp, "%s%s", sep, fsverity_hash_algs[i].name); + sep = ", "; + } + } +} diff --git a/hash_algs.h b/hash_algs.h new file mode 100644 index 0000000..1f16f7a --- /dev/null +++ b/hash_algs.h @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#ifndef HASH_ALGS_H +#define HASH_ALGS_H + +#include <stdio.h> + +#include "util.h" + +struct fsverity_hash_alg { + const char *name; + unsigned int digest_size; + bool cryptographic; + struct hash_ctx *(*create_ctx)(const struct fsverity_hash_alg *alg); +}; + +extern const struct fsverity_hash_alg fsverity_hash_algs[]; + +struct hash_ctx { + const struct fsverity_hash_alg *alg; + void (*init)(struct hash_ctx *ctx); + void (*update)(struct hash_ctx *ctx, const void *data, size_t size); + void (*final)(struct hash_ctx *ctx, u8 *out); + void (*free)(struct hash_ctx *ctx); +}; + +const struct fsverity_hash_alg *find_hash_alg(const char *name); +void show_all_hash_algs(FILE *fp); +#define DEFAULT_HASH_ALG (&fsverity_hash_algs[FS_VERITY_ALG_SHA256]) + +/* + * Largest digest size among all hash algorithms supported by fs-verity. + * This can be increased if needed. + */ +#define FS_VERITY_MAX_DIGEST_SIZE 32 + +static inline struct hash_ctx *hash_create(const struct fsverity_hash_alg *alg) +{ + return alg->create_ctx(alg); +} + +static inline void hash_init(struct hash_ctx *ctx) +{ + ctx->init(ctx); +} + +static inline void hash_update(struct hash_ctx *ctx, + const void *data, size_t size) +{ + ctx->update(ctx, data, size); +} + +static inline void hash_final(struct hash_ctx *ctx, u8 *digest) +{ + ctx->final(ctx, digest); +} + +static inline void hash_free(struct hash_ctx *ctx) +{ + if (ctx) + ctx->free(ctx); +} + +#endif /* HASH_ALGS_H */ diff --git a/mkfsverity.sh b/mkfsverity.sh index dbafa5d..3827fe9 100755 --- a/mkfsverity.sh +++ b/mkfsverity.sh @@ -75,7 +75,7 @@ if ! $KEEP_INPUT; then head -c "$SIZE" /dev/urandom > "$filename" fi -cmd=(./fsveritysetup "$filename" "output-$SIZE.apk") +cmd=(./fsverity setup "$filename" "output-$SIZE.apk") cmd+=("--salt=deadbeef00000000") for i in "${!PATCHES[@]}"; do @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Signature support for 'fsverity setup' + * + * Copyright (C) 2018 Google, Inc. + * + * Written by Eric Biggers, 2018. + */ + +#include <fcntl.h> +#include <openssl/bio.h> +#include <openssl/err.h> +#include <openssl/pem.h> +#include <openssl/pkcs7.h> +#include <string.h> + +#include "fsverity_sys_decls.h" +#include "fsveritysetup.h" +#include "hash_algs.h" + +static void display_openssl_errors(void) +{ + unsigned long e; + const char *file; + int line; + char errmsg[128]; + + if (ERR_peek_error() == 0) + return; + + fprintf(stderr, "OpenSSL library errors:\n"); + while ((e = ERR_get_error_line(&file, &line)) != 0) { + ERR_error_string_n(e, errmsg, sizeof(errmsg)); + fprintf(stderr, "\t%s: %s:%d\n", errmsg, file, line); + } +} + +/* Read a PEM PKCS#8 formatted private key */ +static EVP_PKEY *read_private_key(const char *keyfile) +{ + BIO *bio; + EVP_PKEY *pkey; + + bio = BIO_new_file(keyfile, "r"); + if (!bio) { + error_msg_errno("can't open '%s' for reading", keyfile); + return NULL; + } + + pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL); + if (!pkey) { + error_msg("Failed to parse private key file '%s'.\n" + " Note: it must be in PEM PKCS#8 format.", + keyfile); + display_openssl_errors(); + } + BIO_free(bio); + return pkey; +} + +/* Read a PEM X.509 formatted certificate */ +static X509 *read_certificate(const char *certfile) +{ + BIO *bio; + X509 *cert; + + bio = BIO_new_file(certfile, "r"); + if (!bio) { + error_msg_errno("can't open '%s' for reading", certfile); + return NULL; + } + cert = PEM_read_bio_X509(bio, NULL, NULL, NULL); + if (!cert) { + error_msg("Failed to parse X.509 certificate file '%s'.\n" + " Note: it must be in PEM format.", + certfile); + display_openssl_errors(); + } + BIO_free(bio); + return cert; +} + +/* + * Check that the given data is a valid 'struct fsverity_signed_measurement' + * that matches the given @expected_measurement and @hash_alg. + * + * Return: NULL if valid, else a string describing why the data is invalid. + */ +static const char * +sanity_check_fsverity_measurement(const void *signed_data, size_t size, + const u8 *expected_measurement, + const struct fsverity_hash_alg *hash_alg) +{ + const struct fsverity_signed_measurement *m = signed_data; + + if (size != sizeof(*m) + hash_alg->digest_size) + return "unexpected length"; + + if (le16_to_cpu(m->digest_algorithm) != hash_alg - fsverity_hash_algs) + return "unexpected hash algorithm"; + + if (le16_to_cpu(m->digest_size) != hash_alg->digest_size) + return "wrong digest size for hash algorithm"; + + if (m->reserved1 || + m->reserved2[0] || m->reserved2[1] || m->reserved2[2]) + return "reserved bits set"; + + if (memcmp(expected_measurement, m->digest, hash_alg->digest_size)) + return "wrong hash"; + + return NULL; +} + +/* + * Sign the specified @data_to_sign of length @data_size bytes using the private + * key in @keyfile, the certificate in @certfile, and the hash algorithm + * @hash_alg. Returns the DER-formatted PKCS#7 signature, with the signed data + * included (not detached), in @sig_ret and @sig_size_ret. + */ +static bool sign_data(const void *data_to_sign, size_t data_size, + const char *keyfile, const char *certfile, + const struct fsverity_hash_alg *hash_alg, + void **sig_ret, int *sig_size_ret) +{ + EVP_PKEY *pkey = NULL; + X509 *cert = NULL; + BIO *bio = NULL; + PKCS7 *p7 = NULL; + const EVP_MD *md; + /* + * PKCS#7 signing flags: + * + * - PKCS7_BINARY signing binary data, so skip MIME translation + * + * - PKCS7_NOATTR omit extra authenticated attributes, such as + * SMIMECapabilities + * + * - PKCS7_NOCERTS omit the signer's certificate + * + * - PKCS7_PARTIAL PKCS7_sign() creates a handle only, then + * PKCS7_sign_add_signer() can add a signer later. + * This is necessary to change the message digest + * algorithm from the default of SHA-1. Requires + * OpenSSL 1.0.0 or later. + */ + int pkcs7_flags = PKCS7_BINARY | PKCS7_NOATTR | PKCS7_NOCERTS | + PKCS7_PARTIAL; + void *sig; + int sig_size; + bool ok = false; + + pkey = read_private_key(keyfile); + if (!pkey) + goto out; + + cert = read_certificate(certfile); + if (!cert) + goto out; + + OpenSSL_add_all_digests(); + ASSERT(hash_alg->cryptographic); + md = EVP_get_digestbyname(hash_alg->name); + if (!md) { + fprintf(stderr, + "Warning: '%s' algorithm not found in OpenSSL library.\n" + " Falling back to SHA-256 signature.\n", + hash_alg->name); + md = EVP_sha256(); + } + + bio = BIO_new_mem_buf(data_to_sign, data_size); + if (!bio) { + error_msg("out of memory"); + goto out; + } + + p7 = PKCS7_sign(NULL, NULL, NULL, bio, pkcs7_flags); + if (!p7) { + error_msg("failed to initialize PKCS#7 signature object"); + display_openssl_errors(); + goto out; + } + + if (!PKCS7_sign_add_signer(p7, cert, pkey, md, pkcs7_flags)) { + error_msg("failed to add signer to PKCS#7 signature object"); + display_openssl_errors(); + goto out; + } + + if (PKCS7_final(p7, bio, pkcs7_flags) != 1) { + error_msg("failed to finalize PKCS#7 signature"); + display_openssl_errors(); + goto out; + } + + BIO_free(bio); + bio = BIO_new(BIO_s_mem()); + if (!bio) { + error_msg("out of memory"); + goto out; + } + + if (i2d_PKCS7_bio(bio, p7) != 1) { + error_msg("failed to DER-encode PKCS#7 signature object"); + display_openssl_errors(); + goto out; + } + + sig_size = BIO_get_mem_data(bio, &sig); + *sig_ret = xmemdup(sig, sig_size); + *sig_size_ret = sig_size; + ok = true; +out: + EVP_PKEY_free(pkey); + X509_free(cert); + PKCS7_free(p7); + BIO_free(bio); + return ok; +} + +/* + * Read a file measurement signature in PKCS#7 DER format from @signature_file, + * validate that the signed data matches the expected measurement, then return + * the PKCS#7 DER message in @sig_ret and @sig_size_ret. + */ +static bool read_signature(const char *signature_file, + const u8 *expected_measurement, + const struct fsverity_hash_alg *hash_alg, + void **sig_ret, int *sig_size_ret) +{ + struct filedes file = { .fd = -1 }; + u64 filesize; + void *sig = NULL; + BIO *bio = NULL; + PKCS7 *p7 = NULL; + bool ok = false; + const char *reason; + + if (!open_file(&file, signature_file, O_RDONLY, 0)) + goto out; + if (!get_file_size(&file, &filesize)) + goto out; + if (filesize <= 0) { + error_msg("signature file '%s' is empty", signature_file); + goto out; + } + if (filesize > 1000000) { + error_msg("signature file '%s' is too large", signature_file); + goto out; + } + sig = xmalloc(filesize); + if (!full_read(&file, sig, filesize)) + goto out; + + bio = BIO_new_mem_buf(sig, filesize); + if (!bio) { + error_msg("out of memory"); + goto out; + } + + p7 = d2i_PKCS7_bio(bio, NULL); + if (!p7) { + error_msg("failed to decode PKCS#7 signature from '%s'", + signature_file); + display_openssl_errors(); + goto out; + } + + if (OBJ_obj2nid(p7->type) != NID_pkcs7_signed || + OBJ_obj2nid(p7->d.sign->contents->type) != NID_pkcs7_data) { + reason = "unexpected PKCS#7 content type"; + } else { + const ASN1_OCTET_STRING *o = p7->d.sign->contents->d.data; + + reason = sanity_check_fsverity_measurement(o->data, o->length, + expected_measurement, + hash_alg); + } + if (reason) { + error_msg("signed file measurement from '%s' is invalid (%s)", + signature_file, reason); + goto out; + } + + printf("Using existing signed file measurement from '%s'\n", + signature_file); + *sig_ret = sig; + *sig_size_ret = filesize; + sig = NULL; + ok = true; +out: + filedes_close(&file); + free(sig); + BIO_free(bio); + PKCS7_free(p7); + return ok; +} + +static bool write_signature(const char *signature_file, + const void *sig, int sig_size) +{ + struct filedes file; + bool ok; + + if (!open_file(&file, signature_file, O_WRONLY|O_CREAT|O_TRUNC, 0644)) + return false; + ok = full_write(&file, sig, sig_size); + ok &= filedes_close(&file); + if (ok) + printf("Wrote signed file measurement to '%s'\n", + signature_file); + return ok; +} + +/* + * If requested, append the signed file measurement to the fs-verity footer as a + * PKCS7_SIGNATURE extension item. + * + * Return: exit status code (0 on success, nonzero on failure) + */ +int append_signed_measurement(struct filedes *out, + const struct fsveritysetup_params *params, + const u8 *measurement) +{ + struct fsverity_signed_measurement *data_to_sign = NULL; + void *sig = NULL; + void *extbuf = NULL; + void *tmp; + int sig_size; + int status; + + if (params->signing_key_file) { + size_t data_size = sizeof(*data_to_sign) + + params->hash_alg->digest_size; + + /* Sign the file measurement using the given key */ + + data_to_sign = xzalloc(data_size); + data_to_sign->digest_algorithm = + cpu_to_le16(params->hash_alg - fsverity_hash_algs); + data_to_sign->digest_size = + cpu_to_le16(params->hash_alg->digest_size); + memcpy(data_to_sign->digest, measurement, + params->hash_alg->digest_size); + + ASSERT(sanity_check_fsverity_measurement(data_to_sign, + data_size, measurement, params->hash_alg) == NULL); + + if (!sign_data(data_to_sign, data_size, + params->signing_key_file, + params->signing_cert_file ?: + params->signing_key_file, + params->hash_alg, + &sig, &sig_size)) + goto out_err; + + if (params->signature_file && + !write_signature(params->signature_file, sig, sig_size)) + goto out_err; + } else { + if (!params->signature_file) + return 0; + + /* Using a signature that was already created */ + if (!read_signature(params->signature_file, measurement, + params->hash_alg, &sig, &sig_size)) + goto out_err; + } + + /* Append the signature to the fs-verity footer as an extension item */ + tmp = extbuf = xzalloc(FSVERITY_EXTLEN(sig_size)); + fsverity_append_extension(&tmp, FS_VERITY_EXT_PKCS7_SIGNATURE, + sig, sig_size); + ASSERT(tmp == extbuf + FSVERITY_EXTLEN(sig_size)); + if (!full_write(out, extbuf, FSVERITY_EXTLEN(sig_size))) + goto out_err; + status = 0; +out: + free(data_to_sign); + free(sig); + free(extbuf); + return status; + +out_err: + status = 1; + goto out; +} @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Utility functions for the 'fsverity' program + * + * Copyright (C) 2018 Google, Inc. + * + * Written by Eric Biggers, 2018. + */ + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "util.h" + +/* ========== Memory allocation ========== */ + +void *xmalloc(size_t size) +{ + void *p = malloc(size); + + if (!p) + fatal_error("out of memory"); + return p; +} + +void *xzalloc(size_t size) +{ + return memset(xmalloc(size), 0, size); +} + +void *xmemdup(const void *mem, size_t size) +{ + return memcpy(xmalloc(size), mem, size); +} + +char *xstrdup(const char *s) +{ + return xmemdup(s, strlen(s) + 1); +} + +char *xasprintf(const char *format, ...) +{ + va_list va1, va2; + int size; + char *s; + + va_start(va1, format); + + va_copy(va2, va1); + size = vsnprintf(NULL, 0, format, va2); + va_end(va2); + + ASSERT(size >= 0); + s = xmalloc(size + 1); + vsprintf(s, format, va1); + + va_end(va1); + return s; +} + +/* ========== Error messages and assertions ========== */ + +static void do_error_msg(const char *format, va_list va, int err) +{ + fputs("ERROR: ", stderr); + vfprintf(stderr, format, va); + if (err) + fprintf(stderr, ": %s", strerror(err)); + putc('\n', stderr); +} + +void error_msg(const char *format, ...) +{ + va_list va; + + va_start(va, format); + do_error_msg(format, va, 0); + va_end(va); +} + +void error_msg_errno(const char *format, ...) +{ + va_list va; + + va_start(va, format); + do_error_msg(format, va, errno); + va_end(va); +} + +__noreturn void fatal_error(const char *format, ...) +{ + va_list va; + + va_start(va, format); + do_error_msg(format, va, 0); + va_end(va); + abort(); +} + +__noreturn void assertion_failed(const char *expr, const char *file, int line) +{ + fatal_error("Assertion failed: %s at %s:%d", expr, file, line); +} + +/* ========== File utilities ========== */ + +bool open_file(struct filedes *file, const char *filename, int flags, int mode) +{ + file->fd = open(filename, flags, mode); + if (file->fd < 0) { + error_msg_errno("can't open '%s' for %s", filename, + (flags & O_ACCMODE) == O_RDONLY ? "reading" : + (flags & O_ACCMODE) == O_WRONLY ? "writing" : + "reading and writing"); + return false; + } + file->autodelete = false; + file->name = xstrdup(filename); + file->pos = 0; + return true; +} + +bool open_tempfile(struct filedes *file) +{ + const char *tmpdir = getenv("TMPDIR") ?: P_tmpdir; + char *name = xasprintf("%s/fsverity-XXXXXX", tmpdir); + + file->fd = mkstemp(name); + if (file->fd < 0) { + error_msg_errno("can't create temporary file"); + free(name); + return false; + } + file->autodelete = true; + file->name = name; + file->pos = 0; + return true; +} + +bool get_file_size(struct filedes *file, u64 *size_ret) +{ + struct stat stbuf; + + if (fstat(file->fd, &stbuf) != 0) { + error_msg_errno("can't stat file '%s'", file->name); + return false; + } + *size_ret = stbuf.st_size; + return true; +} + +bool filedes_seek(struct filedes *file, u64 pos, int whence) +{ + off_t res; + + res = lseek(file->fd, pos, whence); + if (res < 0) { + error_msg_errno("seek error on '%s'", file->name); + return false; + } + file->pos = res; + return true; +} + +bool full_read(struct filedes *file, void *buf, size_t count) +{ + while (count) { + int n = read(file->fd, buf, min(count, INT_MAX)); + + if (n < 0) { + error_msg_errno("reading from '%s'", file->name); + return false; + } + if (n == 0) { + error_msg("unexpected end-of-file on '%s'", file->name); + return false; + } + buf += n; + count -= n; + file->pos += n; + } + return true; +} + +bool full_pread(struct filedes *file, void *buf, size_t count, u64 offset) +{ + while (count) { + int n = pread(file->fd, buf, min(count, INT_MAX), offset); + + if (n < 0) { + error_msg_errno("reading from '%s'", file->name); + return false; + } + if (n == 0) { + error_msg("unexpected end-of-file on '%s'", file->name); + return false; + } + buf += n; + count -= n; + offset += n; + } + return true; +} + +bool full_write(struct filedes *file, const void *buf, size_t count) +{ + while (count) { + int n = write(file->fd, buf, min(count, INT_MAX)); + + if (n < 0) { + error_msg_errno("writing to '%s'", file->name); + return false; + } + buf += n; + count -= n; + file->pos += n; + } + return true; +} + +bool full_pwrite(struct filedes *file, const void *buf, size_t count, + u64 offset) +{ + while (count) { + int n = pwrite(file->fd, buf, min(count, INT_MAX), offset); + + if (n < 0) { + error_msg_errno("writing to '%s'", file->name); + return false; + } + buf += n; + count -= n; + offset += n; + } + return true; +} + +/* Copy 'count' bytes of data from 'src' to 'dst' */ +bool copy_file_data(struct filedes *src, struct filedes *dst, u64 count) +{ + char buf[4096]; + + while (count) { + size_t n = min(count, sizeof(buf)); + + if (!full_read(src, buf, n)) + return false; + if (!full_write(dst, buf, n)) + return false; + count -= n; + } + return true; +} + +/* Write 'count' bytes of zeroes to the file */ +bool write_zeroes(struct filedes *file, u64 count) +{ + char buf[4096]; + + memset(buf, 0, min(count, sizeof(buf))); + + while (count) { + size_t n = min(count, sizeof(buf)); + + if (!full_write(file, buf, n)) + return false; + count -= n; + } + return true; +} + +bool filedes_close(struct filedes *file) +{ + int res; + + if (file->fd < 0) + return true; + res = close(file->fd); + if (res != 0) + error_msg_errno("closing '%s'", file->name); + if (file->autodelete) + (void)unlink(file->name); + file->fd = -1; + free(file->name); + file->name = NULL; + return res == 0; +} + +/* ========== String utilities ========== */ + +static int hex2bin_char(char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return 10 + (c - 'a'); + if (c >= 'A' && c <= 'F') + return 10 + (c - 'A'); + return -1; +} + +bool hex2bin(const char *hex, u8 *bin, size_t bin_len) +{ + if (strlen(hex) != 2 * bin_len) + return false; + + while (bin_len--) { + int hi = hex2bin_char(*hex++); + int lo = hex2bin_char(*hex++); + + if (hi < 0 || lo < 0) + return false; + *bin++ = (hi << 4) | lo; + } + return true; +} + +static char bin2hex_char(u8 nibble) +{ + ASSERT(nibble <= 0xf); + + if (nibble < 10) + return '0' + nibble; + return 'a' + (nibble - 10); +} + +void bin2hex(const u8 *bin, size_t bin_len, char *hex) +{ + while (bin_len--) { + *hex++ = bin2hex_char(*bin >> 4); + *hex++ = bin2hex_char(*bin & 0xf); + bin++; + } + *hex = '\0'; +} + +void string_list_append(struct string_list *list, char *string) +{ + ASSERT(list->length <= list->capacity); + if (list->length == list->capacity) { + list->capacity = (list->capacity * 2) + 4; + list->strings = realloc(list->strings, + sizeof(list->strings[0]) * + list->capacity); + if (!list->strings) + fatal_error("out of memory"); + } + list->strings[list->length++] = string; +} + +void string_list_destroy(struct string_list *list) +{ + free(list->strings); + memset(list, 0, sizeof(*list)); +} @@ -0,0 +1,149 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Utility functions and macros for the 'fsverity' program + * + * Copyright (C) 2018 Google, Inc. + */ +#ifndef UTIL_H +#define UTIL_H + +#include <inttypes.h> +#include <stdbool.h> +#include <stddef.h> + +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; + +#ifdef __CHECKER__ +# define __force __attribute__((force)) +#else +# define __force +#endif + +#define __printf(fmt_idx, vargs_idx) \ + __attribute__((format(printf, fmt_idx, vargs_idx))) + +#define __noreturn __attribute__((noreturn)) +#define __cold __attribute__((cold)) + +#define min(a, b) ({ \ + __typeof__(a) _a = (a); \ + __typeof__(b) _b = (b); \ + _a < _b ? _a : _b; \ +}) +#define max(a, b) ({ \ + __typeof__(a) _a = (a); \ + __typeof__(b) _b = (b); \ + _a > _b ? _a : _b; \ +}) + +#define ARRAY_SIZE(A) (sizeof(A) / sizeof((A)[0])) + +#define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d)) + +/* + * Round 'v' up to the next 'alignment'-byte aligned boundary. + * 'alignment' must be a power of 2. + */ +#define ALIGN(v, alignment) (((v) + ((alignment) - 1)) & ~((alignment) - 1)) + +static inline bool is_power_of_2(unsigned long n) +{ + return n != 0 && ((n & (n - 1)) == 0); +} + +static inline int ilog2(unsigned long n) +{ + return (8 * sizeof(n) - 1) - __builtin_clzl(n); +} + +/* ========== Endianness conversion ========== */ + +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +# define cpu_to_le16(v) ((__force __le16)(u16)(v)) +# define le16_to_cpu(v) ((__force u16)(__le16)(v)) +# define cpu_to_le32(v) ((__force __le32)(u32)(v)) +# define le32_to_cpu(v) ((__force u32)(__le32)(v)) +# define cpu_to_le64(v) ((__force __le64)(u64)(v)) +# define le64_to_cpu(v) ((__force u64)(__le64)(v)) +# define cpu_to_be16(v) ((__force __be16)__builtin_bswap16(v)) +# define be16_to_cpu(v) (__builtin_bswap16((__force u16)(v))) +# define cpu_to_be32(v) ((__force __be32)__builtin_bswap32(v)) +# define be32_to_cpu(v) (__builtin_bswap32((__force u32)(v))) +# define cpu_to_be64(v) ((__force __be64)__builtin_bswap64(v)) +# define be64_to_cpu(v) (__builtin_bswap64((__force u64)(v))) +#else +# define cpu_to_le16(v) ((__force __le16)__builtin_bswap16(v)) +# define le16_to_cpu(v) (__builtin_bswap16((__force u16)(v))) +# define cpu_to_le32(v) ((__force __le32)__builtin_bswap32(v)) +# define le32_to_cpu(v) (__builtin_bswap32((__force u32)(v))) +# define cpu_to_le64(v) ((__force __le64)__builtin_bswap64(v)) +# define le64_to_cpu(v) (__builtin_bswap64((__force u64)(v))) +# define cpu_to_be16(v) ((__force __be16)(u16)(v)) +# define be16_to_cpu(v) ((__force u16)(__be16)(v)) +# define cpu_to_be32(v) ((__force __be32)(u32)(v)) +# define be32_to_cpu(v) ((__force u32)(__be32)(v)) +# define cpu_to_be64(v) ((__force __be64)(u64)(v)) +# define be64_to_cpu(v) ((__force u64)(__be64)(v)) +#endif + +/* ========== Memory allocation ========== */ + +void *xmalloc(size_t size); +void *xzalloc(size_t size); +void *xmemdup(const void *mem, size_t size); +char *xstrdup(const char *s); +__printf(1, 2) char *xasprintf(const char *format, ...); + +/* ========== Error messages and assertions ========== */ + +__printf(1, 2) __cold void error_msg(const char *format, ...); +__printf(1, 2) __cold void error_msg_errno(const char *format, ...); +__printf(1, 2) __cold __noreturn void fatal_error(const char *format, ...); +__cold __noreturn void assertion_failed(const char *expr, + const char *file, int line); + +#define ASSERT(e) ({ if (!(e)) assertion_failed(#e, __FILE__, __LINE__); }) + +/* ========== File utilities ========== */ + +struct filedes { + int fd; + bool autodelete; /* unlink when closed? */ + char *name; /* filename, for logging or error messages */ + u64 pos; /* lseek() position */ +}; + +bool open_file(struct filedes *file, const char *filename, int flags, int mode); +bool open_tempfile(struct filedes *file); +bool get_file_size(struct filedes *file, u64 *size_ret); +bool filedes_seek(struct filedes *file, u64 pos, int whence); +bool full_read(struct filedes *file, void *buf, size_t count); +bool full_pread(struct filedes *file, void *buf, size_t count, u64 offset); +bool full_write(struct filedes *file, const void *buf, size_t count); +bool full_pwrite(struct filedes *file, const void *buf, size_t count, + u64 offset); +bool copy_file_data(struct filedes *src, struct filedes *dst, u64 length); +bool write_zeroes(struct filedes *file, u64 length); +bool filedes_close(struct filedes *file); + +/* ========== String utilities ========== */ + +bool hex2bin(const char *hex, u8 *bin, size_t bin_len); +void bin2hex(const u8 *bin, size_t bin_len, char *hex); + +struct string_list { + char **strings; + size_t length; + size_t capacity; +}; + +#define STRING_LIST_INITIALIZER { .strings = NULL, .length = 0, .capacity = 0 } +#define STRING_LIST(_list) struct string_list _list = STRING_LIST_INITIALIZER + +void string_list_append(struct string_list *list, char *string); +void string_list_destroy(struct string_list *list); + +#endif /* UTIL_H */ |