From b5b7879a5dcc967421b1fe582848d5625fe5b072 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 6 Sep 2022 00:45:32 +0200 Subject: [PATCH] repart: Add support for formatting verity partitions This commit adds a new Verity= setting to repart definition files with two possible values: "data" and "hash". If Verity= is set to "data", repart works as before, and populates the partition with the content from CopyBlocks= or CopyFiles=. If Verity= is set to "hash", repart will try to find a matching data partition with Verity=data and equal values for CopyBlocks= or CopyFiles=, Format= and MakeDirectories=. If a matching data partition is found, repart will generate verity hashes for that data partition in the verity partition. The UUID of the data partition is set to the first 128 bits of the verity root hash. The UUID of the hashes partition is set to the final 128 bits of the verity root hash. Fixes #24559 --- man/repart.d.xml | 54 ++++++ src/partition/repart.c | 334 ++++++++++++++++++++++++++++++++++-- test/TEST-58-REPART/test.sh | 2 + test/units/testsuite-58.sh | 46 +++++ 4 files changed, 425 insertions(+), 11 deletions(-) diff --git a/man/repart.d.xml b/man/repart.d.xml index 6a0a6b32b2e..bdbf054fcf6 100644 --- a/man/repart.d.xml +++ b/man/repart.d.xml @@ -580,6 +580,38 @@ This option has no effect if the partition already exists. + + Verity= + + Takes one of off, data or + hash. Defaults to off. If set to off or + data, the partition is populated with content as specified by + CopyBlocks= or CopyFiles=. If set to hash, + the partition will be populated with verity hashes from a matching verity data partition. A matching + data partition is a partition with Verity= set to data and the + same verity match key (as configured with VerityMatchKey=). If not explicitly + configured, the data partition's UUID will be set to the first 128 bits of the verity root hash. + Similarly, if not configured, the hash partition's UUID will be set to the final 128 bits of the + verity root hash. The verity root hash itself will be included in the output of + systemd-repart. + + This option has no effect if the partition already exists. + + Usage of this option in combination with Encrypt= is not supported. + + For each unique VerityMatchKey= value, a single verity data partition + (Verity=data) and a single verity hash partition (Verity=hash) + must be defined. + + + + VerityMatchKey= + + Takes a short, user-chosen identifier string. This setting is used to find sibling + verity partitions for the current verity partition. See the description for + Verity=. + + FactoryReset= @@ -746,6 +778,28 @@ SizeMaxBytes=64M # ln -s 50-root.conf /usr/lib/repart.d/70-root-b.conf # ln -s 60-root-verity.conf /usr/lib/repart.d/80-root-verity-b.conf + + + + + Create a data and verity partition from a OS tree + + Assuming we have an OS tree at /var/tmp/os-tree that we want to package in a root partition + together with a matching verity partition, we can do so as follows: + + # 50-root.conf +[Partition] +Type=root +CopyFiles=/var/tmp/os-tree +Verity=data +VerityMatchKey=root + + + # 60-root-verity.conf +[Partition] +Type=root-verity +Verity=hash +VerityMatchKey=root diff --git a/src/partition/repart.c b/src/partition/repart.c index 29524c205ee..889de712e2c 100644 --- a/src/partition/repart.c +++ b/src/partition/repart.c @@ -132,6 +132,14 @@ typedef enum EncryptMode { _ENCRYPT_MODE_INVALID = -EINVAL, } EncryptMode; +typedef enum VerityMode { + VERITY_OFF, + VERITY_DATA, + VERITY_HASH, + _VERITY_MODE_MAX, + _VERITY_MODE_INVALID = -EINVAL, +} VerityMode; + struct Partition { char *definition_path; char **drop_in_files; @@ -170,12 +178,19 @@ struct Partition { char **copy_files; char **make_directories; EncryptMode encrypt; + VerityMode verity; + char *verity_match_key; uint64_t gpt_flags; int no_auto; int read_only; int growfs; + uint8_t *roothash; + size_t roothash_size; + + Partition *siblings[_VERITY_MODE_MAX]; + LIST_FIELDS(Partition, partitions); }; @@ -211,10 +226,18 @@ static const char *encrypt_mode_table[_ENCRYPT_MODE_MAX] = { [ENCRYPT_KEY_FILE_TPM2] = "key-file+tpm2", }; +static const char *verity_mode_table[_VERITY_MODE_MAX] = { + [VERITY_OFF] = "off", + [VERITY_DATA] = "data", + [VERITY_HASH] = "hash", +}; + #if HAVE_LIBCRYPTSETUP DEFINE_PRIVATE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(encrypt_mode, EncryptMode, ENCRYPT_KEY_FILE); +DEFINE_PRIVATE_STRING_TABLE_LOOKUP(verity_mode, VerityMode); #else DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN(encrypt_mode, EncryptMode, ENCRYPT_KEY_FILE); +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(verity_mode, VerityMode); #endif @@ -282,6 +305,9 @@ static Partition* partition_free(Partition *p) { free(p->format); strv_free(p->copy_files); strv_free(p->make_directories); + free(p->verity_match_key); + + free(p->roothash); return mfree(p); } @@ -407,6 +433,21 @@ static bool context_drop_one_priority(Context *context) { p->dropped = true; log_info("Can't fit partition %s of priority %" PRIi32 ", dropping.", p->definition_path, p->priority); + + /* We ensure that all verity sibling partitions have the same priority, so it's safe + * to drop all siblings here as well. */ + + for (VerityMode mode = VERITY_OFF + 1; mode < _VERITY_MODE_MAX; mode++) { + if (!p->siblings[mode]) + continue; + + if (p->siblings[mode]->dropped) + continue; + + p->siblings[mode]->dropped = true; + log_info("Also dropping sibling verity %s partition %s", + verity_mode_to_string(mode), p->siblings[mode]->definition_path); + } } return true; @@ -1352,6 +1393,8 @@ static int config_parse_uuid( return 0; } +static DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT(config_parse_verity, verity_mode, VerityMode, VERITY_OFF, "Invalid verity mode"); + static int partition_read_definition(Partition *p, const char *path, const char *const *conf_file_dirs) { ConfigTableItem table[] = { @@ -1371,6 +1414,8 @@ static int partition_read_definition(Partition *p, const char *path, const char { "Partition", "CopyFiles", config_parse_copy_files, 0, p }, { "Partition", "MakeDirectories", config_parse_make_dirs, 0, p }, { "Partition", "Encrypt", config_parse_encrypt, 0, &p->encrypt }, + { "Partition", "Verity", config_parse_verity, 0, &p->verity }, + { "Partition", "VerityMatchKey", config_parse_string, 0, &p->verity_match_key }, { "Partition", "Flags", config_parse_gpt_flags, 0, &p->gpt_flags }, { "Partition", "ReadOnly", config_parse_tristate, 0, &p->read_only }, { "Partition", "NoAuto", config_parse_tristate, 0, &p->no_auto }, @@ -1428,6 +1473,31 @@ static int partition_read_definition(Partition *p, const char *path, const char return log_oom(); } + if (p->verity != VERITY_OFF || p->encrypt != ENCRYPT_OFF) { + r = dlopen_cryptsetup(); + if (r < 0) + return log_syntax(NULL, LOG_ERR, path, 1, r, + "libcryptsetup not found, Verity=/Encrypt= are not supported: %m"); + } + + if (p->verity != VERITY_OFF && !p->verity_match_key) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "VerityMatchKey= must be set if Verity=%s", verity_mode_to_string(p->verity)); + + if (p->verity == VERITY_OFF && p->verity_match_key) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "VerityMatchKey= can only be set if Verity= is not \"%s\"", + verity_mode_to_string(p->verity)); + + if (p->verity == VERITY_HASH && (p->copy_files || p->copy_blocks_path || p->copy_blocks_auto || p->format || p->make_directories)) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "CopyBlocks=/CopyFiles=/Format=/MakeDirectories= cannot be used with Verity=%s", + verity_mode_to_string(p->verity)); + + if (p->verity != VERITY_OFF && p->encrypt != ENCRYPT_OFF) + return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), + "Encrypting verity hash/data partitions is not supported"); + /* Verity partitions are read only, let's imply the RO flag hence, unless explicitly configured otherwise. */ if ((gpt_partition_type_is_root_verity(p->type_uuid) || gpt_partition_type_is_usr_verity(p->type_uuid)) && @@ -1442,6 +1512,47 @@ static int partition_read_definition(Partition *p, const char *path, const char return 0; } +static int find_verity_sibling(Context *context, Partition *p, VerityMode mode, Partition **ret) { + Partition *s = NULL; + + assert(p); + assert(p->verity != VERITY_OFF); + assert(p->verity_match_key); + assert(mode != VERITY_OFF); + assert(p->verity != mode); + assert(ret); + + /* Try to find the matching sibling partition of the given type for a verity partition. For a data + * partition, this is the corresponding hash partiton with the same verity name (and vice versa for + * the hash partition). + */ + + LIST_FOREACH(partitions, q, context->partitions) { + if (p == q) + continue; + + if (q->verity != mode) + continue; + + assert(q->verity_match_key); + + if (!streq(p->verity_match_key, q->verity_match_key)) + continue; + + if (s) + return -ENOTUNIQ; + + s = q; + } + + if (!s) + return -ENXIO; + + *ret = s; + + return 0; +} + static int context_read_definitions( Context *context, char **directories, @@ -1480,6 +1591,42 @@ static int context_read_definitions( context->n_partitions++; } + /* Check that each configured verity hash/data partition has a matching verity data/hash partition. */ + + LIST_FOREACH(partitions, p, context->partitions) { + if (p->verity == VERITY_OFF) + continue; + + for (VerityMode mode = VERITY_OFF + 1; mode < _VERITY_MODE_MAX; mode++) { + Partition *q; + + if (p->verity == mode) + continue; + + if (p->siblings[mode]) + continue; + + r = find_verity_sibling(context, p, mode, &q); + if (r == -ENXIO) + return log_syntax(NULL, LOG_ERR, p->definition_path, 1, SYNTHETIC_ERRNO(EINVAL), + "Missing verity %s partition for verity %s partition with VerityMatchKey=%s", + verity_mode_to_string(mode), verity_mode_to_string(p->verity), p->verity_match_key); + if (r == -ENOTUNIQ) + return log_syntax(NULL, LOG_ERR, p->definition_path, 1, SYNTHETIC_ERRNO(EINVAL), + "Multiple verity %s partitions found for verity %s partition with VerityMatchKey=%s", + verity_mode_to_string(mode), verity_mode_to_string(p->verity), p->verity_match_key); + if (r < 0) + return r; + + if (q->priority != p->priority) + return log_syntax(NULL, LOG_ERR, p->definition_path, 1, SYNTHETIC_ERRNO(EINVAL), + "Priority mismatch (%i != %i) for verity sibling partitions with VerityMatchKey=%s", + p->priority, q->priority, p->verity_match_key); + + p->siblings[mode] = q; + } + } + return 0; } @@ -2042,26 +2189,26 @@ static int context_dump_partitions(Context *context, const char *node) { _cleanup_(table_unrefp) Table *t = NULL; uint64_t sum_padding = 0, sum_size = 0; int r; - const size_t dropin_files_col = 13; - bool has_dropin_files = false; + const size_t roothash_col = 13, dropin_files_col = 14; + bool has_roothash = false, has_dropin_files = false; if ((arg_json_format_flags & JSON_FORMAT_OFF) && context->n_partitions == 0) { log_info("Empty partition table."); return 0; } - t = table_new("type", "label", "uuid", "file", "node", "offset", "old size", "raw size", "size", "old padding", "raw padding", "padding", "activity", "drop-in files"); + t = table_new("type", "label", "uuid", "file", "node", "offset", "old size", "raw size", "size", "old padding", "raw padding", "padding", "activity", "roothash", "drop-in files"); if (!t) return log_oom(); if (!DEBUG_LOGGING) { if (arg_json_format_flags & JSON_FORMAT_OFF) (void) table_set_display(t, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3, (size_t) 4, - (size_t) 8, (size_t) 11, dropin_files_col); + (size_t) 8, (size_t) 11, roothash_col, dropin_files_col); else (void) table_set_display(t, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3, (size_t) 4, - (size_t) 5, (size_t) 6, (size_t) 7, (size_t) 9, (size_t) 10, (size_t) 12, - dropin_files_col); + (size_t) 5, (size_t) 6, (size_t) 7, (size_t) 9, (size_t) 10, + (size_t) 12, roothash_col, dropin_files_col); } (void) table_set_align_percent(t, table_get_cell(t, 0, 5), 100); @@ -2073,7 +2220,7 @@ static int context_dump_partitions(Context *context, const char *node) { (void) table_set_align_percent(t, table_get_cell(t, 0, 11), 100); LIST_FOREACH(partitions, p, context->partitions) { - _cleanup_free_ char *size_change = NULL, *padding_change = NULL, *partname = NULL; + _cleanup_free_ char *size_change = NULL, *padding_change = NULL, *partname = NULL, *rh = NULL; char uuid_buffer[SD_ID128_UUID_STRING_MAX]; const char *label, *activity = NULL; @@ -2101,6 +2248,12 @@ static int context_dump_partitions(Context *context, const char *node) { if (p->new_padding != UINT64_MAX) sum_padding += p->new_padding; + if (p->verity == VERITY_HASH) { + rh = p->roothash ? hexmem(p->roothash, p->roothash_size) : strdup("TBD"); + if (!rh) + return log_oom(); + } + r = table_add_many( t, TABLE_STRING, gpt_partition_type_uuid_to_string_harder(p->type_uuid, uuid_buffer), @@ -2116,10 +2269,12 @@ static int context_dump_partitions(Context *context, const char *node) { TABLE_UINT64, p->new_padding, TABLE_STRING, padding_change, TABLE_SET_COLOR, !p->partitions_next && sum_padding > 0 ? ansi_underline() : NULL, TABLE_STRING, activity ?: "unchanged", + TABLE_STRING, rh, TABLE_STRV, p->drop_in_files); if (r < 0) return table_log_add_error(r); + has_roothash = has_roothash || !isempty(rh); has_dropin_files = has_dropin_files || !strv_isempty(p->drop_in_files); } @@ -2144,11 +2299,18 @@ static int context_dump_partitions(Context *context, const char *node) { TABLE_EMPTY, TABLE_STRING, b, TABLE_EMPTY, + TABLE_EMPTY, TABLE_EMPTY); if (r < 0) return table_log_add_error(r); } + if (!has_roothash) { + r = table_hide_column_from_display(t, roothash_col); + if (r < 0) + return log_error_errno(r, "Failed to set columns to display: %m"); + } + if (!has_dropin_files) { r = table_hide_column_from_display(t, dropin_files_col); if (r < 0) @@ -2353,7 +2515,15 @@ static int context_dump_partition_bar(Context *context, const char *node) { return 0; } -static int context_dump(Context *context, const char *node) { +static bool context_has_roothash(Context *context) { + LIST_FOREACH(partitions, p, context->partitions) + if (p->roothash) + return true; + + return false; +} + +static int context_dump(Context *context, const char *node, bool late) { int r; assert(context); @@ -2362,11 +2532,23 @@ static int context_dump(Context *context, const char *node) { if (arg_pretty == 0 && FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) return 0; + /* If we're outputting JSON, only dump after doing all operations so we can include the roothashes + * in the output. */ + if (!late && !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) + return 0; + + /* If we're not outputting JSON, only dump again after doing all operations if there are any + * roothashes that we need to communicate to the user. */ + if (late && FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF) && !context_has_roothash(context)) + return 0; + r = context_dump_partitions(context, node); if (r < 0) return r; - if (arg_json_format_flags & JSON_FORMAT_OFF) { + /* Make sure we only write the partition bar once, even if we're writing the partition table twice to + * communicate roothashes. */ + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF) && !late) { putc('\n', stdout); r = context_dump_partition_bar(context, node); @@ -3172,6 +3354,130 @@ static int context_mkfs(Context *context) { return 0; } +static int do_verity_format( + LoopDevice *data_device, + LoopDevice *hash_device, + uint64_t sector_size, + uint8_t **ret_roothash, + size_t *ret_roothash_size) { + +#if HAVE_LIBCRYPTSETUP + _cleanup_(sym_crypt_freep) struct crypt_device *cd = NULL; + _cleanup_free_ uint8_t *rh = NULL; + size_t rhs; + int r; + + assert(data_device); + assert(hash_device); + assert(sector_size > 0); + assert(ret_roothash); + assert(ret_roothash_size); + + r = dlopen_cryptsetup(); + if (r < 0) + return log_error_errno(r, "libcryptsetup not found, cannot setup verity: %m"); + + r = sym_crypt_init(&cd, hash_device->node); + if (r < 0) + return log_error_errno(r, "Failed to allocate libcryptsetup context: %m"); + + r = sym_crypt_format( + cd, CRYPT_VERITY, NULL, NULL, NULL, NULL, 0, + &(struct crypt_params_verity){ + .data_device = data_device->node, + .flags = CRYPT_VERITY_CREATE_HASH, + .hash_name = "sha256", + .hash_type = 1, + .data_block_size = sector_size, + .hash_block_size = sector_size, + .salt_size = 32, + }); + if (r < 0) + return log_error_errno(r, "Failed to setup verity hash data: %m"); + + r = sym_crypt_get_volume_key_size(cd); + if (r < 0) + return log_error_errno(r, "Failed to determine verity root hash size: %m"); + rhs = (size_t) r; + + rh = malloc(rhs); + if (!rh) + return log_oom(); + + r = sym_crypt_volume_key_get(cd, CRYPT_ANY_SLOT, (char *) rh, &rhs, NULL, 0); + if (r < 0) + return log_error_errno(r, "Failed to get verity root hash: %m"); + + *ret_roothash = TAKE_PTR(rh); + *ret_roothash_size = rhs; + + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "libcryptsetup is not supported, cannot setup verity: %m"); +#endif +} + +static int context_verity(Context *context) { + int fd = -1, r; + + assert(context); + + LIST_FOREACH(partitions, p, context->partitions) { + Partition *dp; + _cleanup_(loop_device_unrefp) LoopDevice *hash_device = NULL, *data_device = NULL; + _cleanup_free_ uint8_t *rh = NULL; + size_t rhs = 0; /* Initialize to work around for GCC false positive. */ + + if (p->dropped) + continue; + + if (PARTITION_EXISTS(p)) /* Never format existing partitions */ + continue; + + if (p->verity != VERITY_HASH) + continue; + + assert_se(dp = p->siblings[VERITY_DATA]); + assert(!dp->dropped); + + if (fd < 0) + assert_se((fd = fdisk_get_devfd(context->fdisk_context)) >= 0); + + r = loop_device_make(fd, O_RDONLY, dp->offset, dp->new_size, 0, LOCK_EX, &data_device); + if (r < 0) + return log_error_errno(r, + "Failed to make loopback device of verity data partition %" PRIu64 ": %m", + p->partno); + + r = loop_device_make(fd, O_RDWR, p->offset, p->new_size, 0, LOCK_EX, &hash_device); + if (r < 0) + return log_error_errno(r, + "Failed to make loopback device of verity hash partition %" PRIu64 ": %m", + p->partno); + + r = do_verity_format(data_device, hash_device, context->sector_size, &rh, &rhs); + if (r < 0) + return r; + + assert(rhs >= sizeof(sd_id128_t) * 2); + + if (!dp->new_uuid_is_set) { + memcpy_safe(dp->new_uuid.bytes, rh, sizeof(sd_id128_t)); + dp->new_uuid_is_set = true; + } + + if (!p->new_uuid_is_set) { + memcpy_safe(p->new_uuid.bytes, rh + rhs - sizeof(sd_id128_t), sizeof(sd_id128_t)); + p->new_uuid_is_set = true; + } + + p->roothash = TAKE_PTR(rh); + p->roothash_size = rhs; + } + + return 0; +} + static int partition_acquire_uuid(Context *context, Partition *p, sd_id128_t *ret) { struct { sd_id128_t type_uuid; @@ -3317,7 +3623,7 @@ static int context_acquire_partition_uuids_and_labels(Context *context) { if (!sd_id128_is_null(p->current_uuid)) p->new_uuid = p->current_uuid; /* Never change initialized UUIDs */ - else if (!p->new_uuid_is_set) { + else if (!p->new_uuid_is_set && p->verity == VERITY_OFF) { /* Not explicitly set by user! */ r = partition_acquire_uuid(context, p, &p->new_uuid); if (r < 0) @@ -3587,6 +3893,10 @@ static int context_write_partition_table( if (r < 0) return r; + r = context_verity(context); + if (r < 0) + return r; + r = context_mangle_partitions(context); if (r < 0) return r; @@ -5039,12 +5349,14 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; - context_dump(context, node); + (void) context_dump(context, node, /*late=*/ false); r = context_write_partition_table(context, node, from_scratch); if (r < 0) return r; + (void) context_dump(context, node, /*late=*/ true); + return 0; } diff --git a/test/TEST-58-REPART/test.sh b/test/TEST-58-REPART/test.sh index 4aff2788e43..063ce9425dc 100755 --- a/test/TEST-58-REPART/test.sh +++ b/test/TEST-58-REPART/test.sh @@ -10,6 +10,8 @@ TEST_DESCRIPTION="test systemd-repart" test_append_files() { if ! get_bool "${TEST_NO_QEMU:=}"; then install_dmevent + image_install jq + instmods dm_verity =md generate_module_dependencies fi } diff --git a/test/units/testsuite-58.sh b/test/units/testsuite-58.sh index fddcda3872a..fd972a38b96 100755 --- a/test/units/testsuite-58.sh +++ b/test/units/testsuite-58.sh @@ -598,6 +598,51 @@ EOF assert_in "$imgs/zero1 : start= 2048, size= 20480, type=${root_guid}, uuid=00000000-0000-0000-0000-000000000000" "$output" } +test_verity() { + local defs imgs output + + if systemd-detect-virt --quiet --container; then + echo "Skipping verity test in container." + return + fi + + defs="$(mktemp --directory "/tmp/test-repart.XXXXXXXXXX")" + imgs="$(mktemp --directory "/var/tmp/test-repart.XXXXXXXXXX")" + # shellcheck disable=SC2064 + trap "rm -rf '$defs' '$imgs'" RETURN + + cat >"$defs/root.conf" <"$defs/verity.conf" <= 512 and <= PAGE_SIZE, and # must be powers of 2. Which leaves exactly four different ones to test on -- 2.47.3