From 39175477bd6e094542671599e0e258daa6351115 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 16 Oct 2025 14:59:04 +0100 Subject: [PATCH 1/6] mkosi: provide detached verity signatures too for minimal images Useful for manual testing in the VM --- mkosi/mkosi.postinst.chroot | 1 + 1 file changed, 1 insertion(+) diff --git a/mkosi/mkosi.postinst.chroot b/mkosi/mkosi.postinst.chroot index 32d2ad00f6..2251f00850 100755 --- a/mkosi/mkosi.postinst.chroot +++ b/mkosi/mkosi.postinst.chroot @@ -28,6 +28,7 @@ rm -f /etc/resolv.conf for f in "$BUILDROOT"/usr/share/*.verity.sig; do jq --join-output '.rootHash' "$f" >"${f%.verity.sig}.roothash" + jq --join-output '.signature' "$f" | base64 --decode >"${f%.verity.sig}.roothash.p7s" done # We want /var/log/journal to be created on first boot so it can be created with the right chattr settings by From 5e97d50e171fcaf5f50fd349a2be189b61593f31 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 16 Oct 2025 14:59:50 +0100 Subject: [PATCH 2/6] dissect: fix image policy check for bare dm-verity filesystem The root_hash_sig pointer might be set, but to an empty iovec. Check that the length is > 0 instead. Follow-up for cd22d8562dd085f5c234cf26b4dd773029418833 --- src/shared/dissect-image.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index de9475e6d3..be40c77260 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -881,7 +881,7 @@ static int dissect_image( encrypted = streq_ptr(fstype, "crypto_LUKS"); if (verity_settings_data_covers(verity, PARTITION_ROOT)) - found_flags = verity->root_hash_sig ? PARTITION_POLICY_SIGNED : PARTITION_POLICY_VERITY; + found_flags = verity->root_hash_sig_size > 0 ? PARTITION_POLICY_SIGNED : PARTITION_POLICY_VERITY; else found_flags = encrypted ? PARTITION_POLICY_ENCRYPTED : PARTITION_POLICY_UNPROTECTED; From 26bf1b9e85f7121258a24cf69302fe5727855f40 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 16 Oct 2025 16:21:57 +0100 Subject: [PATCH 3/6] json: add json_dispatch_unhex_iovec helper --- src/libsystemd/sd-json/json-util.c | 18 ++++++++++++++++++ src/libsystemd/sd-json/json-util.h | 1 + 2 files changed, 19 insertions(+) diff --git a/src/libsystemd/sd-json/json-util.c b/src/libsystemd/sd-json/json-util.c index b5d687c644..556d4c786b 100644 --- a/src/libsystemd/sd-json/json-util.c +++ b/src/libsystemd/sd-json/json-util.c @@ -25,6 +25,24 @@ #include "unit-name.h" #include "user-util.h" +int json_dispatch_unhex_iovec(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + _cleanup_free_ void *buffer = NULL; + struct iovec *iov = ASSERT_PTR(userdata); + size_t sz; + int r; + + if (!sd_json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + r = sd_json_variant_unhex(variant, &buffer, &sz); + if (r < 0) + return json_log(variant, flags, r, "JSON field '%s' is not valid hex data.", strna(name)); + + free_and_replace(iov->iov_base, buffer); + iov->iov_len = sz; + return 0; +} + int json_dispatch_unbase64_iovec(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { _cleanup_free_ void *buffer = NULL; struct iovec *iov = ASSERT_PTR(userdata); diff --git a/src/libsystemd/sd-json/json-util.h b/src/libsystemd/sd-json/json-util.h index abacb298c6..28d19a2db2 100644 --- a/src/libsystemd/sd-json/json-util.h +++ b/src/libsystemd/sd-json/json-util.h @@ -108,6 +108,7 @@ int json_log_internal(sd_json_variant *variant, int level, int error, const char #define json_log_oom(variant, flags) \ json_log(variant, flags, SYNTHETIC_ERRNO(ENOMEM), "Out of memory.") +int json_dispatch_unhex_iovec(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata); int json_dispatch_unbase64_iovec(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata); int json_dispatch_byte_array_iovec(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata); int json_dispatch_user_group_name(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata); From 674b4b4f9639d07c1408e4e22eb27092e3aee10e Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Tue, 14 Oct 2025 23:30:51 +0100 Subject: [PATCH 4/6] mountfsd: add support for verity-protected bare filesystems Add optional varlink parameters to pass in verity data/roothash/sig --- src/mountfsd/mountwork.c | 58 ++++++++++++++++--- .../varlink-io.systemd.MountFileSystem.c | 6 ++ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c index 0751791d53..141d8f62de 100644 --- a/src/mountfsd/mountwork.c +++ b/src/mountfsd/mountwork.c @@ -23,6 +23,7 @@ #include "hashmap.h" #include "image-policy.h" #include "io-util.h" +#include "iovec-util.h" #include "json-util.h" #include "loop-util.h" #include "main-func.h" @@ -92,6 +93,9 @@ typedef struct MountImageParameters { char *password; ImagePolicy *image_policy; bool verity_sharing; + struct iovec verity_root_hash; + struct iovec verity_root_hash_sig; + unsigned verity_data_fd_idx; } MountImageParameters; static void mount_image_parameters_done(MountImageParameters *p) { @@ -99,6 +103,8 @@ static void mount_image_parameters_done(MountImageParameters *p) { p->password = erase_and_free(p->password); p->image_policy = image_policy_free(p->image_policy); + iovec_done(&p->verity_root_hash); + iovec_done(&p->verity_root_hash_sig); } static int validate_image_fd(int fd, MountImageParameters *p) { @@ -286,13 +292,16 @@ static int vl_method_mount_image( void *userdata) { static const sd_json_dispatch_field dispatch_table[] = { - { "imageFileDescriptor", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint, offsetof(MountImageParameters, image_fd_idx), SD_JSON_MANDATORY }, - { "userNamespaceFileDescriptor", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint, offsetof(MountImageParameters, userns_fd_idx), 0 }, - { "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MountImageParameters, read_only), 0 }, - { "growFileSystems", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MountImageParameters, growfs), 0 }, - { "password", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MountImageParameters, password), 0 }, - { "imagePolicy", SD_JSON_VARIANT_STRING, json_dispatch_image_policy, offsetof(MountImageParameters, image_policy), 0 }, - { "veritySharing", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(MountImageParameters, verity_sharing), 0 }, + { "imageFileDescriptor", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint, offsetof(MountImageParameters, image_fd_idx), SD_JSON_MANDATORY }, + { "userNamespaceFileDescriptor", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint, offsetof(MountImageParameters, userns_fd_idx), 0 }, + { "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MountImageParameters, read_only), 0 }, + { "growFileSystems", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MountImageParameters, growfs), 0 }, + { "password", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MountImageParameters, password), 0 }, + { "imagePolicy", SD_JSON_VARIANT_STRING, json_dispatch_image_policy, offsetof(MountImageParameters, image_policy), 0 }, + { "veritySharing", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(MountImageParameters, verity_sharing), 0 }, + { "verityDataFileDescriptor", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint, offsetof(MountImageParameters, verity_data_fd_idx), 0 }, + { "verityRootHash", SD_JSON_VARIANT_STRING, json_dispatch_unhex_iovec, offsetof(MountImageParameters, verity_root_hash), 0 }, + { "verityRootHashSignature", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MountImageParameters, verity_root_hash_sig), 0 }, VARLINK_DISPATCH_POLKIT_FIELD, {} }; @@ -301,13 +310,14 @@ static int vl_method_mount_image( _cleanup_(mount_image_parameters_done) MountImageParameters p = { .image_fd_idx = UINT_MAX, .userns_fd_idx = UINT_MAX, + .verity_data_fd_idx = UINT_MAX, .read_only = -1, .growfs = -1, }; _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *aj = NULL; - _cleanup_close_ int image_fd = -EBADF, userns_fd = -EBADF; + _cleanup_close_ int image_fd = -EBADF, userns_fd = -EBADF, verity_data_fd = -EBADF; _cleanup_(image_policy_freep) ImagePolicy *use_policy = NULL; Hashmap **polkit_registry = ASSERT_PTR(userdata); _cleanup_free_ char *ps = NULL; @@ -323,6 +333,13 @@ static int vl_method_mount_image( if (r != 0) return r; + /* Verity data and roothash have to be either both set, or both unset. The sig can be set only if + * the roothash is set. */ + if ((p.verity_data_fd_idx != UINT_MAX) != (p.verity_root_hash.iov_len > 0)) + return sd_varlink_error_invalid_parameter_name(link, "verityDataFileDescriptor"); + if (p.verity_root_hash_sig.iov_len > 0 && p.verity_root_hash.iov_len == 0) + return sd_varlink_error_invalid_parameter_name(link, "verityRootHashSignature"); + if (p.image_fd_idx != UINT_MAX) { image_fd = sd_varlink_peek_dup_fd(link, p.image_fd_idx); if (image_fd < 0) @@ -348,6 +365,30 @@ static int vl_method_mount_image( return r; image_is_trusted = r; + if (p.verity_data_fd_idx != UINT_MAX) { + verity_data_fd = sd_varlink_peek_dup_fd(link, p.verity_data_fd_idx); + if (verity_data_fd < 0) + return log_debug_errno(verity_data_fd, "Failed to peek verity data fd from client: %m"); + + r = fd_verify_safe_flags(verity_data_fd); + if (r < 0) + return log_debug_errno(r, "Verity data file descriptor has unsafe flags set: %m"); + + verity.data_path = strdup(FORMAT_PROC_FD_PATH(verity_data_fd)); + if (!verity.data_path) + return -ENOMEM; + + verity.designator = PARTITION_ROOT; + + verity.root_hash = TAKE_PTR(p.verity_root_hash.iov_base); + verity.root_hash_size = p.verity_root_hash.iov_len; + p.verity_root_hash.iov_len = 0; + + verity.root_hash_sig = TAKE_PTR(p.verity_root_hash_sig.iov_base); + verity.root_hash_sig_size = p.verity_root_hash_sig.iov_len; + p.verity_root_hash_sig.iov_len = 0; + } + const char *polkit_details[] = { "read_only", one_zero(p.read_only > 0), NULL, @@ -408,6 +449,7 @@ static int vl_method_mount_image( DISSECT_IMAGE_ADD_PARTITION_DEVICES | DISSECT_IMAGE_PIN_PARTITION_DEVICES | (p.verity_sharing ? DISSECT_IMAGE_VERITY_SHARE : 0) | + (p.verity_data_fd_idx != UINT_MAX ? DISSECT_IMAGE_NO_PARTITION_TABLE : 0) | DISSECT_IMAGE_ALLOW_USERSPACE_VERITY; /* Let's see if we have acquired the privilege to mount untrusted images already */ diff --git a/src/shared/varlink-io.systemd.MountFileSystem.c b/src/shared/varlink-io.systemd.MountFileSystem.c index ce54cf3839..4d87040338 100644 --- a/src/shared/varlink-io.systemd.MountFileSystem.c +++ b/src/shared/varlink-io.systemd.MountFileSystem.c @@ -62,6 +62,12 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_DEFINE_INPUT(imagePolicy, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Whether to automatically reuse already set up dm-verity devices that share the same roothash."), SD_VARLINK_DEFINE_INPUT(veritySharing, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("File descriptor of the file containing the dm-verity data, if the image is a bare filesystem rather than a DDI."), + SD_VARLINK_DEFINE_INPUT(verityDataFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The expected dm-verity root hash as an hex encoded string, if the image is a bare filesystem rather than a DDI."), + SD_VARLINK_DEFINE_INPUT(verityRootHash, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The expected signature for the dm-verity root hash as a Base64 encoded string, if the image is a bare filesystem rather than a DDI."), + SD_VARLINK_DEFINE_INPUT(verityRootHashSignature, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), VARLINK_DEFINE_POLKIT_INPUT, SD_VARLINK_FIELD_COMMENT("An array with information about contained partitions that have been prepared for mounting, as well as their mount file descriptors."), SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(partitions, PartitionInfo, SD_VARLINK_ARRAY), From fad01f798d1308fa6bd81eac1b13b3d14d9a5380 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Tue, 14 Oct 2025 23:32:54 +0100 Subject: [PATCH 5/6] dissect: add support for verity-protected bare filesystems via mountfsd Needed to implement support for RootHashSignature=/RootVerity=/RootHash= and friends when going through mountfsd, for example with user units, so that system and user units provide the same features at the same level --- man/systemd.exec.xml | 6 +++--- src/core/namespace.c | 1 + src/dissect/dissect.c | 1 + src/nspawn/nspawn.c | 1 + src/shared/dissect-image.c | 18 +++++++++++++++++- src/shared/dissect-image.h | 2 +- test/units/TEST-50-DISSECT.mountfsd.sh | 5 +++++ 7 files changed, 29 insertions(+), 5 deletions(-) diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index 6ffe3b9989..f3bb1371be 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -275,7 +275,7 @@ image. There's currently no option to configure the root hash for the /usr/ file system via the unit file directly. - + @@ -298,7 +298,7 @@ configure the root hash signature for the /usr/ via the unit file directly. - + @@ -319,7 +319,7 @@ Discoverable Partitions Specification. - + diff --git a/src/core/namespace.c b/src/core/namespace.c index 00d760f7ff..6cf4f8b0a1 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -2604,6 +2604,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { p->root_image, userns_fd, p->root_image_policy, + p->verity, dissect_image_flags, &dissected_image); if (r < 0) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 7d921959cb..332471a409 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -2311,6 +2311,7 @@ static int run(int argc, char *argv[]) { arg_image, userns_fd, arg_image_policy, + &arg_verity_settings, arg_flags, &m); if (r < 0) diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index f157b53c29..69f134bb75 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -6349,6 +6349,7 @@ static int run(int argc, char *argv[]) { arg_image, userns_fd, arg_image_policy, + &arg_verity_settings, dissect_image_flags, &dissected_image); if (r < 0) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index be40c77260..b597ee263a 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -4513,6 +4513,7 @@ int verity_dissect_and_mount( src_fd >= 0 ? FORMAT_PROC_FD_PATH(src_fd) : src, userns_fd, image_policy, + verity, dissect_image_flags, &dissected_image); if (r < 0) @@ -4679,6 +4680,7 @@ int mountfsd_mount_image( const char *path, int userns_fd, const ImagePolicy *image_policy, + const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret) { @@ -4695,13 +4697,14 @@ int mountfsd_mount_image( }; _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; - _cleanup_close_ int image_fd = -EBADF; + _cleanup_close_ int image_fd = -EBADF, verity_data_fd = -EBADF; _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; _cleanup_free_ char *ps = NULL; const char *error_id; int r; assert(path); + assert(verity); assert(ret); r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); @@ -4736,6 +4739,16 @@ int mountfsd_mount_image( return log_error_errno(r, "Failed to format image policy to string: %m"); } + if (verity->data_path) { + verity_data_fd = open(verity->data_path, O_RDONLY|O_CLOEXEC); + if (verity_data_fd < 0) + return log_error_errno(errno, "Failed to open verity data file '%s': %m", verity->data_path); + + r = sd_varlink_push_dup_fd(vl, verity_data_fd); + if (r < 0) + return log_error_errno(r, "Failed to push verity data fd into varlink connection: %m"); + } + sd_json_variant *reply = NULL; r = varlink_callbo_and_log( vl, @@ -4748,6 +4761,9 @@ int mountfsd_mount_image( SD_JSON_BUILD_PAIR("growFileSystems", SD_JSON_BUILD_BOOLEAN(FLAGS_SET(flags, DISSECT_IMAGE_GROWFS))), SD_JSON_BUILD_PAIR_CONDITION(!!ps, "imagePolicy", SD_JSON_BUILD_STRING(ps)), SD_JSON_BUILD_PAIR("veritySharing", SD_JSON_BUILD_BOOLEAN(FLAGS_SET(flags, DISSECT_IMAGE_VERITY_SHARE))), + SD_JSON_BUILD_PAIR_CONDITION(verity_data_fd >= 0, "verityDataFileDescriptor", SD_JSON_BUILD_UNSIGNED(userns_fd >= 0 ? 2 : 1)), + JSON_BUILD_PAIR_IOVEC_HEX("verityRootHash", &((struct iovec) { .iov_base = verity->root_hash, .iov_len = verity->root_hash_size })), + JSON_BUILD_PAIR_IOVEC_BASE64("verityRootHashSignature", &((struct iovec) { .iov_base = verity->root_hash_sig, .iov_len = verity->root_hash_sig_size })), SD_JSON_BUILD_PAIR("allowInteractiveAuthentication", SD_JSON_BUILD_BOOLEAN(FLAGS_SET(flags, DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH)))); if (r < 0) return r; diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index a050f10701..98fc440a44 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -257,5 +257,5 @@ static inline const char* dissected_partition_fstype(const DissectedPartition *m int get_common_dissect_directory(char **ret); -int mountfsd_mount_image(const char *path, int userns_fd, const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret); +int mountfsd_mount_image(const char *path, int userns_fd, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); int mountfsd_mount_directory(const char *path, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); diff --git a/test/units/TEST-50-DISSECT.mountfsd.sh b/test/units/TEST-50-DISSECT.mountfsd.sh index b6ff5012bf..f4409f55bc 100755 --- a/test/units/TEST-50-DISSECT.mountfsd.sh +++ b/test/units/TEST-50-DISSECT.mountfsd.sh @@ -72,6 +72,11 @@ if [ "$VERITY_SIG_SUPPORTED" -eq 1 ]; then systemd-run -M testuser@ --user --pipe --wait \ --property RootImage="$MINIMAL_IMAGE.gpt" \ test -e "/dev/mapper/${MINIMAL_IMAGE_ROOTHASH}-verity" + + systemd-run -M testuser@ --user --pipe --wait \ + --property RootImage="$MINIMAL_IMAGE.raw" \ + --property ExtensionImages=/tmp/app0.raw \ + sh -c "test -e \"/dev/mapper/${MINIMAL_IMAGE_ROOTHASH}-verity\" && test -e \"/dev/mapper/$( Date: Thu, 16 Oct 2025 15:07:54 +0100 Subject: [PATCH 6/6] test: add coverage for image policy and bare filesystems with verity --- test/units/TEST-50-DISSECT.mountfsd.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/units/TEST-50-DISSECT.mountfsd.sh b/test/units/TEST-50-DISSECT.mountfsd.sh index f4409f55bc..cca502dfcb 100755 --- a/test/units/TEST-50-DISSECT.mountfsd.sh +++ b/test/units/TEST-50-DISSECT.mountfsd.sh @@ -77,6 +77,20 @@ if [ "$VERITY_SIG_SUPPORTED" -eq 1 ]; then --property RootImage="$MINIMAL_IMAGE.raw" \ --property ExtensionImages=/tmp/app0.raw \ sh -c "test -e \"/dev/mapper/${MINIMAL_IMAGE_ROOTHASH}-verity\" && test -e \"/dev/mapper/$(