From c2923fdcd771e1e6470a6c67c23d4b21f536e7f6 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Tue, 2 Jun 2020 15:35:58 +0100 Subject: [PATCH] dissect/nspawn: add support for dm-verity root hash signature Since cryptsetup 2.3.0 a new API to verify dm-verity volumes by a pkcs7 signature, with the public key in the kernel keyring, is available. Use it if libcryptsetup supports it. --- README | 6 ++- man/systemd-nspawn.xml | 14 +++++- meson.build | 2 + shell-completion/bash/systemd-nspawn | 6 ++- src/core/namespace.c | 6 +-- src/dissect/dissect.c | 57 ++++++++++++++++++++----- src/nspawn/nspawn.c | 41 +++++++++++++++++- src/shared/dissect-image.c | 64 +++++++++++++++++++++++++--- src/shared/dissect-image.h | 6 +-- 9 files changed, 176 insertions(+), 26 deletions(-) diff --git a/README b/README index 4f4a21eeca..4269f0c73d 100644 --- a/README +++ b/README @@ -35,6 +35,7 @@ LICENSE: REQUIREMENTS: Linux kernel >= 3.13 Linux kernel >= 4.2 for unified cgroup hierarchy support + Linux kernel >= 5.4 for signed Verity images support Kernel Config Options: CONFIG_DEVTMPFS @@ -102,6 +103,9 @@ REQUIREMENTS: CONFIG_EFIVAR_FS CONFIG_EFI_PARTITION + Required for signed Verity images support: + CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG + We recommend to turn off Real-Time group scheduling in the kernel when using systemd. RT group scheduling effectively makes RT scheduling unavailable for most userspace, since it @@ -144,7 +148,7 @@ REQUIREMENTS: libblkid >= 2.24 (from util-linux) (optional) libkmod >= 15 (optional) PAM >= 1.1.2 (optional) - libcryptsetup (optional) + libcryptsetup (optional), >= 2.3.0 required for signed Verity images support libaudit (optional) libacl (optional) libselinux (optional) diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index abcddbf00a..e28572db6d 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -304,7 +304,7 @@ Single file system images (i.e. file systems without a surrounding partition table) can be opened using dm-verity if the integrity data is passed using the and - options. + (and optionally ) options. Any other partitions, such as foreign partitions or swap partitions are not mounted. May not be specified together with , . @@ -399,6 +399,18 @@ is read from it and automatically used, also as formatted hexadecimal characters. + + + + Takes a PKCS7 formatted binary signature of the option as a path + to a DER encoded signature file or as an ASCII base64 string encoding of the DER encoded signature, prefixed + by base64:. The dm-verity volume will only be opened if the signature of the root hash hex + string is valid and done by a public key present in the kernel keyring. If this option is not specified, but a + file with the .roothash.p7s suffix is found next to the image file, bearing otherwise the + same name (except if the image has the .raw suffix, in which case the signature file must + not have it in its name), the signature is read from it and automatically used. + + diff --git a/meson.build b/meson.build index 8f1d1b5897..827fdfc8cf 100644 --- a/meson.build +++ b/meson.build @@ -1035,6 +1035,8 @@ if want_libcryptsetup != 'false' and not skip_deps conf.set10('HAVE_CRYPT_SET_METADATA_SIZE', have and cc.has_function('crypt_set_metadata_size', dependencies : libcryptsetup)) + conf.set10('HAVE_CRYPT_ACTIVATE_BY_SIGNED_KEY', + have and cc.has_function('crypt_activate_by_signed_key', dependencies : libcryptsetup)) else have = false libcryptsetup = [] diff --git a/shell-completion/bash/systemd-nspawn b/shell-completion/bash/systemd-nspawn index a731167680..a8bd406fb3 100644 --- a/shell-completion/bash/systemd-nspawn +++ b/shell-completion/bash/systemd-nspawn @@ -71,7 +71,7 @@ _systemd_nspawn() { --pivot-root --property --private-users --network-namespace-path --network-ipvlan --network-veth-extra --network-zone -p --port --system-call-filter --overlay --overlay-ro --settings --rlimit --hostname --no-new-privileges --oom-score-adjust --cpu-affinity - --resolv-conf --timezone' + --resolv-conf --timezone --root-hash-sig' ) _init_completion || return @@ -183,6 +183,10 @@ _systemd_nspawn() { --timezone) comps=$( systemd-nspawn --timezone=help 2>/dev/null ) ;; + --root-hash-sig) + compopt -o nospace + comps=$( compgen -A file -- "$cur" ) + ;; esac COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) return 0 diff --git a/src/core/namespace.c b/src/core/namespace.c index 7bb4440747..a9985c5657 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -1268,7 +1268,7 @@ int setup_namespace( _cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL; _cleanup_(dissected_image_unrefp) DissectedImage *dissected_image = NULL; _cleanup_free_ void *root_hash_decoded = NULL; - _cleanup_free_ char *verity_data = NULL; + _cleanup_free_ char *verity_data = NULL, *hash_sig_path = NULL; MountEntry *m = NULL, *mounts = NULL; size_t n_mounts; bool require_prefix = false; @@ -1299,7 +1299,7 @@ int setup_namespace( if (r < 0) return log_debug_errno(r, "Failed to create loop device for root image: %m"); - r = verity_metadata_load(root_image, root_hash_path, root_hash ? NULL : &root_hash_decoded, root_hash ? NULL : &root_hash_size, root_verity ? NULL : &verity_data); + r = verity_metadata_load(root_image, root_hash_path, root_hash ? NULL : &root_hash_decoded, root_hash ? NULL : &root_hash_size, root_verity ? NULL : &verity_data, &hash_sig_path); if (r < 0) return log_debug_errno(r, "Failed to load root hash: %m"); dissect_image_flags |= root_verity || verity_data ? DISSECT_IMAGE_NO_PARTITION_TABLE : 0; @@ -1308,7 +1308,7 @@ int setup_namespace( if (r < 0) return log_debug_errno(r, "Failed to dissect image: %m"); - r = dissected_image_decrypt(dissected_image, NULL, root_hash ?: root_hash_decoded, root_hash_size, root_verity ?: verity_data, dissect_image_flags, &decrypted_image); + r = dissected_image_decrypt(dissected_image, NULL, root_hash ?: root_hash_decoded, root_hash_size, root_verity ?: verity_data, hash_sig_path, NULL, 0, dissect_image_flags, &decrypted_image); if (r < 0) return log_debug_errno(r, "Failed to decrypt dissected image: %m"); } diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 2a8eaca5bd..66ac638401 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -28,9 +28,14 @@ static DissectImageFlags arg_flags = DISSECT_IMAGE_REQUIRE_ROOT|DISSECT_IMAGE_DI static void *arg_root_hash = NULL; static char *arg_verity_data = NULL; static size_t arg_root_hash_size = 0; +static char *arg_root_hash_sig_path = NULL; +static void *arg_root_hash_sig = NULL; +static size_t arg_root_hash_sig_size = 0; STATIC_DESTRUCTOR_REGISTER(arg_root_hash, freep); STATIC_DESTRUCTOR_REGISTER(arg_verity_data, freep); +STATIC_DESTRUCTOR_REGISTER(arg_root_hash_sig_path, freep); +STATIC_DESTRUCTOR_REGISTER(arg_root_hash_sig, freep); static void help(void) { printf("%s [OPTIONS...] IMAGE\n" @@ -43,6 +48,10 @@ static void help(void) { " --fsck=BOOL Run fsck before mounting\n" " --discard=MODE Choose 'discard' mode (disabled, loop, all, crypto)\n" " --root-hash=HASH Specify root hash for verity\n" + " --root-hash-sig=SIG Specify pkcs7 signature of root hash for verity\n" + " as a DER encoded PKCS7, either as a path to a file\n" + " or as an ASCII base64 encoded string prefixed by\n" + " 'base64:'\n" " --verity-data=PATH Specify data file with hash tree for verity if it is\n" " not embedded in IMAGE\n", program_invocation_short_name, @@ -57,17 +66,19 @@ static int parse_argv(int argc, char *argv[]) { ARG_ROOT_HASH, ARG_FSCK, ARG_VERITY_DATA, + ARG_ROOT_HASH_SIG, }; static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "mount", no_argument, NULL, 'm' }, - { "read-only", no_argument, NULL, 'r' }, - { "discard", required_argument, NULL, ARG_DISCARD }, - { "root-hash", required_argument, NULL, ARG_ROOT_HASH }, - { "fsck", required_argument, NULL, ARG_FSCK }, - { "verity-data", required_argument, NULL, ARG_VERITY_DATA }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "mount", no_argument, NULL, 'm' }, + { "read-only", no_argument, NULL, 'r' }, + { "discard", required_argument, NULL, ARG_DISCARD }, + { "root-hash", required_argument, NULL, ARG_ROOT_HASH }, + { "fsck", required_argument, NULL, ARG_FSCK }, + { "verity-data", required_argument, NULL, ARG_VERITY_DATA }, + { "root-hash-sig", required_argument, NULL, ARG_ROOT_HASH_SIG }, {} }; @@ -140,6 +151,31 @@ static int parse_argv(int argc, char *argv[]) { return r; break; + case ARG_ROOT_HASH_SIG: { + char *value; + + if ((value = startswith(optarg, "base64:"))) { + void *p; + size_t l; + + r = unbase64mem(value, strlen(value), &p, &l); + if (r < 0) + return log_error_errno(r, "Failed to parse root hash signature '%s': %m", optarg); + + free_and_replace(arg_root_hash_sig, p); + arg_root_hash_sig_size = l; + arg_root_hash_sig_path = mfree(arg_root_hash_sig_path); + } else { + r = parse_path_argument_and_warn(optarg, false, &arg_root_hash_sig_path); + if (r < 0) + return r; + arg_root_hash_sig = mfree(arg_root_hash_sig); + arg_root_hash_sig_size = 0; + } + + break; + } + case ARG_FSCK: r = parse_boolean(optarg); if (r < 0) @@ -202,7 +238,8 @@ static int run(int argc, char *argv[]) { return log_error_errno(r, "Failed to set up loopback device: %m"); r = verity_metadata_load(arg_image, NULL, arg_root_hash ? NULL : &arg_root_hash, &arg_root_hash_size, - arg_verity_data ? NULL : &arg_verity_data); + arg_verity_data ? NULL : &arg_verity_data, + arg_root_hash_sig_path || arg_root_hash_sig ? NULL : &arg_root_hash_sig_path); if (r < 0) return log_error_errno(r, "Failed to read verity artefacts for %s: %m", arg_image); arg_flags |= arg_verity_data ? DISSECT_IMAGE_NO_PARTITION_TABLE : 0; @@ -279,7 +316,7 @@ static int run(int argc, char *argv[]) { } case ACTION_MOUNT: - r = dissected_image_decrypt_interactively(m, NULL, arg_root_hash, arg_root_hash_size, arg_verity_data, arg_flags, &di); + r = dissected_image_decrypt_interactively(m, NULL, arg_root_hash, arg_root_hash_size, arg_verity_data, arg_root_hash_sig_path, arg_root_hash_sig, arg_root_hash_sig_size, arg_flags, &di); if (r < 0) return r; diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 2f86d4094d..0f2d01c0aa 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -200,6 +200,9 @@ static unsigned long arg_clone_ns_flags = CLONE_NEWIPC|CLONE_NEWPID|CLONE_NEWUTS static MountSettingsMask arg_mount_settings = MOUNT_APPLY_APIVFS_RO|MOUNT_APPLY_TMPFS_TMP; static void *arg_root_hash = NULL; static char *arg_verity_data = NULL; +static char *arg_root_hash_sig_path = NULL; +static void *arg_root_hash_sig = NULL; +static size_t arg_root_hash_sig_size = 0; static size_t arg_root_hash_size = 0; static char **arg_syscall_allow_list = NULL; static char **arg_syscall_deny_list = NULL; @@ -244,6 +247,8 @@ STATIC_DESTRUCTOR_REGISTER(arg_property_message, sd_bus_message_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_parameters, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_root_hash, freep); STATIC_DESTRUCTOR_REGISTER(arg_verity_data, freep); +STATIC_DESTRUCTOR_REGISTER(arg_root_hash_sig_path, freep); +STATIC_DESTRUCTOR_REGISTER(arg_root_hash_sig, freep); STATIC_DESTRUCTOR_REGISTER(arg_syscall_allow_list, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_syscall_deny_list, strv_freep); #if HAVE_SECCOMP @@ -305,6 +310,10 @@ static int help(void) { " --read-only Mount the root directory read-only\n" " --volatile[=MODE] Run the system in volatile mode\n" " --root-hash=HASH Specify verity root hash for root disk image\n" + " --root-hash-sig=SIG Specify pkcs7 signature of root hash for verity\n" + " as a DER encoded PKCS7, either as a path to a file\n" + " or as an ASCII base64 encoded string prefixed by\n" + " 'base64:'\n" " --verity-data=PATH Specify hash device for verity\n" " --pivot-root=PATH[:PATH]\n" " Pivot root to given directory in the container\n\n" @@ -667,6 +676,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_OCI_BUNDLE, ARG_NO_PAGER, ARG_VERITY_DATA, + ARG_ROOT_HASH_SIG, }; static const struct option options[] = { @@ -733,6 +743,7 @@ static int parse_argv(int argc, char *argv[]) { { "oci-bundle", required_argument, NULL, ARG_OCI_BUNDLE }, { "no-pager", no_argument, NULL, ARG_NO_PAGER }, { "verity-data", required_argument, NULL, ARG_VERITY_DATA }, + { "root-hash-sig", required_argument, NULL, ARG_ROOT_HASH_SIG }, {} }; @@ -1327,6 +1338,31 @@ static int parse_argv(int argc, char *argv[]) { return r; break; + case ARG_ROOT_HASH_SIG: { + char *value; + + if ((value = startswith(optarg, "base64:"))) { + void *p; + size_t l; + + r = unbase64mem(value, strlen(value), &p, &l); + if (r < 0) + return log_error_errno(r, "Failed to parse root hash signature '%s': %m", optarg); + + free_and_replace(arg_root_hash_sig, p); + arg_root_hash_sig_size = l; + arg_root_hash_sig_path = mfree(arg_root_hash_sig_path); + } else { + r = parse_path_argument_and_warn(optarg, false, &arg_root_hash_sig_path); + if (r < 0) + return r; + arg_root_hash_sig = mfree(arg_root_hash_sig); + arg_root_hash_sig_size = 0; + } + + break; + } + case ARG_SYSTEM_CALL_FILTER: { bool negative; const char *items; @@ -5143,7 +5179,8 @@ static int run(int argc, char *argv[]) { } r = verity_metadata_load(arg_image, NULL, arg_root_hash ? NULL : &arg_root_hash, &arg_root_hash_size, - arg_verity_data ? NULL : &arg_verity_data); + arg_verity_data ? NULL : &arg_verity_data, + arg_root_hash_sig_path || arg_root_hash_sig ? NULL : &arg_root_hash_sig_path); if (r < 0) { log_error_errno(r, "Failed to read verity artefacts for %s: %m", arg_image); goto finish; @@ -5193,7 +5230,7 @@ static int run(int argc, char *argv[]) { if (!arg_root_hash && dissected_image->can_verity) log_notice("Note: image %s contains verity information, but no root hash specified! Proceeding without integrity checking.", arg_image); - r = dissected_image_decrypt_interactively(dissected_image, NULL, arg_root_hash, arg_root_hash_size, arg_verity_data, 0, &decrypted_image); + r = dissected_image_decrypt_interactively(dissected_image, NULL, arg_root_hash, arg_root_hash_size, arg_verity_data, arg_root_hash_sig_path, arg_root_hash_sig, arg_root_hash_sig_size, 0, &decrypted_image); if (r < 0) goto finish; diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index e576518c6b..fdf4d481f6 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -1223,6 +1223,9 @@ static int verity_partition( const void *root_hash, size_t root_hash_size, const char *verity_data, + const char *root_hash_sig_path, + const void *root_hash_sig, + size_t root_hash_sig_size, DissectImageFlags flags, DecryptedImage *d) { @@ -1267,7 +1270,25 @@ static int verity_partition( if (r < 0) return r; - r = crypt_activate_by_volume_key(cd, name, root_hash, root_hash_size, CRYPT_ACTIVATE_READONLY); + if (root_hash_sig || root_hash_sig_path) { +#if HAVE_CRYPT_ACTIVATE_BY_SIGNED_KEY + if (root_hash_sig) + r = crypt_activate_by_signed_key(cd, name, root_hash, root_hash_size, root_hash_sig, root_hash_sig_size, CRYPT_ACTIVATE_READONLY); + else { + _cleanup_free_ char *hash_sig = NULL; + size_t hash_sig_size; + + r = read_full_file_full(AT_FDCWD, root_hash_sig_path, 0, &hash_sig, &hash_sig_size); + if (r < 0) + return r; + + r = crypt_activate_by_signed_key(cd, name, root_hash, root_hash_size, hash_sig, hash_sig_size, CRYPT_ACTIVATE_READONLY); + } +#else + r = log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "activation of verity device with signature requested, but not supported by cryptsetup due to missing crypt_activate_by_signed_key()"); +#endif + } else + r = crypt_activate_by_volume_key(cd, name, root_hash, root_hash_size, CRYPT_ACTIVATE_READONLY); if (r < 0) return r; @@ -1287,6 +1308,9 @@ int dissected_image_decrypt( const void *root_hash, size_t root_hash_size, const char *verity_data, + const char *root_hash_sig_path, + const void *root_hash_sig, + size_t root_hash_sig_size, DissectImageFlags flags, DecryptedImage **ret) { @@ -1333,7 +1357,7 @@ int dissected_image_decrypt( k = PARTITION_VERITY_OF(i); if (k >= 0) { - r = verity_partition(p, m->partitions + k, root_hash, root_hash_size, verity_data, flags, d); + r = verity_partition(p, m->partitions + k, root_hash, root_hash_size, verity_data, root_hash_sig_path, root_hash_sig, root_hash_sig_size, flags, d); if (r < 0) return r; } @@ -1359,6 +1383,9 @@ int dissected_image_decrypt_interactively( const void *root_hash, size_t root_hash_size, const char *verity_data, + const char *root_hash_sig_path, + const void *root_hash_sig, + size_t root_hash_sig_size, DissectImageFlags flags, DecryptedImage **ret) { @@ -1369,7 +1396,7 @@ int dissected_image_decrypt_interactively( n--; for (;;) { - r = dissected_image_decrypt(m, passphrase, root_hash, root_hash_size, verity_data, flags, ret); + r = dissected_image_decrypt(m, passphrase, root_hash, root_hash_size, verity_data, root_hash_sig_path, root_hash_sig, root_hash_sig_size, flags, ret); if (r >= 0) return r; if (r == -EKEYREJECTED) @@ -1421,8 +1448,8 @@ int decrypted_image_relinquish(DecryptedImage *d) { return 0; } -int verity_metadata_load(const char *image, const char *root_hash_path, void **ret_roothash, size_t *ret_roothash_size, char **ret_verity_data) { - _cleanup_free_ char *verity_filename = NULL; +int verity_metadata_load(const char *image, const char *root_hash_path, void **ret_roothash, size_t *ret_roothash_size, char **ret_verity_data, char **ret_roothashsig) { + _cleanup_free_ char *verity_filename = NULL, *roothashsig_filename = NULL; _cleanup_free_ void *roothash_decoded = NULL; size_t roothash_decoded_size = 0; int r; @@ -1437,6 +1464,8 @@ int verity_metadata_load(const char *image, const char *root_hash_path, void **r *ret_roothash_size = 0; if (ret_verity_data) *ret_verity_data = NULL; + if (ret_roothashsig) + *ret_roothashsig = NULL; return 0; } @@ -1461,6 +1490,29 @@ int verity_metadata_load(const char *image, const char *root_hash_path, void **r } } + if (ret_roothashsig) { + char *e; + + /* Follow naming convention recommended by the relevant RFC: + * https://tools.ietf.org/html/rfc5751#section-3.2.1 */ + roothashsig_filename = new(char, strlen(image) + STRLEN(".roothash.p7s") + 1); + if (!roothashsig_filename) + return -ENOMEM; + strcpy(roothashsig_filename, image); + e = endswith(roothashsig_filename, ".raw"); + if (e) + strcpy(e, ".roothash.p7s"); + else + strcat(roothashsig_filename, ".roothash.p7s"); + + r = access(roothashsig_filename, R_OK); + if (r < 0) { + if (errno != ENOENT) + return -errno; + roothashsig_filename = mfree(roothashsig_filename); + } + } + if (ret_roothash) { _cleanup_free_ char *text = NULL; assert(ret_roothash_size); @@ -1507,6 +1559,8 @@ int verity_metadata_load(const char *image, const char *root_hash_path, void **r } if (ret_verity_data) *ret_verity_data = TAKE_PTR(verity_filename); + if (roothashsig_filename) + *ret_roothashsig = TAKE_PTR(roothashsig_filename); return 1; } diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 6a53b94948..c7d078f4b7 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -87,8 +87,8 @@ int dissect_image_and_warn(int fd, const char *name, const void *root_hash, size DissectedImage* dissected_image_unref(DissectedImage *m); DEFINE_TRIVIAL_CLEANUP_FUNC(DissectedImage*, dissected_image_unref); -int dissected_image_decrypt(DissectedImage *m, const char *passphrase, const void *root_hash, size_t root_hash_size, const char *verity_data, DissectImageFlags flags, DecryptedImage **ret); -int dissected_image_decrypt_interactively(DissectedImage *m, const char *passphrase, const void *root_hash, size_t root_hash_size, const char *verity_data, DissectImageFlags flags, DecryptedImage **ret); +int dissected_image_decrypt(DissectedImage *m, const char *passphrase, const void *root_hash, size_t root_hash_size, const char *verity_data, const char *root_hash_sig_path, const void *root_hash_sig, size_t root_hash_sig_size, DissectImageFlags flags, DecryptedImage **ret); +int dissected_image_decrypt_interactively(DissectedImage *m, const char *passphrase, const void *root_hash, size_t root_hash_size, const char *verity_data, const char *root_hash_sig_path, const void *root_hash_sig, size_t root_hash_sig_size, DissectImageFlags flags, DecryptedImage **ret); int dissected_image_mount(DissectedImage *m, const char *dest, uid_t uid_shift, DissectImageFlags flags); int dissected_image_acquire_metadata(DissectedImage *m); @@ -100,6 +100,6 @@ int decrypted_image_relinquish(DecryptedImage *d); const char* partition_designator_to_string(int i) _const_; int partition_designator_from_string(const char *name) _pure_; -int verity_metadata_load(const char *image, const char *root_hash_path, void **ret_roothash, size_t *ret_roothash_size, char **ret_verity_data); +int verity_metadata_load(const char *image, const char *root_hash_path, void **ret_roothash, size_t *ret_roothash_size, char **ret_verity_data, char **ret_roothashsig); bool dissected_image_can_do_verity(const DissectedImage *image, unsigned partition_designator); bool dissected_image_has_verity(const DissectedImage *image, unsigned partition_designator);