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