/* 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 * * 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 #include "path.h" #include "strutils.h" #include "fileutils.h" #include "pathnames.h" #ifdef CRYPTSETUP_VIA_DLOPEN # include /* 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*/