mirror of
https://github.com/systemd/systemd.git
synced 2026-08-10 17:14:31 +00:00
repart,dissect: explicitly support DDIs that are both signed *and* encrypted (#43009)
This is a pretty relevant usecase: preparing an image on some trusted host, submitting it to some other host that authenticates it and decrypts it, and consumes it only then. Let's support this explicitly.
This commit is contained in:
@@ -709,6 +709,10 @@
|
||||
|
||||
<para>This option has no effect if the partition already exists.</para>
|
||||
|
||||
<para>This option may be combined with <varname>Verity=data</varname> in order to place the
|
||||
encrypted partition inside a dm-verity protection envelope, see the description of
|
||||
<varname>Verity=</varname> below.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v247"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
@@ -773,7 +777,19 @@
|
||||
|
||||
<para>This option has no effect if the partition already exists.</para>
|
||||
|
||||
<para>Usage of this option in combination with <varname>Encrypt=</varname> is not supported.</para>
|
||||
<para><literal>data</literal> may be combined with <varname>Encrypt=</varname>, 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 <citerefentry><refentrytitle>systemd-dissect</refentrytitle><manvolnum>1</manvolnum></citerefentry>
|
||||
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. <varname>Encrypt=</varname> may not be combined with
|
||||
<literal>Verity=hash</literal> or <literal>Verity=signature</literal>. Since the ciphertext is only
|
||||
generated while the final image is built, <varname>Minimize=</varname> cannot be used for the hash
|
||||
partition of an encrypted data partition; use <varname>SizeMaxBytes=</varname> on the data partition
|
||||
to size the hash partition instead.</para>
|
||||
|
||||
<para>For each unique <varname>VerityMatchKey=</varname> value, a single verity data partition
|
||||
(<literal>Verity=data</literal>) and a single verity hash partition (<literal>Verity=hash</literal>)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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" <<EOF
|
||||
[Partition]
|
||||
Type=root-${architecture}
|
||||
CopyFiles=${defs}
|
||||
Verity=data
|
||||
VerityMatchKey=root
|
||||
Encrypt=key-file
|
||||
SizeMaxBytes=1G
|
||||
EOF
|
||||
|
||||
tee "$defs/verity-hash.conf" <<EOF
|
||||
[Partition]
|
||||
Type=root-${architecture}-verity
|
||||
Verity=hash
|
||||
VerityMatchKey=root
|
||||
Encrypt=key-file
|
||||
EOF
|
||||
|
||||
(! systemd-repart --offline="$OFFLINE" \
|
||||
--definitions="$defs" \
|
||||
--seed="$seed" \
|
||||
--dry-run=yes \
|
||||
--empty=create \
|
||||
--size=auto \
|
||||
--key-file="$imgs/key" \
|
||||
"$imgs/refused")
|
||||
|
||||
# Minimize= on the hash partition of an encrypted data partition must be refused. Set Minimize= on the
|
||||
# data partition as well, so that the generic "data partition does not set CopyBlocks= or Minimize="
|
||||
# check doesn't fire first and the encryption-specific check is actually reached.
|
||||
tee "$defs/verity-data.conf" <<EOF
|
||||
[Partition]
|
||||
Type=root-${architecture}
|
||||
Format=ext4
|
||||
CopyFiles=${defs}
|
||||
Verity=data
|
||||
VerityMatchKey=root
|
||||
Encrypt=key-file
|
||||
Minimize=guess
|
||||
SizeMaxBytes=1G
|
||||
EOF
|
||||
|
||||
tee "$defs/verity-hash.conf" <<EOF
|
||||
[Partition]
|
||||
Type=root-${architecture}-verity
|
||||
Verity=hash
|
||||
VerityMatchKey=root
|
||||
Minimize=yes
|
||||
EOF
|
||||
|
||||
(! systemd-repart --offline="$OFFLINE" \
|
||||
--definitions="$defs" \
|
||||
--seed="$seed" \
|
||||
--dry-run=yes \
|
||||
--empty=create \
|
||||
--size=auto \
|
||||
--key-file="$imgs/key" \
|
||||
"$imgs/refused") |& grep "Minimize= cannot be set for verity hash partitions whose data partition is encrypted" >/dev/null
|
||||
|
||||
# Now a valid combination: the hash partition is sized via SizeMaxBytes= of the data partition
|
||||
tee "$defs/verity-data.conf" <<EOF
|
||||
[Partition]
|
||||
Type=root-${architecture}
|
||||
CopyFiles=${defs}
|
||||
Verity=data
|
||||
VerityMatchKey=root
|
||||
Encrypt=key-file
|
||||
SizeMaxBytes=1G
|
||||
EOF
|
||||
|
||||
tee "$defs/verity-hash.conf" <<EOF
|
||||
[Partition]
|
||||
Type=root-${architecture}-verity
|
||||
Verity=hash
|
||||
VerityMatchKey=root
|
||||
EOF
|
||||
|
||||
output=$(systemd-repart --offline="$OFFLINE" \
|
||||
--definitions="$defs" \
|
||||
--seed="$seed" \
|
||||
--dry-run=no \
|
||||
--empty=create \
|
||||
--size=auto \
|
||||
--key-file="$imgs/key" \
|
||||
--json=pretty \
|
||||
"$imgs/verity-encrypt")
|
||||
|
||||
drh=$(jq -r ".[] | select(.type == \"root-${architecture}\") | .roothash" <<<"$output")
|
||||
hrh=$(jq -r ".[] | select(.type == \"root-${architecture}-verity\") | .roothash" <<<"$output")
|
||||
|
||||
assert_neq "$drh" "null"
|
||||
assert_eq "$drh" "$hrh"
|
||||
|
||||
if systemd-detect-virt --quiet --container; then
|
||||
echo "Skipping verity+encrypt test dissect part in container."
|
||||
return
|
||||
fi
|
||||
|
||||
loop="$(systemd-dissect --attach "$imgs/verity-encrypt")"
|
||||
|
||||
# Make sure the loopback device gets cleaned up
|
||||
# shellcheck disable=SC2064
|
||||
trap "rm -rf '$defs' '$imgs' ; systemd-dissect --detach '$loop'" RETURN ERR
|
||||
|
||||
# The data partition must contain LUKS ciphertext as its outermost layer ...
|
||||
blkid --output value --match-tag TYPE "${loop}p1" | grep -x crypto_LUKS
|
||||
|
||||
# ... and the verity hash data must cover exactly that ciphertext
|
||||
veritysetup verify "${loop}p1" "${loop}p2" "$drh"
|
||||
|
||||
# Dissection must set up verity as the outer envelope and LUKS inside of it. The LUKS passphrase is
|
||||
# picked up via the dissect.passphrase credential.
|
||||
mkdir -p "$imgs/creds"
|
||||
echo -n "wetterfrosch" >"$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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user