diff --git a/man/repart.d.xml b/man/repart.d.xml index 8de04357354..eaa83f61713 100644 --- a/man/repart.d.xml +++ b/man/repart.d.xml @@ -709,6 +709,10 @@ This option has no effect if the partition already exists. + This option may be combined with Verity=data in order to place the + encrypted partition inside a dm-verity protection envelope, see the description of + Verity= below. + @@ -773,7 +777,19 @@ This option has no effect if the partition already exists. - Usage of this option in combination with Encrypt= is not supported. + data may be combined with Encrypt=, in which case the + partition is first encrypted with LUKS2 and the verity hash data is then generated from the + resulting ciphertext, i.e. Verity is the outer protection envelope and LUKS2 the inner. Tools such + as systemd-dissect1 + will then set up the dm-verity device first, and open the LUKS2 volume on top of it, so that all + encrypted data read is also authenticated via the verity root hash. Note that in this case the + verity hashes necessarily cover freshly generated ciphertext, hence the root hash (and thus the + default partition UUIDs derived from it) will change on every build, even if the plaintext contents + are fully reproducible. Encrypt= may not be combined with + Verity=hash or Verity=signature. Since the ciphertext is only + generated while the final image is built, Minimize= cannot be used for the hash + partition of an encrypted data partition; use SizeMaxBytes= on the data partition + to size the hash partition instead. For each unique VerityMatchKey= value, a single verity data partition (Verity=data) and a single verity hash partition (Verity=hash) diff --git a/src/repart/repart.c b/src/repart/repart.c index 002fb782be5..bd8df612633 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -3138,9 +3138,9 @@ static int partition_read_definition( verity_mode_to_string(p->verity)); } - if (p->verity != VERITY_OFF && p->encrypt != ENCRYPT_OFF) + if (IN_SET(p->verity, VERITY_HASH, VERITY_SIG) && p->encrypt != ENCRYPT_OFF) return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), - "Encrypting verity hash/data partitions is not supported."); + "Encrypting verity hash/signature partitions is not supported."); if (p->verity == VERITY_SIG && (p->size_min != UINT64_MAX || p->size_max != UINT64_MAX)) return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), @@ -3667,6 +3667,13 @@ static int context_read_definitions(Context *context) { if (dp->minimize == MINIMIZE_OFF && !(dp->copy_blocks_path || dp->copy_blocks_auto)) return log_syntax(NULL, LOG_ERR, p->definition_path, 1, SYNTHETIC_ERRNO(EINVAL), "Minimize= set for verity hash partition but data partition does not set CopyBlocks= or Minimize=."); + + /* The verity hash of an encrypted data partition covers the ciphertext, which is generated + * with a fresh volume key only when the final image is built, hence it cannot be + * precalculated for minimizing purposes. */ + if (dp->encrypt != ENCRYPT_OFF) + return log_syntax(NULL, LOG_ERR, p->definition_path, 1, SYNTHETIC_ERRNO(EINVAL), + "Minimize= cannot be set for verity hash partitions whose data partition is encrypted, use SizeMaxBytes= on the data partition instead."); } LIST_FOREACH(partitions, p, context->partitions) { @@ -5323,6 +5330,15 @@ static int partition_target_prepare( return 0; } +static void partition_target_drop_decrypted(PartitionTarget *t) { + assert(t); + + /* Deactivate the dm-crypt device again, so that subsequent access to the target reaches the + * encrypted data as it is stored on disk (i.e. the ciphertext). Requires the target to have been + * sync'ed first. */ + t->decrypted = decrypted_partition_target_free(t->decrypted); +} + static int partition_target_grow(PartitionTarget *t, uint64_t size) { int r; @@ -5990,6 +6006,9 @@ static int partition_format_verity_hash( if (r < 0) return r; + if (p->partno != UINT64_MAX) + log_info("Calculating Verity protection data for future partition %" PRIu64 "...", p->partno); + if (!node) { r = partition_target_prepare(context, p, p->new_size, /* need_path= */ true, &t); if (r < 0) @@ -6411,6 +6430,11 @@ static int context_copy_blocks(Context *context) { p->partno, FORMAT_TIMESPAN(time_spent, 0)); if (p->siblings[VERITY_HASH] && !partition_defer(context, p->siblings[VERITY_HASH])) { + /* The verity hash must cover the partition contents as stored on disk, i.e. the + * ciphertext if the partition is encrypted, hence tear down the dm-crypt device + * first. */ + partition_target_drop_decrypted(t); + r = partition_format_verity_hash(context, p->siblings[VERITY_HASH], /* node= */ NULL, partition_target_path(t)); if (r < 0) @@ -7600,6 +7624,11 @@ static int context_mkfs(Context *context) { return r; if (p->siblings[VERITY_HASH] && !partition_defer(context, p->siblings[VERITY_HASH])) { + /* The verity hash must cover the partition contents as stored on disk, i.e. the + * ciphertext if the partition is encrypted, hence tear down the dm-crypt device + * first. */ + partition_target_drop_decrypted(t); + r = partition_format_verity_hash(context, p->siblings[VERITY_HASH], /* node= */ NULL, partition_target_path(t)); if (r < 0) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 1bfb2492922..8835bca354c 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -2868,8 +2868,9 @@ static DecryptedImage* decrypted_image_free(DecryptedImage *d) { if (!d) return NULL; - for (size_t i = 0; i < d->n_decrypted; i++) { - DecryptedPartition *p = d->decrypted + i; + /* Detach in reverse order so that nested dm-verity and dm-crypt are detached in the right order. */ + for (size_t i = d->n_decrypted; i > 0; i--) { + DecryptedPartition *p = d->decrypted + i - 1; if (p->device && p->name && !p->relinquished) { _cleanup_free_ char *node = NULL; @@ -3005,6 +3006,13 @@ static int decrypt_partition( if (!FLAGS_SET(policy_flags, PARTITION_POLICY_ENCRYPTED)) return log_debug_errno(SYNTHETIC_ERRNO(ERFKILL), "Attempted to unlock partition via LUKS, but it's prohibited."); + /* If a dm-verity device was already set up on this partition, then the partition is both Verity + * protected and LUKS encrypted, with Verity being the outer envelope and LUKS the inner. In that + * case open the LUKS device on top of the verity device, so that everything we read is + * authenticated. Since verity devices are read-only, the LUKS layer must then be read-only, too. */ + const char *source_node = m->decrypted_node ?: m->node; + bool on_verity = !!m->decrypted_node; + r = dlopen_cryptsetup(LOG_DEBUG); if (r < 0) return r; @@ -3016,7 +3024,7 @@ static int decrypt_partition( if (!GREEDY_REALLOC0(d->decrypted, d->n_decrypted + 1)) return -ENOMEM; - r = sym_crypt_init(&cd, m->node); + r = sym_crypt_init(&cd, source_node); if (r < 0) return log_debug_errno(r, "Failed to initialize dm-crypt: %m"); @@ -3027,8 +3035,8 @@ static int decrypt_partition( return log_debug_errno(r, "Failed to load LUKS metadata: %m"); r = sym_crypt_activate_by_passphrase(cd, name, CRYPT_ANY_SLOT, passphrase, strlen(passphrase), - ((flags & DISSECT_IMAGE_DEVICE_READ_ONLY) ? CRYPT_ACTIVATE_READONLY : 0) | - ((flags & DISSECT_IMAGE_DISCARD_ON_CRYPTO) ? CRYPT_ACTIVATE_ALLOW_DISCARDS : 0)); + ((flags & DISSECT_IMAGE_DEVICE_READ_ONLY) || on_verity ? CRYPT_ACTIVATE_READONLY : 0) | + ((flags & DISSECT_IMAGE_DISCARD_ON_CRYPTO) && !on_verity ? CRYPT_ACTIVATE_ALLOW_DISCARDS : 0)); if (r < 0) { log_debug_errno(r, "Failed to activate LUKS device: %m"); return r == -EPERM ? -EKEYREJECTED : r; @@ -3043,7 +3051,7 @@ static int decrypt_partition( .device = TAKE_PTR(cd), }; - m->decrypted_node = TAKE_PTR(node); + free_and_replace(m->decrypted_node, node); close_and_replace(m->mount_node_fd, fd); return 0; @@ -3542,7 +3550,7 @@ success: .device = TAKE_PTR(cd), }; - m->decrypted_node = TAKE_PTR(node); + free_and_replace(m->decrypted_node, node); close_and_replace(m->mount_node_fd, mount_node_fd); return 0; @@ -3602,10 +3610,10 @@ int dissected_image_decrypt( PartitionPolicyFlags fl = image_policy_get_exhaustively(policy, i); - r = decrypt_partition(m, p, passphrase, flags, fl, d); - if (r < 0) - return r; - + /* Set up Verity first, and only then LUKS: if a partition is both Verity protected and + * encrypted, Verity is the outer envelope and LUKS the inner, i.e. the Verity hash data + * covers the LUKS ciphertext, and decrypt_partition() then stacks the LUKS device on top of + * the verity device. */ k = partition_verity_hash_of(i); if (k >= 0) { r = verity_partition(m, i, p, m->partitions + k, root, verity, flags, fl, d); @@ -3613,6 +3621,10 @@ int dissected_image_decrypt( return r; } + r = decrypt_partition(m, p, passphrase, flags, fl, d); + if (r < 0) + return r; + if (!p->decrypted_fstype && p->mount_node_fd >= 0 && p->decrypted_node) { r = probe_filesystem_full(p->mount_node_fd, p->decrypted_node, 0, UINT64_MAX, /* bool restrict_fstypes= */ true, &p->decrypted_fstype); if (r < 0 && r != -EUCLEAN) diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh index 551e72c53c2..d5fe4897eac 100755 --- a/test/units/TEST-58-REPART.sh +++ b/test/units/TEST-58-REPART.sh @@ -1556,6 +1556,168 @@ EOF veritysetup dump "${loop}p2" | grep 'Data blocks:' | grep "$data_verity_blocks" >/dev/null } +testcase_verity_encrypt() { + local defs imgs output loop drh hrh part_size dm_devno verity_dep + + if ( . /etc/os-release && [[ "$ID" == "postmarketos" ]] ); then + echo "Skipping verity+encrypt test on postmarketOS." + return + fi + + defs="$(mktemp --directory "/tmp/test-repart.defs.XXXXXXXXXX")" + imgs="$(mktemp --directory "/var/tmp/test-repart.imgs.XXXXXXXXXX")" + # shellcheck disable=SC2064 + trap "rm -rf '$defs' '$imgs'" RETURN + chmod 0755 "$defs" + + echo "*** dm-verity + LUKS2 (verity envelope around encrypted data) ***" + + echo -n "wetterfrosch" >"$imgs/key" + + # Encrypting verity hash partitions must be refused + tee "$defs/verity-data.conf" </dev/null + + # Now a valid combination: the hash partition is sized via SizeMaxBytes= of the data partition + tee "$defs/verity-data.conf" <"$imgs/creds/dissect.passphrase" + + systemd-dissect --root-hash "$drh" "$imgs/verity-encrypt" + systemd-dissect --root-hash "$drh" --validate --image-policy "root=encrypted+verity" "$imgs/verity-encrypt" + # A policy that doesn't allow encryption must be refused + (! systemd-dissect --root-hash "$drh" --validate --image-policy "root=verity" "$imgs/verity-encrypt") + + CREDENTIALS_DIRECTORY="$imgs/creds" systemd-dissect --root-hash "$drh" -M "$imgs/verity-encrypt" "$imgs/mnt" + + # shellcheck disable=SC2064 + trap "umount --quiet --recursive '$imgs/mnt' || : ; rm -rf '$defs' '$imgs' ; systemd-dissect --detach '$loop'" RETURN ERR + + # Check that both DM layers are stacked as expected: the mounted device is a LUKS volume backed by a + # dm-verity device + dm_devno=$(findmnt --noheadings --output MAJ:MIN "$imgs/mnt" | tr -d ' ') + [[ "$(dmsetup table -j "${dm_devno%%:*}" -m "${dm_devno##*:}" | cut -d' ' -f3)" == "crypt" ]] + verity_dep=$(dmsetup deps -o devname -j "${dm_devno%%:*}" -m "${dm_devno##*:}" | sed 's/.*(\(.*\))/\1/') + [[ "$(dmsetup table "/dev/mapper/$verity_dep" | cut -d' ' -f3)" == "verity" ]] + + # The copied-in files must be intact + cmp "$defs/verity-data.conf" "$imgs/mnt$defs/verity-data.conf" + + systemd-dissect -U "$imgs/mnt" + + # Now corrupt a block in the middle of the encrypted data partition and check that verification fails + part_size=$(blockdev --getsize64 "${loop}p1") + dd if=/dev/urandom of="${loop}p1" bs=4096 count=1 seek=$(( part_size / 2 / 4096 )) oflag=direct conv=notrunc + (! veritysetup verify "${loop}p1" "${loop}p2" "$drh") +} + testcase_exclude_files() { local defs imgs root output