diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md
index d9453b5d3ce..5f6ff5d7316 100644
--- a/docs/ENVIRONMENT.md
+++ b/docs/ENVIRONMENT.md
@@ -146,6 +146,14 @@ All tools:
RootImage=/ExtensionImages=/MountImages= in unit files default to enabled,
while other uses default to disabled for this.
+* `$SYSTEMD_DISSECT_TPM2_UNLOCK=` — takes a boolean. If false, turns off
+ unlocking of LUKS encrypted partitions in disk images via a TPM2 token
+ enrolled in the LUKS2 header (e.g. via `systemd-cryptenroll --tpm2-device=…`)
+ for the tools that do this by default if no passphrase is supplied, i.e.
+ `systemd-dissect`, `systemd-nspawn` and `systemd-sysext`/`systemd-confext`.
+ It cannot turn this on for other uses (including `systemd-mountfsd`),
+ which act on behalf of unprivileged clients.
+
`systemctl`:
* `$SYSTEMCTL_FORCE_BUS=1` — if set, do not connect to PID 1's private D-Bus
diff --git a/man/systemd-dissect.xml b/man/systemd-dissect.xml
index 670fb2a5741..b5fc5293c1b 100644
--- a/man/systemd-dissect.xml
+++ b/man/systemd-dissect.xml
@@ -152,7 +152,14 @@
When the OS image contains LUKS encrypted or Verity integrity protected file systems
appropriate volumes are automatically set up and marked for automatic disassembly when the image is
- unmounted.
+ unmounted. LUKS volumes carrying a TPM2 token (as enrolled via the
+ option of
+ systemd-cryptenroll1)
+ are unlocked via the local TPM2 automatically, before a passphrase is requested. This may be turned
+ off via the $SYSTEMD_DISSECT_TPM2_UNLOCK environment variable. Note that this
+ requires privileges: when invoked unprivileged, images are mounted via
+ systemd-mountfsd.service8,
+ which does not currently support unlocking via TPM2.
The OS image may either be specified as path to an OS image stored in a regular file or may
refer to block device node (in the latter case, the block device must be the "whole" device, i.e. not
diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml
index 58d4c84b36c..44983b72108 100644
--- a/man/systemd-nspawn.xml
+++ b/man/systemd-nspawn.xml
@@ -341,7 +341,12 @@
/efi (or /boot as fallback) in case a directory by this name exists
and is empty.
- Partitions encrypted with LUKS are automatically decrypted. Also, on GPT images dm-verity data integrity
+ Partitions encrypted with LUKS are automatically decrypted, via a TPM2 token enrolled in the LUKS2
+ header if one matches the local TPM2 (this may be turned off via the
+ $SYSTEMD_DISSECT_TPM2_UNLOCK environment variable), or otherwise by prompting for a
+ passphrase. Unlocking via TPM2 requires privileges: when invoked unprivileged, images are mounted via
+ systemd-mountfsd.service8,
+ which does not currently support it. Also, on GPT images dm-verity data integrity
hash partitions are set up if the root hash for them is specified using the
option.
diff --git a/man/systemd-sysext.xml b/man/systemd-sysext.xml
index e9d275fac49..984441add5b 100644
--- a/man/systemd-sysext.xml
+++ b/man/systemd-sysext.xml
@@ -101,7 +101,11 @@
systemd-nspawn1
supports via its / switches and those that the
service manager supports via /. Similar to
- them they may optionally carry Verity authentication information.
+ them they may optionally carry Verity authentication information. LUKS encrypted extension images
+ carrying a TPM2 token (as enrolled via the option of
+ systemd-cryptenroll1)
+ are unlocked via the local TPM2 automatically, without any passphrase. This may be turned off via the
+ $SYSTEMD_DISSECT_TPM2_UNLOCK environment variable.
System extensions are searched for in the directories
/etc/extensions/, /run/extensions/ and
diff --git a/man/systemd-tpm2-generator.xml b/man/systemd-tpm2-generator.xml
index b45cf29be86..57aee889b94 100644
--- a/man/systemd-tpm2-generator.xml
+++ b/man/systemd-tpm2-generator.xml
@@ -53,6 +53,16 @@
systemd-tpm2-swtpm.service8
service.
+ In the initrd, the generator also orders
+ systemd-sysext-initrd.service8
+ and
+ systemd-confext-initrd.service8
+ after tpm2.target and
+ systemd-pcrphase-initrd.service8,
+ so that LUKS encrypted extension images sealed to the TPM2 can be unlocked. This is not done if the
+ software TPM fallback is in use, as the software TPM only becomes available after the extension images
+ are merged, hence such images cannot be unlocked in the initrd in that case.
+
systemd-tpm2-generator implements
systemd.generator7.
diff --git a/src/bootctl/bootctl.c b/src/bootctl/bootctl.c
index b345c6a6b8c..465f709e1a6 100644
--- a/src/bootctl/bootctl.c
+++ b/src/bootctl/bootctl.c
@@ -801,9 +801,7 @@ static int run(int argc, char *argv[]) {
LIBCRYPTO_NOTE(recommended);
LIBCRYPTSETUP_NOTE(suggested);
LIBMOUNT_NOTE(recommended);
- LIBTSS2_ESYS_NOTE(suggested);
- LIBTSS2_MU_NOTE(suggested);
- LIBTSS2_RC_NOTE(suggested);
+ TPM2_NOTE(suggested);
log_setup();
diff --git a/src/coredump/coredumpctl.c b/src/coredump/coredumpctl.c
index 14f2b33b9cc..06275b9faaf 100644
--- a/src/coredump/coredumpctl.c
+++ b/src/coredump/coredumpctl.c
@@ -295,6 +295,7 @@ static int run(int argc, char *argv[]) {
LIBCRYPTO_NOTE(suggested);
LIBCRYPTSETUP_NOTE(suggested);
LIBMOUNT_NOTE(recommended);
+ TPM2_NOTE(suggested);
setlocale(LC_ALL, "");
log_setup();
diff --git a/src/cryptenroll/cryptenroll-tpm2.c b/src/cryptenroll/cryptenroll-tpm2.c
index 21b77bd9c04..c5613cd7d10 100644
--- a/src/cryptenroll/cryptenroll-tpm2.c
+++ b/src/cryptenroll/cryptenroll-tpm2.c
@@ -206,6 +206,7 @@ int load_volume_key_tpm2(
cd,
UINT32_MAX,
token,
+ LOG_ERR,
&hash_pcr_mask,
&pcr_bank,
&pubkey,
diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c
index fb616a3426c..893a516f2c0 100644
--- a/src/cryptsetup/cryptsetup.c
+++ b/src/cryptsetup/cryptsetup.c
@@ -2041,6 +2041,7 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2(
cd,
arg_tpm2_pcr_mask, /* if != UINT32_MAX we'll only look for tokens with this PCR mask */
token, /* search for the token with this index, or any later index than this */
+ LOG_ERR,
&hash_pcr_mask,
&pcr_bank,
&pubkey,
diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c
index eb113f4d004..a1266eeebfb 100644
--- a/src/dissect/dissect.c
+++ b/src/dissect/dissect.c
@@ -92,7 +92,8 @@ static DissectImageFlags arg_flags =
DISSECT_IMAGE_GROWFS |
DISSECT_IMAGE_PIN_PARTITION_DEVICES |
DISSECT_IMAGE_ADD_PARTITION_DEVICES |
- DISSECT_IMAGE_ALLOW_USERSPACE_VERITY;
+ DISSECT_IMAGE_ALLOW_USERSPACE_VERITY |
+ DISSECT_IMAGE_TPM2_UNLOCK;
static VeritySettings arg_verity_settings = VERITY_SETTINGS_DEFAULT;
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
static PagerFlags arg_pager_flags = 0;
@@ -1970,6 +1971,7 @@ static int run(int argc, char *argv[]) {
LIBCRYPTSETUP_NOTE(suggested);
LIBMOUNT_NOTE(recommended);
LIBSELINUX_NOTE(recommended);
+ TPM2_NOTE(suggested);
log_setup();
diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c
index 5068a28f53c..d35538e7680 100644
--- a/src/firstboot/firstboot.c
+++ b/src/firstboot/firstboot.c
@@ -1736,6 +1736,7 @@ static int run(int argc, char *argv[]) {
LIBMOUNT_NOTE(recommended);
LIBSELINUX_NOTE(recommended);
PASSWORD_NOTE(suggested);
+ TPM2_NOTE(suggested);
r = parse_argv(argc, argv);
if (r <= 0)
diff --git a/src/journal/journalctl.c b/src/journal/journalctl.c
index 2023e3bf287..41d11a3543a 100644
--- a/src/journal/journalctl.c
+++ b/src/journal/journalctl.c
@@ -1012,6 +1012,7 @@ static int run(int argc, char *argv[]) {
LIBMOUNT_NOTE(recommended);
LIBPCRE2_NOTE(suggested);
LIBQRENCODE_NOTE(suggested);
+ TPM2_NOTE(suggested);
setlocale(LC_ALL, "");
log_setup();
diff --git a/src/kernel-install/kernel-install.c b/src/kernel-install/kernel-install.c
index 0fb3a4034e0..905f8d236a9 100644
--- a/src/kernel-install/kernel-install.c
+++ b/src/kernel-install/kernel-install.c
@@ -1740,6 +1740,7 @@ static int run(int argc, char* argv[]) {
LIBCRYPTO_NOTE(suggested);
LIBCRYPTSETUP_NOTE(suggested);
LIBMOUNT_NOTE(recommended);
+ TPM2_NOTE(suggested);
log_setup();
diff --git a/src/machine/image-dbus.c b/src/machine/image-dbus.c
index 4a61b48f3f7..27b1cc15740 100644
--- a/src/machine/image-dbus.c
+++ b/src/machine/image-dbus.c
@@ -295,7 +295,7 @@ int bus_image_method_get_hostname(
int r;
if (!image->metadata_valid) {
- r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
+ r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope, /* extra_flags= */ 0);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
@@ -314,7 +314,7 @@ int bus_image_method_get_machine_id(
int r;
if (!image->metadata_valid) {
- r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
+ r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope, /* extra_flags= */ 0);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
@@ -343,7 +343,7 @@ int bus_image_method_get_machine_info(
int r;
if (!image->metadata_valid) {
- r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
+ r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope, /* extra_flags= */ 0);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
@@ -361,7 +361,7 @@ int bus_image_method_get_os_release(
int r;
if (!image->metadata_valid) {
- r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
+ r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope, /* extra_flags= */ 0);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
diff --git a/src/machine/machined-varlink.c b/src/machine/machined-varlink.c
index ae121395c01..4b449ff2656 100644
--- a/src/machine/machined-varlink.c
+++ b/src/machine/machined-varlink.c
@@ -633,7 +633,7 @@ static int list_image_one_and_maybe_read_metadata(Manager *m, sd_varlink *link,
assert(image);
if (should_acquire_metadata(am) && !image->metadata_valid) {
- r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope);
+ r = image_read_metadata(image, /* root= */ NULL, &image_policy_container, m->runtime_scope, /* extra_flags= */ 0);
if (r < 0 && am != ACQUIRE_METADATA_GRACEFUL)
return log_debug_errno(r, "Failed to read image metadata: %m");
if (r < 0)
diff --git a/src/machine/machined.c b/src/machine/machined.c
index 66d128c15b3..ee84ef60604 100644
--- a/src/machine/machined.c
+++ b/src/machine/machined.c
@@ -365,6 +365,7 @@ static int run(int argc, char *argv[]) {
LIBCRYPTSETUP_NOTE(suggested);
LIBMOUNT_NOTE(recommended);
LIBSELINUX_NOTE(recommended);
+ TPM2_NOTE(suggested);
log_set_facility(LOG_AUTH);
log_setup();
diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c
index af84ace1eb9..c5adac7852b 100644
--- a/src/mountfsd/mountwork.c
+++ b/src/mountfsd/mountwork.c
@@ -1494,6 +1494,7 @@ static int run(int argc, char *argv[]) {
LIBCRYPTO_NOTE(suggested);
LIBCRYPTSETUP_NOTE(suggested);
LIBMOUNT_NOTE(recommended);
+ TPM2_NOTE(suggested);
log_setup();
diff --git a/src/mstack/mstack-tool.c b/src/mstack/mstack-tool.c
index a8d38d91644..455d2b1a033 100644
--- a/src/mstack/mstack-tool.c
+++ b/src/mstack/mstack-tool.c
@@ -373,6 +373,7 @@ static int run(int argc, char *argv[]) {
LIBCRYPTO_NOTE(suggested);
LIBCRYPTSETUP_NOTE(suggested);
LIBMOUNT_NOTE(recommended);
+ TPM2_NOTE(suggested);
log_setup();
diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c
index 988184a6631..e2b02ece7f4 100644
--- a/src/nspawn/nspawn.c
+++ b/src/nspawn/nspawn.c
@@ -3877,6 +3877,7 @@ static DissectImageFlags determine_dissect_image_flags(void) {
(arg_read_only ? DISSECT_IMAGE_READ_ONLY : DISSECT_IMAGE_FSCK|DISSECT_IMAGE_GROWFS) |
DISSECT_IMAGE_ALLOW_USERSPACE_VERITY |
DISSECT_IMAGE_VERITY_SHARE |
+ DISSECT_IMAGE_TPM2_UNLOCK |
(arg_console_mode == CONSOLE_INTERACTIVE && arg_ask_password ? DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH : 0) |
((arg_userns_ownership == USER_NAMESPACE_OWNERSHIP_FOREIGN) ? DISSECT_IMAGE_FOREIGN_UID :
(arg_userns_ownership != USER_NAMESPACE_OWNERSHIP_AUTO) ? DISSECT_IMAGE_IDENTITY_UID : 0);
@@ -6114,6 +6115,7 @@ static int run(int argc, char *argv[]) {
LIBMOUNT_NOTE(recommended);
LIBSECCOMP_NOTE(recommended);
LIBSELINUX_NOTE(recommended);
+ TPM2_NOTE(suggested);
(void) dlopen_cryptsetup(LOG_DEBUG);
(void) dlopen_libacl(LOG_DEBUG);
(void) dlopen_libblkid(LOG_DEBUG);
diff --git a/src/portable/portabled-image-bus.c b/src/portable/portabled-image-bus.c
index 5a5393e986a..935f0e619bb 100644
--- a/src/portable/portabled-image-bus.c
+++ b/src/portable/portabled-image-bus.c
@@ -61,7 +61,7 @@ int bus_image_common_get_os_release(
return 1;
if (!image->metadata_valid) {
- r = image_read_metadata(image, /* root= */ NULL, &image_policy_service, m->runtime_scope);
+ r = image_read_metadata(image, /* root= */ NULL, &image_policy_service, m->runtime_scope, /* extra_flags= */ 0);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
}
diff --git a/src/portable/portabled.c b/src/portable/portabled.c
index e42ffed4d4f..4ac85b1870e 100644
--- a/src/portable/portabled.c
+++ b/src/portable/portabled.c
@@ -161,6 +161,7 @@ static int run(int argc, char *argv[]) {
LIBCRYPTSETUP_NOTE(recommended);
LIBMOUNT_NOTE(required);
LIBSELINUX_NOTE(recommended);
+ TPM2_NOTE(suggested);
log_setup();
diff --git a/src/shared/cryptsetup-tpm2.c b/src/shared/cryptsetup-tpm2.c
index e989c3c5fde..e1ba115dbf3 100644
--- a/src/shared/cryptsetup-tpm2.c
+++ b/src/shared/cryptsetup-tpm2.c
@@ -297,6 +297,7 @@ int find_tpm2_auto_data(
struct crypt_device *cd,
uint32_t search_pcr_mask,
int start_token,
+ int log_level,
uint32_t *ret_hash_pcr_mask,
uint16_t *ret_pcr_bank,
struct iovec *ret_pubkey,
@@ -355,7 +356,7 @@ int find_tpm2_auto_data(
if (ERRNO_IS_NEG_CRYPTSETUP_TOKEN_SKIP(r))
continue;
if (r < 0)
- return log_error_errno(r, "Failed to read JSON token data off disk: %m");
+ return log_full_errno(log_level, r, "Failed to read JSON token data off disk: %m");
r = tpm2_parse_luks2_json(
v,
@@ -378,13 +379,16 @@ int find_tpm2_auto_data(
if (r == -EUCLEAN) /* Gracefully handle issues in JSON fields not owned by us */
continue;
if (r < 0)
- return log_error_errno(r, "Failed to parse TPM2 JSON data: %m");
+ return log_full_errno(log_level, r, "Failed to parse TPM2 JSON data: %m");
if (search_pcr_mask == UINT32_MAX ||
search_pcr_mask == hash_pcr_mask) {
if (start_token <= 0)
- log_info("Automatically discovered security TPM2 token unlocks volume.");
+ /* Syslog priorities count the other way round: the higher the number, the
+ * less severe. Hence MAX() keeps this at LOG_INFO for callers passing
+ * LOG_ERR, and lowers it to LOG_DEBUG for callers requesting quiet operation. */
+ log_full(MAX(log_level, LOG_INFO), "Automatically discovered security TPM2 token unlocks volume.");
*ret_hash_pcr_mask = hash_pcr_mask;
*ret_pcr_bank = pcr_bank;
@@ -410,8 +414,164 @@ int find_tpm2_auto_data(
/* PCR mask doesn't match what is configured, ignore this entry, let's see next */
}
- return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "No valid TPM2 token data found.");
+ return log_full_errno(log_level, SYNTHETIC_ERRNO(ENXIO), "No valid TPM2 token data found.");
#else
- return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support not available.");
+ return log_full_errno(log_level, SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support not available.");
+#endif
+}
+
+int cryptsetup_tpm2_unlock_auto(
+ struct crypt_device *cd,
+ int *ret_keyslot,
+ char **ret_passphrase) {
+
+#if HAVE_LIBCRYPTSETUP && HAVE_TPM2
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *signature_json = NULL;
+ _cleanup_(tpm2_context_unrefp) Tpm2Context *tpm2_context = NULL;
+ bool found_some = false, signature_load_failed = false;
+ int r;
+
+ assert(cd);
+ assert(ret_keyslot);
+ assert(ret_passphrase);
+
+ /* Non-interactive counterpart to acquire_tpm2_key(), for use from library code. Walks all
+ * "systemd-tpm2" tokens in the LUKS2 header and returns the first one that unseals against the
+ * local TPM2 (auto-detected), as the base64-encoded keyslot passphrase (which is how
+ * systemd-cryptenroll and systemd-repart enroll the sealed secret) plus the keyslot it belongs to.
+ * PIN-protected tokens are skipped, we never prompt. Logs at debug level only, except that the
+ * pcrlock policy helpers log on their own for pcrlock-bound tokens. Returns -ENXIO if no token could
+ * be used. */
+
+ for (int token = 0;; token++) {
+ _cleanup_(iovec_done) struct iovec pubkey = {}, salt = {}, srk = {}, pcrlock_nv = {};
+ _cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy pcrlock_policy = {};
+ _cleanup_(iovec_done_erase) struct iovec secret = {};
+ _cleanup_(erase_and_freep) char *b64 = NULL;
+ _cleanup_free_ char *pubkey_policy_ref = NULL;
+ struct iovec *blobs = NULL, *policy_hash = NULL;
+ size_t n_blobs = 0, n_policy_hash = 0;
+ uint32_t hash_pcr_mask, pubkey_pcr_mask;
+ uint16_t pcr_bank, primary_alg;
+ TPM2Flags flags;
+ int keyslot;
+
+ CLEANUP_ARRAY(blobs, n_blobs, iovec_array_free);
+ CLEANUP_ARRAY(policy_hash, n_policy_hash, iovec_array_free);
+
+ r = find_tpm2_auto_data(
+ cd,
+ /* search_pcr_mask= */ UINT32_MAX,
+ token,
+ LOG_DEBUG,
+ &hash_pcr_mask,
+ &pcr_bank,
+ &pubkey,
+ &pubkey_policy_ref,
+ &pubkey_pcr_mask,
+ &primary_alg,
+ &blobs,
+ &n_blobs,
+ &policy_hash,
+ &n_policy_hash,
+ &salt,
+ &srk,
+ &pcrlock_nv,
+ &flags,
+ &keyslot,
+ &token,
+ /* ret_argon2id_params= */ NULL);
+ if (r == -ENXIO) /* No (more) TPM2 tokens in the header */
+ break;
+ if (r < 0)
+ return r;
+
+ if (flags & (TPM2_FLAGS_USE_PIN|TPM2_FLAGS_USE_ARGON2ID)) {
+ log_debug("TPM2 token %i requires a PIN (or Argon2id), skipping.", token);
+ continue;
+ }
+
+ found_some = true;
+
+ if (pubkey_pcr_mask != 0) {
+ /* The signature file is the same for all tokens, hence load it only once, and don't
+ * retry if that failed. */
+ if (!signature_json && !signature_load_failed) {
+ r = tpm2_load_pcr_signature(/* path= */ NULL, &signature_json);
+ if (r == -ENOMEM)
+ return r;
+ if (r < 0) {
+ log_debug_errno(r, "Failed to load PCR signature, cannot use TPM2 tokens bound to a public key: %m");
+ signature_load_failed = true;
+ }
+ }
+ if (!signature_json)
+ continue;
+ }
+
+ if (FLAGS_SET(flags, TPM2_FLAGS_USE_PCRLOCK)) {
+ r = tpm2_pcrlock_policy_load(/* path= */ NULL, &pcrlock_policy);
+ if (r == 0) /* Not found? Then search among passed credentials */
+ r = tpm2_pcrlock_policy_from_credentials(&srk, &pcrlock_nv, &pcrlock_policy);
+ if (r == -ENOMEM)
+ return r;
+ if (r < 0)
+ log_debug_errno(r, "Failed to load pcrlock policy for TPM2 token %i, skipping: %m", token);
+ else if (r == 0)
+ log_debug("No pcrlock policy found for TPM2 token %i, skipping.", token);
+ if (r <= 0)
+ continue;
+ }
+
+ /* Only talk to the TPM2 once we know there's a token we could actually use */
+ if (!tpm2_context) {
+ _cleanup_free_ char *device = NULL;
+
+ r = tpm2_find_device_auto(&device);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to find TPM2 device: %m");
+
+ r = tpm2_context_new(device, &tpm2_context);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to create TPM2 context: %m");
+ }
+
+ r = tpm2_unseal(tpm2_context,
+ hash_pcr_mask,
+ pcr_bank,
+ &pubkey,
+ pubkey_policy_ref,
+ pubkey_pcr_mask,
+ signature_json,
+ /* pin= */ NULL,
+ FLAGS_SET(flags, TPM2_FLAGS_USE_PCRLOCK) ? &pcrlock_policy : NULL,
+ primary_alg,
+ blobs,
+ n_blobs,
+ policy_hash,
+ n_policy_hash,
+ &srk,
+ &secret);
+ if (ERRNO_IS_NEG_TPM2_TOKEN_MISMATCH(r)) {
+ log_debug_errno(r, "TPM2 token %i does not match current system state, skipping: %m", token);
+ continue;
+ }
+ if (r < 0)
+ return log_debug_errno(r, "Failed to unseal secret using TPM2: %m");
+
+ /* The sealed secret is enrolled base64-encoded as keyslot passphrase, for compat with homed */
+ if (base64mem(secret.iov_base, secret.iov_len, &b64) < 0)
+ return -ENOMEM;
+
+ *ret_keyslot = keyslot;
+ *ret_passphrase = TAKE_PTR(b64);
+ return 0;
+ }
+
+ return log_debug_errno(SYNTHETIC_ERRNO(ENXIO),
+ found_some ? "No TPM2 token matching the current system state found in LUKS2 header."
+ : "No TPM2 token usable without user interaction enrolled in LUKS2 header.");
+#else
+ return -EOPNOTSUPP;
#endif
}
diff --git a/src/shared/cryptsetup-tpm2.h b/src/shared/cryptsetup-tpm2.h
index 6d0370847d2..f3348f3d242 100644
--- a/src/shared/cryptsetup-tpm2.h
+++ b/src/shared/cryptsetup-tpm2.h
@@ -33,10 +33,16 @@ int acquire_tpm2_key(
const Argon2IdParameters *argon2id_params,
struct iovec *ret_decrypted_key);
+int cryptsetup_tpm2_unlock_auto(
+ struct crypt_device *cd,
+ int *ret_keyslot,
+ char **ret_passphrase);
+
int find_tpm2_auto_data(
struct crypt_device *cd,
uint32_t search_pcr_mask,
int start_token,
+ int log_level,
uint32_t *ret_hash_pcr_mask,
uint16_t *ret_pcr_bank,
struct iovec *ret_pubkey,
diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c
index fa0fd1928f6..7c2a7901a9e 100644
--- a/src/shared/discover-image.c
+++ b/src/shared/discover-image.c
@@ -2228,7 +2228,7 @@ int image_setup_pool(RuntimeScope scope, ImageClass class, bool use_btrfs_subvol
return 0;
}
-int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_policy, RuntimeScope scope) {
+int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_policy, RuntimeScope scope, DissectImageFlags extra_flags) {
_cleanup_(release_lock_file) LockFile global_lock = LOCK_FILE_INIT, local_lock = LOCK_FILE_INIT;
int r;
@@ -2316,7 +2316,8 @@ int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_pol
DISSECT_IMAGE_PIN_PARTITION_DEVICES |
DISSECT_IMAGE_VALIDATE_OS |
DISSECT_IMAGE_VALIDATE_OS_EXT |
- DISSECT_IMAGE_ALLOW_USERSPACE_VERITY;
+ DISSECT_IMAGE_ALLOW_USERSPACE_VERITY |
+ extra_flags;
r = loop_device_make_by_path(
i->path,
diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h
index 71d00cbdb07..63fc9a65c7b 100644
--- a/src/shared/discover-image.h
+++ b/src/shared/discover-image.h
@@ -76,7 +76,7 @@ int image_get_pool_usage(RuntimeScope scope, ImageClass class, uint64_t *ret);
int image_get_pool_limit(RuntimeScope scope, ImageClass class, uint64_t *ret);
int image_setup_pool(RuntimeScope scope, ImageClass class, bool use_btrfs_subvol, bool use_btrfs_quota);
-int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_policy, RuntimeScope scope);
+int image_read_metadata(Image *i, const char *root, const ImagePolicy *image_policy, RuntimeScope scope, DissectImageFlags extra_flags);
bool image_in_search_path(RuntimeScope scope, ImageClass class, const char *root, const char *image);
diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c
index 10ef86ed59d..9246f0745cc 100644
--- a/src/shared/dissect-image.c
+++ b/src/shared/dissect-image.c
@@ -21,6 +21,7 @@
#include "constants.h"
#include "copy.h"
#include "crypto-util.h"
+#include "cryptsetup-tpm2.h"
#include "cryptsetup-util.h"
#include "device-private.h"
#include "devnum-util.h"
@@ -47,6 +48,7 @@
#include "json-util.h"
#include "libmount-util.h"
#include "loop-util.h"
+#include "memory-util.h"
#include "mkdir.h"
#include "mount-util.h"
#include "mountpoint-util.h"
@@ -3020,7 +3022,7 @@ static int decrypt_partition(
if (!streq(m->fstype, "crypto_LUKS"))
return 0;
- if (!passphrase)
+ if (!passphrase && !FLAGS_SET(flags, DISSECT_IMAGE_TPM2_UNLOCK))
return -ENOKEY;
if (!FLAGS_SET(policy_flags, PARTITION_POLICY_ENCRYPTED))
@@ -3054,12 +3056,43 @@ static int decrypt_partition(
if (r < 0)
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) || 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;
+ uint32_t activate_flags = ((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);
+
+ /* A supplied passphrase is used exclusively, the TPM2 is not consulted then, even if
+ * DISSECT_IMAGE_TPM2_UNLOCK is set. Interactive callers only supply one after the TPM2 attempt below
+ * failed and they prompted the user. */
+ if (passphrase) {
+ r = sym_crypt_activate_by_passphrase(cd, name, CRYPT_ANY_SLOT, passphrase, strlen(passphrase), activate_flags);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to activate LUKS device: %m");
+ return r == -EPERM ? -EKEYREJECTED : r;
+ }
+ } else {
+ /* No passphrase, but TPM2 unlocking was requested (see check above): see if a TPM2 token
+ * enrolled in the LUKS2 header (e.g. via "systemd-cryptenroll --tpm2-device=…") unseals
+ * against the local TPM2. If not — no TPM2, no token, policy mismatch — this fails with
+ * -ENOKEY, exactly as if no TPM2 token existed, so that interactive callers prompt for a
+ * passphrase and retry with it. If the unsealed secret is rejected by its keyslot we don't
+ * try the remaining tokens, matching what systemd-cryptsetup does. */
+ _cleanup_(erase_and_freep) char *tpm2_passphrase = NULL;
+ int keyslot;
+
+ r = cryptsetup_tpm2_unlock_auto(cd, &keyslot, &tpm2_passphrase);
+ if (r == -ENOMEM)
+ return r;
+ if (r < 0) {
+ log_debug_errno(r, "Unable to unlock LUKS device via TPM2, and no passphrase supplied: %m");
+ return -ENOKEY;
+ }
+
+ r = sym_crypt_activate_by_passphrase(cd, name, keyslot, tpm2_passphrase, strlen(tpm2_passphrase), activate_flags);
+ if (r == -EPERM) {
+ log_debug_errno(r, "Secret unsealed via TPM2 does not unlock LUKS keyslot %i, token out of sync with keyslot: %m", keyslot);
+ return -ENOKEY;
+ }
+ if (r < 0)
+ return log_debug_errno(r, "Failed to activate LUKS device with TPM2-unsealed secret: %m");
}
fd = open(node, O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_NOCTTY);
@@ -3611,7 +3644,8 @@ int dissected_image_decrypt(
*
* = 0 → There was nothing to decrypt/setup
* > 0 → Decrypted/setup successfully
- * -ENOKEY → dm-crypt: there's something to decrypt but no decryption key was supplied
+ * -ENOKEY → dm-crypt: there's something to decrypt but no decryption key was supplied (or
+ * TPM2 unlocking was requested via DISSECT_IMAGE_TPM2_UNLOCK, but didn't succeed)
* -EKEYREJECTED → dm-crypt: Passed key was not correct
* -EDESTADDRREQ → dm-verity: there's something to setup but no signature was supplied
* -EBUSY → dm-verity: Generic Verity error (kernel is not very explanatory)
@@ -3629,6 +3663,12 @@ int dissected_image_decrypt(
if (r >= 0)
SET_FLAG(flags, DISSECT_IMAGE_VERITY_SHARE, r);
+ /* The environment may only turn TPM2 unlocking off, never on: callers such as systemd-mountfsd
+ * act on behalf of unprivileged clients and must not be talked into unsealing images for them. */
+ r = secure_getenv_bool("SYSTEMD_DISSECT_TPM2_UNLOCK");
+ if (r == 0)
+ flags &= ~DISSECT_IMAGE_TPM2_UNLOCK;
+
#if HAVE_LIBCRYPTSETUP
r = decrypted_image_new(&d);
if (r < 0)
diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h
index 7ba04ebfdd1..4cede743d5f 100644
--- a/src/shared/dissect-image.h
+++ b/src/shared/dissect-image.h
@@ -81,6 +81,7 @@ typedef enum DissectImageFlags {
DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH = 1 << 27, /* Allow interactive authorization when going through mountfsd */
DISSECT_IMAGE_FOREIGN_UID = 1 << 28, /* Request a foreign UID range mapping */
DISSECT_IMAGE_IDENTITY_UID = 1 << 29, /* Explicitly request an identity UID range mapping */
+ DISSECT_IMAGE_TPM2_UNLOCK = 1 << 30, /* If no passphrase is supplied, try unlocking LUKS partitions via a TPM2 token enrolled in the LUKS2 header */
} DissectImageFlags;
typedef struct DissectedImage {
diff --git a/src/shared/forward.h b/src/shared/forward.h
index f70a9a12753..d508f24ccda 100644
--- a/src/shared/forward.h
+++ b/src/shared/forward.h
@@ -20,6 +20,7 @@ typedef enum BusTransport BusTransport;
typedef enum CatFlags CatFlags;
typedef enum CertificateSourceType CertificateSourceType;
typedef enum ConditionType ConditionType;
+typedef enum DissectImageFlags DissectImageFlags;
typedef enum DnsAnswerFlags DnsAnswerFlags;
typedef enum DnsCacheMode DnsCacheMode;
typedef enum DnsOverTlsMode DnsOverTlsMode;
diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c
index fe2b7938a2c..097774ca11c 100644
--- a/src/shared/tpm2-util.c
+++ b/src/shared/tpm2-util.c
@@ -12109,6 +12109,8 @@ int tpm2_parse_luks2_json(
const char *s = sd_json_variant_string(w);
if (!s)
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Public key policy reference is not a string.");
+ if (!iovec_is_set(&pubkey))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Public key policy reference set, but no public key included in JSON data, refusing.");
pubkey_ref = strdup(s);
if (!pubkey_ref)
return log_oom_debug();
diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c
index d751dd90c05..28545481c7f 100644
--- a/src/sysext/sysext.c
+++ b/src/sysext/sysext.c
@@ -2068,7 +2068,8 @@ static int merge_subprocess(
DISSECT_IMAGE_USR_NO_ROOT |
DISSECT_IMAGE_ADD_PARTITION_DEVICES |
DISSECT_IMAGE_PIN_PARTITION_DEVICES |
- DISSECT_IMAGE_ALLOW_USERSPACE_VERITY;
+ DISSECT_IMAGE_ALLOW_USERSPACE_VERITY |
+ DISSECT_IMAGE_TPM2_UNLOCK;
r = verity_settings_load(&verity_settings, img->path, NULL, NULL);
if (r < 0)
@@ -2651,7 +2652,7 @@ static int image_discover_and_read_metadata(const Context *c, Hashmap **ret_imag
return log_error_errno(r, "Failed to discover images: %m");
HASHMAP_FOREACH(img, images) {
- r = image_read_metadata(img, c->root, image_class_info[c->image_class].default_image_policy, RUNTIME_SCOPE_SYSTEM);
+ r = image_read_metadata(img, c->root, image_class_info[c->image_class].default_image_policy, RUNTIME_SCOPE_SYSTEM, DISSECT_IMAGE_TPM2_UNLOCK);
if (r < 0)
return log_error_errno(r, "Failed to read metadata for image %s: %m", img->name);
}
@@ -3249,6 +3250,7 @@ static int run(int argc, char *argv[]) {
LIBCRYPTSETUP_NOTE(recommended);
LIBMOUNT_NOTE(required);
LIBSELINUX_NOTE(recommended);
+ TPM2_NOTE(suggested);
log_setup();
diff --git a/src/sysinstall/sysinstall.c b/src/sysinstall/sysinstall.c
index ce5775e9b99..b7f3e45e2ae 100644
--- a/src/sysinstall/sysinstall.c
+++ b/src/sysinstall/sysinstall.c
@@ -2109,6 +2109,7 @@ static int run(int argc, char *argv[]) {
LIBCRYPTO_NOTE(suggested);
LIBCRYPTSETUP_NOTE(suggested);
LIBMOUNT_NOTE(recommended);
+ TPM2_NOTE(suggested);
setlocale(LC_ALL, "");
diff --git a/src/systemctl/systemctl-main.c b/src/systemctl/systemctl-main.c
index fea186d9cd2..03cc9b1830f 100644
--- a/src/systemctl/systemctl-main.c
+++ b/src/systemctl/systemctl-main.c
@@ -30,6 +30,7 @@ static int run(int argc, char *argv[]) {
LIBCRYPTSETUP_NOTE(suggested);
LIBMOUNT_NOTE(recommended);
LIBSELINUX_NOTE(recommended);
+ TPM2_NOTE(suggested);
setlocale(LC_ALL, "");
log_setup();
diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c
index 43fc0193392..5ce7fc7b7ef 100644
--- a/src/sysupdate/sysupdate.c
+++ b/src/sysupdate/sysupdate.c
@@ -3674,9 +3674,7 @@ static int run(int argc, char *argv[]) {
LIBFDISK_NOTE(recommended);
LIBMOUNT_NOTE(recommended);
LIBSELINUX_NOTE(recommended);
- LIBTSS2_ESYS_NOTE(suggested);
- LIBTSS2_MU_NOTE(suggested);
- LIBTSS2_RC_NOTE(suggested);
+ TPM2_NOTE(suggested);
log_setup();
diff --git a/src/sysusers/sysusers.c b/src/sysusers/sysusers.c
index 3771185f538..b639e822ac9 100644
--- a/src/sysusers/sysusers.c
+++ b/src/sysusers/sysusers.c
@@ -2288,6 +2288,7 @@ static int run(int argc, char *argv[]) {
LIBCRYPTO_NOTE(suggested);
LIBMOUNT_NOTE(recommended);
LIBSELINUX_NOTE(recommended);
+ TPM2_NOTE(suggested);
char **args = NULL;
r = parse_argv(argc, argv, &args);
diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c
index b1f81d127c7..8477222add2 100644
--- a/src/tmpfiles/tmpfiles.c
+++ b/src/tmpfiles/tmpfiles.c
@@ -5288,6 +5288,7 @@ static int run(int argc, char *argv[]) {
LIBCRYPTO_NOTE(suggested);
LIBMOUNT_NOTE(recommended);
LIBSELINUX_NOTE(recommended);
+ TPM2_NOTE(suggested);
char **args = NULL;
r = parse_argv(argc, argv, &args);
diff --git a/src/tpm2-setup/tpm2-generator.c b/src/tpm2-setup/tpm2-generator.c
index 6c27fa106af..a9dc1462ab7 100644
--- a/src/tpm2-setup/tpm2-generator.c
+++ b/src/tpm2-setup/tpm2-generator.c
@@ -12,6 +12,7 @@
#include "path-util.h"
#include "proc-cmdline.h"
#include "special.h"
+#include "strv.h"
#include "tpm2-util.h"
/* A small generator that enqueues tpm2.target as synchronization point if the TPM2 device hasn't shown up
@@ -74,6 +75,39 @@ static int generate_tpm_target_symlink(Tpm2Support support, bool software_fallba
return 0;
}
+static int generate_initrd_extension_ordering(bool software_fallback_enabled) {
+ int r;
+
+ /* systemd-sysext-initrd.service and systemd-confext-initrd.service might have to unlock LUKS
+ * encrypted extension images via the TPM2, hence order them after tpm2.target and after the
+ * "enter-initrd" PCR measurement. This cannot be done statically in the units: they run before
+ * local-fs-pre.target, but the software TPM fallback only becomes available after the early ESP
+ * mount, which is ordered after local-fs-pre.target, so with the fallback the ordering would be
+ * a cycle. Hence add it here, and only if the fallback is not in use. */
+
+ if (!in_initrd())
+ return 0;
+
+ if (software_fallback_enabled) {
+ log_debug("Not ordering initrd extension image units after tpm2.target, as the software TPM fallback is in use.");
+ return 0;
+ }
+
+ FOREACH_STRING(u, "systemd-sysext-initrd.service", "systemd-confext-initrd.service") {
+ r = write_drop_in(
+ arg_dest,
+ u,
+ 50, "tpm2",
+ "# Automatically generated by systemd-tpm2-generator\n\n"
+ "[Unit]\n"
+ "After=tpm2.target systemd-pcrphase-initrd.service\n");
+ if (r < 0)
+ return log_error_errno(r, "Failed to order %s after tpm2.target: %m", u);
+ }
+
+ return 0;
+}
+
static int generate_swtpm_symlink(Tpm2Support support) {
int r;
@@ -158,7 +192,13 @@ static int generate_now(void) {
if (r < 0)
return r;
- r = generate_tpm_target_symlink(support, /* software_fallback_enabled= */ r > 0);
+ bool software_fallback_enabled = r > 0;
+
+ r = generate_tpm_target_symlink(support, software_fallback_enabled);
+ if (r < 0)
+ return r;
+
+ r = generate_initrd_extension_ordering(software_fallback_enabled);
if (r < 0)
return r;
diff --git a/test/units/TEST-70-TPM2.dissect.sh b/test/units/TEST-70-TPM2.dissect.sh
new file mode 100755
index 00000000000..9b31b295a74
--- /dev/null
+++ b/test/units/TEST-70-TPM2.dissect.sh
@@ -0,0 +1,188 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -eux
+set -o pipefail
+
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+# Test that systemd-dissect and systemd-sysext unlock a DDI whose LUKS partition carries a "systemd-tpm2"
+# LUKS2 token via the local TPM2 if no passphrase is supplied, that a supplied passphrase is used exclusively,
+# and that systemd-dissect falls back to asking for a passphrase if the token does not match the current
+# system state (DISSECT_IMAGE_TPM2_UNLOCK in dissect-image.c).
+#
+# The images' LUKS partitions are bound to PCR 16 (the resettable "debug" PCR nothing else in this test
+# depends on), so that extending that PCR afterwards invalidates the tokens. The PCR is reset on exit.
+#
+# Without a matching TPM2 token and without $PASSWORD, systemd-dissect asks for a passphrase via
+# ask_password_auto() with an infinite timeout. That query is satisfied here via the "dissect.passphrase"
+# credential, so that the fallback can be exercised without blocking on the password agent forever in this
+# non-interactive environment. For the same reason there is no "no passphrase at all + non-matching token
+# must fail" case.
+
+if ! tpm_has_pcr sha256 16; then
+ echo "TPM2 has no sha256 PCR 16, skipping"
+ exit 77
+fi
+
+export SYSTEMD_LOG_LEVEL=debug
+
+WORKDIR="$(mktemp -d /tmp/dissect-tpm2.XXXXXXXXXX)"
+IMAGE="$WORKDIR/image.raw"
+IMAGE_REPART="$WORKDIR/image-repart.raw"
+MNT="$WORKDIR/mnt"
+LOG="$WORKDIR/dissect.log"
+# Fake root for systemd-sysext --root=, so that the real /usr is left alone
+SYSEXT_ROOT="$WORKDIR/root"
+SYSEXT_NAME="dissect-tpm2"
+DEVICE=""
+
+at_exit() {
+ set +e
+
+ if mountpoint -q "$MNT"; then
+ systemd-dissect --umount "$MNT"
+ fi
+ if [[ -n "$DEVICE" ]]; then
+ systemd-dissect --detach "$DEVICE"
+ fi
+ if [[ -d "$SYSEXT_ROOT" ]]; then
+ systemd-sysext --root="$SYSEXT_ROOT" unmerge
+ fi
+
+ rm -rf "$WORKDIR"
+
+ # Undo the PCR extension below, for the subtests that run after this one
+ tpm2_pcrreset 16
+}
+
+trap at_exit EXIT
+
+# All mounts are wrapped in timeout(1): should TPM2 unlocking not work, systemd-dissect would otherwise
+# block on the password agent (see above) instead of failing the test.
+dissect_mount() {
+ if ! timeout --kill-after=10 120 systemd-dissect --mount "$IMAGE" "$MNT" &>"$LOG"; then
+ cat "$LOG"
+ return 1
+ fi
+ test -f "$MNT/usr/lib/os-release"
+ systemd-dissect --umount "$MNT"
+}
+
+# Enroll the TPM2 into the LUKS partition of the specified image, bound to PCR 16
+enroll_tpm2() {
+ local image="${1:?}"
+
+ DEVICE="$(systemd-dissect --attach "$image")"
+ udevadm wait --settle --timeout=10 "$DEVICE"p1
+ # Use --tpm2-public-key= and --tpm2-pcrlock= to suppress auto-loading any PCR public key or pcrlock
+ # policy from the host
+ systemd-cryptenroll --unlock-key-file="$WORKDIR/passphrase" --tpm2-device=auto --tpm2-public-key= --tpm2-pcrlock= --tpm2-pcrs=16 "$DEVICE"p1
+ systemd-dissect --detach "$DEVICE"
+ DEVICE=""
+}
+
+mkdir -p "$WORKDIR/defs" "$WORKDIR/creds" "$MNT"
+echo -n passphrase >"$WORKDIR/passphrase"
+# Avoid "has 0644 mode that is too permissive" messages
+chmod 0600 "$WORKDIR/passphrase"
+# The same passphrase as a credential, for the cases where systemd-dissect has to ask for it
+cp "$WORKDIR/passphrase" "$WORKDIR/creds/dissect.passphrase"
+
+# Build a DDI with a passphrase-encrypted root partition, then enroll the TPM2 into it with
+# systemd-cryptenroll, i.e. the way such an image would be prepared by hand today.
+cat >"$WORKDIR/defs/root.conf" <"$WORKDIR/ext/usr/lib/extension-release.d/extension-release.$SYSEXT_NAME"
+touch "$WORKDIR/ext/usr/lib/$SYSEXT_NAME-marker"
+echo "ID=testtest" >"$SYSEXT_ROOT/usr/lib/os-release"
+
+cat >"$WORKDIR/defs/root.conf" <"$WORKDIR/defs/root.conf" <