diff --git a/TODO b/TODO index 3307ce1992..8a3861c3e0 100644 --- a/TODO +++ b/TODO @@ -117,6 +117,8 @@ Deprecations and removals: Features: +* Add support for extra verity configuration options to systemd-reart (FEC, hash type, etc) + * measure credentials picked up from SMBIOS to some suitable PCR * measure GPT and LUKS headers somewhere when we use them (i.e. in diff --git a/man/repart.d.xml b/man/repart.d.xml index 6a0a6b32b2..bdbf054fcf 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 ea8468dc29..889de712e2 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 no_dropin_files = true; + 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,11 +2269,13 @@ 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); - no_dropin_files = no_dropin_files && strv_isempty(p->drop_in_files); + has_roothash = has_roothash || !isempty(rh); + has_dropin_files = has_dropin_files || !strv_isempty(p->drop_in_files); } if ((arg_json_format_flags & JSON_FORMAT_OFF) && (sum_padding > 0 || sum_size > 0)) { @@ -2144,12 +2299,19 @@ 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 (no_dropin_files) { + 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) return log_error_errno(r, "Failed to set columns to display: %m"); @@ -2353,6 +2515,55 @@ static int context_dump_partition_bar(Context *context, const char *node) { return 0; } +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); + assert(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; + + /* 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); + if (r < 0) + return r; + + putc('\n', stdout); + } + + fflush(stdout); + + return 0; +} + + static bool context_changed(const Context *context) { assert(context); @@ -3143,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; @@ -3288,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) @@ -3512,21 +3847,6 @@ static int context_write_partition_table( assert(context); - if (arg_pretty > 0 || - (arg_pretty < 0 && isatty(STDOUT_FILENO) > 0) || - !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { - - (void) context_dump_partitions(context, node); - - if (arg_json_format_flags & JSON_FORMAT_OFF) { - putc('\n', stdout); - (void) context_dump_partition_bar(context, node); - putc('\n', stdout); - } - - fflush(stdout); - } - if (!from_scratch && !context_changed(context)) { log_info("No changes."); return 0; @@ -3573,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; @@ -4420,6 +4744,9 @@ static int parse_argv(int argc, char *argv[]) { if (arg_tpm2_pcr_mask == UINT32_MAX) arg_tpm2_pcr_mask = TPM2_PCR_MASK_DEFAULT; + if (arg_pretty < 0 && isatty(STDOUT_FILENO)) + arg_pretty = true; + return 1; } @@ -5022,10 +5349,14 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; + (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 4aff2788e4..063ce9425d 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 fddcda3872..fd972a38b9 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