diff options
Diffstat (limited to 'libmount/src/hook_veritydev.c')
-rw-r--r-- | libmount/src/hook_veritydev.c | 646 |
1 files changed, 646 insertions, 0 deletions
diff --git a/libmount/src/hook_veritydev.c b/libmount/src/hook_veritydev.c new file mode 100644 index 0000000..f91778a --- /dev/null +++ b/libmount/src/hook_veritydev.c @@ -0,0 +1,646 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * This file is part of libmount from util-linux project. + * + * Copyright (C) 2019 Microsoft Corporation + * Copyright (C) 2022 Karel Zak <kzak@redhat.com> + * + * libmount is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * + * Please, see comment in libmount/src/hooks.c to understand how hooks work. + */ +#include "mountP.h" + +#ifdef HAVE_CRYPTSETUP + +#include <libcryptsetup.h> +#include "path.h" +#include "strutils.h" +#include "fileutils.h" +#include "pathnames.h" + +#ifdef CRYPTSETUP_VIA_DLOPEN +# include <dlfcn.h> + +/* Pointers to libcryptsetup functions (initiliazed by dlsym()) */ +struct verity_opers { + void (*crypt_set_debug_level)(int); + void (*crypt_set_log_callback)(struct crypt_device *, void (*log)(int, const char *, void *), void *); + int (*crypt_init_data_device)(struct crypt_device **, const char *, const char *); + int (*crypt_load)(struct crypt_device *, const char *, void *); + int (*crypt_get_volume_key_size)(struct crypt_device *); +# ifdef HAVE_CRYPT_ACTIVATE_BY_SIGNED_KEY + int (*crypt_activate_by_signed_key)(struct crypt_device *, const char *, const char *, size_t, const char *, size_t, uint32_t); +# endif + int (*crypt_activate_by_volume_key)(struct crypt_device *, const char *, const char *, size_t, uint32_t); + void (*crypt_free)(struct crypt_device *); + int (*crypt_init_by_name)(struct crypt_device **, const char *); + int (*crypt_get_verity_info)(struct crypt_device *, struct crypt_params_verity *); + int (*crypt_volume_key_get)(struct crypt_device *, int, char *, size_t *, const char *, size_t); + + int (*crypt_deactivate_by_name)(struct crypt_device *, const char *, uint32_t); +}; + +/* libcryptsetup functions names and offsets in 'struct verity_opers' */ +struct verity_sym { + const char *name; + size_t offset; /* offset of the symbol in verity_opers */ +}; + +# define DEF_VERITY_SYM(_name) \ + { \ + .name = # _name, \ + .offset = offsetof(struct verity_opers, _name), \ + } + +/* All required symbols */ +static const struct verity_sym verity_symbols[] = +{ + DEF_VERITY_SYM( crypt_set_debug_level ), + DEF_VERITY_SYM( crypt_set_log_callback ), + DEF_VERITY_SYM( crypt_init_data_device ), + DEF_VERITY_SYM( crypt_load ), + DEF_VERITY_SYM( crypt_get_volume_key_size ), +# ifdef HAVE_CRYPT_ACTIVATE_BY_SIGNED_KEY + DEF_VERITY_SYM( crypt_activate_by_signed_key ), +# endif + DEF_VERITY_SYM( crypt_activate_by_volume_key ), + DEF_VERITY_SYM( crypt_free ), + DEF_VERITY_SYM( crypt_init_by_name ), + DEF_VERITY_SYM( crypt_get_verity_info ), + DEF_VERITY_SYM( crypt_volume_key_get ), + + DEF_VERITY_SYM( crypt_deactivate_by_name ), +}; +#endif /* CRYPTSETUP_VIA_DLOPEN */ + + +/* Data used by all verity hooks */ +struct hookset_data { + char *devname; /* the device */ +#ifdef CRYPTSETUP_VIA_DLOPEN + void *dl; /* dlopen() */ + struct verity_opers dl_funcs; /* dlsym() */ +#endif +}; + +/* libcryptsetup call -- dlopen version requires 'struct hookset_data *hsd' */ +#ifdef CRYPTSETUP_VIA_DLOPEN +# define verity_call(_func) (hsd->dl_funcs._func) +#else +# define verity_call(_func) (_func) +#endif + + +static void delete_veritydev(struct libmnt_context *cxt, + const struct libmnt_hookset *hs, + struct hookset_data *hsd); + + +#ifdef CRYPTSETUP_VIA_DLOPEN +static int load_libcryptsetup_symbols(struct libmnt_context *cxt, + const struct libmnt_hookset *hs, + struct hookset_data *hsd) +{ + size_t i; + int flags = RTLD_LAZY | RTLD_LOCAL; + + assert(cxt); + assert(hsd); + assert(hsd->dl == NULL); + + /* glibc extension: mnt_context_deferred_delete_veritydev is called immediately after, don't unload on dl_close */ +#ifdef RTLD_NODELETE + flags |= RTLD_NODELETE; +#endif + /* glibc extension: might help to avoid further symbols clashes */ +#ifdef RTLD_DEEPBIND + flags |= RTLD_DEEPBIND; +#endif + hsd->dl = dlopen("libcryptsetup.so.12", flags); + if (!hsd->dl) { + DBG(HOOK, ul_debugobj(hs, "cannot dlopen libcryptsetup")); + return -ENOTSUP; + } + + /* clear errors first, then load all the libcryptsetup symbols */ + dlerror(); + + /* dlsym() */ + for (i = 0; i < ARRAY_SIZE(verity_symbols); i++) { + char *errmsg; + const struct verity_sym *def = &verity_symbols[i]; + void **sym; + + sym = (void **) ((char *) (&hsd->dl_funcs) + def->offset); + *sym = dlsym(hsd->dl, def->name); + + errmsg = dlerror(); + if (errmsg) { + DBG(HOOK, ul_debugobj(hs, "dlsym failed %s: %s", def->name, errmsg)); + return -ENOTSUP; + } + } + return 0; +} +#endif + +/* libcryptsetup callback */ +static void libcryptsetup_log(int level __attribute__((__unused__)), + const char *msg, void *data) +{ + const struct libmnt_hookset *hs = (struct libmnt_hookset *) data; + DBG(HOOK, ul_debugobj(hs, "cryptsetup: %s", msg)); +} + +/* free global data */ +static void free_hookset_data( struct libmnt_context *cxt, + const struct libmnt_hookset *hs) +{ + struct hookset_data *hsd = mnt_context_get_hookset_data(cxt, hs); + + if (!hsd) + return; + if (hsd->devname) + delete_veritydev(cxt, hs, hsd); +#ifdef CRYPTSETUP_VIA_DLOPEN + if (hsd->dl) + dlclose(hsd->dl); +#endif + free(hsd); + mnt_context_set_hookset_data(cxt, hs, NULL); +} + +/* global data, used by all callbacks */ +static struct hookset_data *new_hookset_data( + struct libmnt_context *cxt, + const struct libmnt_hookset *hs) +{ + struct hookset_data *hsd = calloc(1, sizeof(struct hookset_data)); + + if (hsd && mnt_context_set_hookset_data(cxt, hs, hsd) != 0) + goto failed; + +#ifdef CRYPTSETUP_VIA_DLOPEN + if (load_libcryptsetup_symbols(cxt, hs, hsd) != 0) + goto failed; +#endif + if (mnt_context_is_verbose(cxt)) + verity_call( crypt_set_debug_level(CRYPT_DEBUG_ALL) ); + + verity_call( crypt_set_log_callback(NULL, libcryptsetup_log, (void *) hs) ); + + return hsd; +failed: + free(hsd); + return NULL; +} + +/* libmount callback -- cleanup all */ +static int hookset_deinit(struct libmnt_context *cxt, const struct libmnt_hookset *hs) +{ + DBG(HOOK, ul_debugobj(hs, "deinit '%s'", hs->name)); + + /* remove all our hooks */ + while (mnt_context_remove_hook(cxt, hs, 0, NULL) == 0); + + /* free and remove global hookset data */ + free_hookset_data(cxt, hs); + + return 0; +} + +/* check mount options for verity stuff */ +static int is_veritydev_required(struct libmnt_context *cxt, + const struct libmnt_hookset *hs, + struct libmnt_optlist *ol) +{ + const char *src; + unsigned long flags = 0; + + assert(cxt); + assert((cxt->flags & MNT_FL_MOUNTFLAGS_MERGED)); + + if (cxt->action != MNT_ACT_MOUNT) + return 0; + if (!cxt->fs) + return 0; + src = mnt_fs_get_srcpath(cxt->fs); + if (!src) + return 0; /* backing file not set */ + + ol = mnt_context_get_optlist(cxt); + if (!ol) + return 0; + if (mnt_optlist_is_bind(ol) + || mnt_optlist_is_move(ol) + || mnt_context_propagation_only(cxt)) + return 0; + + if (mnt_context_get_user_mflags(cxt, &flags)) + return 0; + + if (flags & (MNT_MS_HASH_DEVICE | MNT_MS_ROOT_HASH | MNT_MS_HASH_OFFSET)) { + DBG(HOOK, ul_debugobj(hs, "verity options detected")); + return 1; + } + + return 0; +} + +static void delete_veritydev(struct libmnt_context *cxt, + const struct libmnt_hookset *hs, + struct hookset_data *hsd) +{ + uint32_t flags = 0; + int rc; + + if (!hsd || !hsd->devname) + return; + + if (mnt_context_get_status(cxt) != 0) + /* + * mount(2) success, use deferred deactivation + */ + flags |= CRYPT_DEACTIVATE_DEFERRED; + + rc = verity_call( crypt_deactivate_by_name(NULL, hsd->devname, flags) ); + + DBG(HOOK, ul_debugobj(hs, "deleted %s [rc=%d%s]", + hsd->devname, rc, + flags & CRYPT_DEACTIVATE_DEFERRED ? " deferred" : "" )); + if (rc == 0) { + free(hsd->devname); + hsd->devname = NULL; + } + + return; +} + +/* Taken from https://gitlab.com/cryptsetup/cryptsetup/blob/master/lib/utils_crypt.c#L225 */ +static size_t crypt_hex_to_bytes(const char *hex, char **result) +{ + char buf[3] = "xx\0", *endp, *bytes; + size_t i, len; + + len = strlen(hex); + if (len % 2) + return -EINVAL; + len /= 2; + + bytes = malloc(len); + if (!bytes) + return -ENOMEM; + + for (i = 0; i < len; i++) { + memcpy(buf, &hex[i * 2], 2); + errno = 0; + bytes[i] = strtoul(buf, &endp, 16); + if (errno || endp != &buf[2]) { + free(bytes); + return -EINVAL; + } + } + *result = bytes; + return i; +} + + +static int setup_veritydev( struct libmnt_context *cxt, + const struct libmnt_hookset *hs, + struct hookset_data *hsd, + struct libmnt_optlist *ol) +{ + struct libmnt_opt *opt; + const char *backing_file, + *hash_device = NULL, + *root_hash_file = NULL, + *fec_device = NULL, + *root_hash_sig_file = NULL; + char *key = NULL, + *root_hash_binary = NULL, + *mapper_device = NULL, + *root_hash = NULL, + *hash_sig = NULL; + + size_t hash_size, hash_sig_size = 0, keysize = 0; + struct crypt_params_verity crypt_params = {}; + struct crypt_device *crypt_dev = NULL; + + int rc = 0; + /* Use the same default for FEC parity bytes as cryptsetup uses */ + uint64_t offset = 0, fec_offset = 0, fec_roots = 2; + uint32_t crypt_activate_flags = CRYPT_ACTIVATE_READONLY; + + struct stat hash_sig_st; + + assert(cxt); + assert(cxt->fs); + assert(hsd); + assert(hsd->devname == NULL); + + /* dm-verity volumes are read-only, and mount will fail if not set */ + mnt_optlist_append_flags(ol, MS_RDONLY, cxt->map_linux); + + backing_file = mnt_fs_get_srcpath(cxt->fs); + if (!backing_file) + return -EINVAL; + else { + /* To avoid clashes, prefix libmnt_ to all mapper devices */ + char *p, *path = strdup(backing_file); + if (!path) + return -ENOMEM; + + p = stripoff_last_component(path); + if (p) + mapper_device = calloc(strlen(p) + sizeof("libmnt_"), sizeof(char)); + if (mapper_device) { + strcat(mapper_device, "libmnt_"); + strcat(mapper_device, p); + } + free(path); + if (!mapper_device) + return -ENOMEM; + } + + DBG(HOOK, ul_debugobj(hs, "verity: setup for %s [%s]", backing_file, mapper_device)); + + /* verity.hashdevice= */ + if (!rc && (opt = mnt_optlist_get_opt(ol, MNT_MS_HASH_DEVICE, cxt->map_userspace))) + hash_device = mnt_opt_get_value(opt); + + /* verity.roothash= */ + if (!rc && (opt = mnt_optlist_get_opt(ol, MNT_MS_ROOT_HASH, cxt->map_userspace))) { + root_hash = strdup(mnt_opt_get_value(opt)); + rc = root_hash ? 0 : -ENOMEM; + } + + /* verity.hashoffset= */ + if (!rc && (opt = mnt_optlist_get_opt(ol, MNT_MS_HASH_OFFSET, cxt->map_userspace)) + && mnt_opt_has_value(opt)) { + if (strtosize(mnt_opt_get_value(opt), &offset)) { + DBG(HOOK, ul_debugobj(hs, "failed to parse verity.hashoffset=")); + rc = -MNT_ERR_MOUNTOPT; + } + } + + /* verity.roothashfile= */ + if (!rc && (opt = mnt_optlist_get_opt(ol, MNT_MS_ROOT_HASH_FILE, cxt->map_userspace))) + root_hash_file = mnt_opt_get_value(opt); + + /* verity.fecdevice= */ + if (!rc && (opt = mnt_optlist_get_opt(ol, MNT_MS_FEC_DEVICE, cxt->map_userspace))) + fec_device = mnt_opt_get_value(opt); + + /* verity.fecoffset= */ + if (!rc && (opt = mnt_optlist_get_opt(ol, MNT_MS_FEC_OFFSET, cxt->map_userspace)) + && mnt_opt_has_value(opt) + && strtosize(mnt_opt_get_value(opt), &fec_offset)) { + DBG(HOOK, ul_debugobj(hs, "failed to parse verity.fecoffset=")); + rc = -MNT_ERR_MOUNTOPT; + } + + /* verity.fecroots= */ + if (!rc && (opt = mnt_optlist_get_opt(ol, MNT_MS_FEC_ROOTS, cxt->map_userspace)) + && mnt_opt_has_value(opt) + && strtosize(mnt_opt_get_value(opt), &fec_roots)) { + DBG(HOOK, ul_debugobj(hs, "failed to parse verity.fecroots=")); + rc = -MNT_ERR_MOUNTOPT; + } + + /* verity.roothashsig= */ + if (!rc && (opt = mnt_optlist_get_opt(ol, MNT_MS_ROOT_HASH_SIG, cxt->map_userspace)) + && mnt_opt_has_value(opt)) { + root_hash_sig_file = mnt_opt_get_value(opt); + + DBG(HOOK, ul_debugobj(hs, "verity: checking %s", root_hash_sig_file)); + + rc = ul_path_stat(NULL, &hash_sig_st, 0, root_hash_sig_file); + if (rc == 0) + rc = S_ISREG(hash_sig_st.st_mode) && hash_sig_st.st_size ? 0 : -EINVAL; + if (rc == 0) { + hash_sig_size = hash_sig_st.st_size; + hash_sig = malloc(hash_sig_size); + rc = hash_sig ? 0 : -ENOMEM; + } + if (rc == 0) { + rc = ul_path_read(NULL, hash_sig, hash_sig_size, root_hash_sig_file); + rc = rc < (int)hash_sig_size ? -1 : 0; + } + } + + /* verity.oncorruption= */ + if (!rc && (opt = mnt_optlist_get_opt(ol, MNT_MS_VERITY_ON_CORRUPTION, cxt->map_userspace)) + && mnt_opt_has_value(opt)) { + const char *val = mnt_opt_get_value(opt); + if (!strcmp(val, "ignore")) + crypt_activate_flags |= CRYPT_ACTIVATE_IGNORE_CORRUPTION; + else if (!strcmp(val, "restart")) + crypt_activate_flags |= CRYPT_ACTIVATE_RESTART_ON_CORRUPTION; + else if (!strcmp(val, "panic")) + /* Added by libcryptsetup v2.3.4 - ignore on lower versions, as with other optional features */ +#ifdef CRYPT_ACTIVATE_PANIC_ON_CORRUPTION + crypt_activate_flags |= CRYPT_ACTIVATE_PANIC_ON_CORRUPTION; +#else + DBG(HOOK, ul_debugobj(hs, "verity.oncorruption=panic not supported by libcryptsetup, ignoring")); +#endif + else { + DBG(HOOK, ul_debugobj(hs, "failed to parse verity.oncorruption=")); + rc = -MNT_ERR_MOUNTOPT; + } + } + + if (!rc && root_hash && root_hash_file) { + DBG(HOOK, ul_debugobj(hs, "verity.roothash and verity.roothashfile are mutually exclusive")); + rc = -EINVAL; + } else if (!rc && root_hash_file) { + rc = ul_path_read_string(NULL, &root_hash, root_hash_file); + rc = rc < 1 ? rc : 0; + } + + if (!rc && (!hash_device || !root_hash)) { + DBG(HOOK, ul_debugobj(hs, "verity.hashdevice and one of verity.roothash or verity.roothashfile are mandatory")); + rc = -EINVAL; + } + + if (!rc) + rc = verity_call( crypt_init_data_device(&crypt_dev, hash_device, backing_file) ); + if (rc) + goto done; + + memset(&crypt_params, 0, sizeof(struct crypt_params_verity)); + crypt_params.hash_area_offset = offset; + crypt_params.fec_area_offset = fec_offset; + crypt_params.fec_roots = fec_roots; + crypt_params.fec_device = fec_device; + crypt_params.flags = 0; + + rc = verity_call( crypt_load(crypt_dev, CRYPT_VERITY, &crypt_params) ); + if (rc < 0) + goto done; + + hash_size = verity_call( crypt_get_volume_key_size(crypt_dev) ); + if (crypt_hex_to_bytes(root_hash, &root_hash_binary) != hash_size) { + DBG(HOOK, ul_debugobj(hs, "root hash %s is not of length %zu", root_hash, hash_size)); + rc = -EINVAL; + goto done; + } + + if (hash_sig) { +#ifdef HAVE_CRYPT_ACTIVATE_BY_SIGNED_KEY + rc = verity_call( crypt_activate_by_signed_key(crypt_dev, mapper_device, root_hash_binary, hash_size, + hash_sig, hash_sig_size, crypt_activate_flags) ); +#else + rc = -EINVAL; + DBG(HOOK, ul_debugobj(hs, "verity.roothashsig=%s passed but libcryptsetup does not provide crypt_activate_by_signed_key()", hash_sig)); +#endif + } else + rc = verity_call( crypt_activate_by_volume_key(crypt_dev, mapper_device, root_hash_binary, hash_size, + crypt_activate_flags) ); + + /* + * If the mapper device already exists, and if libcryptsetup supports it, get the root + * hash associated with the existing one and compare it with the parameter passed by + * the user. If they match, then we can be sure the user intended to mount the exact + * same device, and simply reuse it and return success. + * The kernel does the refcounting for us. + * If libcryptsetup does not support getting the root hash out of an existing device, + * then return an error and tell the user that the device is already in use. + * Pass through only OOM errors or mismatching root hash errors. + */ + if (rc == -EEXIST) { + DBG(HOOK, ul_debugobj(hs, "%s already in use as /dev/mapper/%s", backing_file, mapper_device)); + + verity_call( crypt_free(crypt_dev) ); + + rc = verity_call( crypt_init_by_name(&crypt_dev, mapper_device) ); + if (!rc) { + rc = verity_call( crypt_get_verity_info(crypt_dev, &crypt_params) ); + if (!rc) { + key = calloc(hash_size, 1); + if (!key) { + rc = -ENOMEM; + goto done; + } + } + if (!rc) { + keysize = hash_size; + rc = verity_call( crypt_volume_key_get(crypt_dev, CRYPT_ANY_SLOT, key, &keysize, NULL, 0) ); + } + if (!rc) { + DBG(HOOK, ul_debugobj(hs, "comparing root hash of existing device with %s", root_hash)); + if (memcmp(key, root_hash_binary, hash_size)) { + DBG(HOOK, ul_debugobj(hs, "existing device's hash does not match with %s", root_hash)); + rc = -EINVAL; + goto done; + } + } else { + DBG(HOOK, ul_debugobj(hs, "libcryptsetup does not support extracting root hash of existing device")); + } + } + if (rc) { + rc = -EEXIST; + } else { + /* + * Ensure that, if signatures are supported, we only reuse the device if the previous mount + * used the same settings, so that a previous unsigned mount will not be reused if the user + * asks to use signing for the new one, and viceversa. + */ +#ifdef HAVE_CRYPT_ACTIVATE_BY_SIGNED_KEY + if (!!hash_sig != !!(crypt_params.flags & CRYPT_VERITY_ROOT_HASH_SIGNATURE)) { + rc = -EINVAL; + DBG(HOOK, ul_debugobj(hs, "existing device and new mount have to either be both opened with signature or both without")); + goto done; + } +#endif + DBG(HOOK, ul_debugobj(hs, "root hash of %s matches %s, reusing device", mapper_device, root_hash)); + } + } + + if (!rc) { + hsd->devname = calloc(strlen(mapper_device) + + sizeof(_PATH_DEV_MAPPER) + 2, sizeof(char)); + if (!hsd->devname) + rc = -ENOMEM; + else { + strcat(hsd->devname, _PATH_DEV_MAPPER "/"); + strcat(hsd->devname, mapper_device); + rc = mnt_fs_set_source(cxt->fs, hsd->devname); + } + } + +done: + verity_call( crypt_free(crypt_dev) ); + + free(root_hash_binary); + free(mapper_device); + free(root_hash); + free(hash_sig); + free(key); + return rc; +} +/* call after mount(2) */ +static int hook_mount_post( + struct libmnt_context *cxt, + const struct libmnt_hookset *hs, + void *data __attribute__((__unused__))) +{ + + delete_veritydev(cxt, hs, mnt_context_get_hookset_data(cxt, hs)); + return 0; +} + +/* + * first call (first callback in this hookset) + */ +static int hook_prepare_source( + struct libmnt_context *cxt, + const struct libmnt_hookset *hs, + void *data __attribute__((__unused__))) +{ + struct libmnt_optlist *ol; + struct hookset_data *hsd; + int rc; + + assert(cxt); + + ol = mnt_context_get_optlist(cxt); + if (!ol) + return -ENOMEM; + + if (!is_veritydev_required(cxt, hs, ol)) + return 0; + + hsd = new_hookset_data(cxt, hs); + if (!hsd) + return -ENOMEM; + + rc = setup_veritydev(cxt, hs, hsd, ol); + if (!rc) { + rc = mnt_context_append_hook(cxt, hs, + MNT_STAGE_MOUNT_POST, + NULL, hook_mount_post); + if (rc) + delete_veritydev(cxt, hs, hsd); + } + return rc; +} + + +const struct libmnt_hookset hookset_veritydev = +{ + .name = "__veritydev", + + .firststage = MNT_STAGE_PREP_SOURCE, + .firstcall = hook_prepare_source, + + .deinit = hookset_deinit +}; +#endif /*HAVE_CRYPTSETUP*/ + + + |