dissect-image: unlock LUKS partitions in DDIs via enrolled TPM2 tokens (#44132)

This commit is contained in:
Lennart Poettering authored and GitHub committed 2026-10-14 09:36:29 +02:00
commit d4a70ecc31
44 files changed
+540 -35

No files matched your search

+8
View File
@@ -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
+8 -1
View File
@@ -152,7 +152,14 @@
<para>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.</para>
unmounted. LUKS volumes carrying a TPM2 token (as enrolled via the <option>--tpm2-device=</option>
option of
<citerefentry><refentrytitle>systemd-cryptenroll</refentrytitle><manvolnum>1</manvolnum></citerefentry>)
are unlocked via the local TPM2 automatically, before a passphrase is requested. This may be turned
off via the <varname>$SYSTEMD_DISSECT_TPM2_UNLOCK</varname> environment variable. Note that this
requires privileges: when invoked unprivileged, images are mounted via
<citerefentry><refentrytitle>systemd-mountfsd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
which does not currently support unlocking via TPM2.</para>
<para>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
+6 -1
View File
@@ -341,7 +341,12 @@
<filename>/efi</filename> (or <filename>/boot</filename> as fallback) in case a directory by this name exists
and is empty.</para>
<para>Partitions encrypted with LUKS are automatically decrypted. Also, on GPT images dm-verity data integrity
<para>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
<varname>$SYSTEMD_DISSECT_TPM2_UNLOCK</varname> environment variable), or otherwise by prompting for a
passphrase. Unlocking via TPM2 requires privileges: when invoked unprivileged, images are mounted via
<citerefentry><refentrytitle>systemd-mountfsd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
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>--root-hash=</option>
option.</para>
+5 -1
View File
@@ -101,7 +101,11 @@
<citerefentry><refentrytitle>systemd-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>
supports via its <option>--directory=</option>/<option>--image=</option> switches and those that the
service manager supports via <option>RootDirectory=</option>/<option>RootImage=</option>. Similar to
them they may optionally carry Verity authentication information.</para>
them they may optionally carry Verity authentication information. LUKS encrypted extension images
carrying a TPM2 token (as enrolled via the <option>--tpm2-device=</option> option of
<citerefentry><refentrytitle>systemd-cryptenroll</refentrytitle><manvolnum>1</manvolnum></citerefentry>)
are unlocked via the local TPM2 automatically, without any passphrase. This may be turned off via the
<varname>$SYSTEMD_DISSECT_TPM2_UNLOCK</varname> environment variable.</para>
<para>System extensions are searched for in the directories
<filename>/etc/extensions/</filename>, <filename>/run/extensions/</filename> and
+10
View File
@@ -53,6 +53,16 @@
<citerefentry><refentrytitle>systemd-tpm2-swtpm.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
service.</para>
<para>In the initrd, the generator also orders
<citerefentry><refentrytitle>systemd-sysext-initrd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
and
<citerefentry><refentrytitle>systemd-confext-initrd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
after <filename>tpm2.target</filename> and
<citerefentry><refentrytitle>systemd-pcrphase-initrd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
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.</para>
<para><command>systemd-tpm2-generator</command> implements
<citerefentry><refentrytitle>systemd.generator</refentrytitle><manvolnum>7</manvolnum></citerefentry>.</para>
</refsect1>
+1 -3
View File
@@ -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();
+1
View File
@@ -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();
+1
View File
@@ -206,6 +206,7 @@ int load_volume_key_tpm2(
cd,
UINT32_MAX,
token,
LOG_ERR,
&hash_pcr_mask,
&pcr_bank,
&pubkey,
+1
View File
@@ -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,
+3 -1
View File
@@ -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();
+1
View File
@@ -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)
+1
View File
@@ -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();
+1
View File
@@ -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();
+4 -4
View File
@@ -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");
}
+1 -1
View File
@@ -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)
+1
View File
@@ -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();
+1
View File
@@ -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();
+1
View File
@@ -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();
+2
View File
@@ -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);
+1 -1
View File
@@ -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");
}
+1
View File
@@ -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();
+165 -5
View File
@@ -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
}
+6
View File
@@ -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,
+3 -2
View File
@@ -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,
+1 -1
View File
@@ -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);
+48 -8
View File
@@ -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)
+1
View File
@@ -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 {
+1
View File
@@ -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;
+2
View File
@@ -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();
+4 -2
View File
@@ -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();
+1
View File
@@ -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, "");
+1
View File
@@ -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();
+1 -3
View File
@@ -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();
+1
View File
@@ -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);
+1
View File
@@ -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);
+41 -1
View File
@@ -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;
+188
View File
@@ -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" <<EOF
[Partition]
Type=root
Format=ext4
CopyFiles=/usr/lib/os-release:/usr/lib/os-release
Encrypt=key-file
EOF
systemd-repart --definitions="$WORKDIR/defs" \
--empty=create \
--size=80M \
--dry-run=no \
--offline=no \
--key-file="$WORKDIR/passphrase" \
"$IMAGE"
systemd-dissect --validate --image-policy=root=encrypted "$IMAGE"
enroll_tpm2 "$IMAGE"
# 1. No passphrase at all: must be unlocked via the TPM2 token
dissect_mount
grep -q "Automatically discovered security TPM2 token unlocks volume" "$LOG"
grep -q "Completed TPM2 key unsealing" "$LOG"
# 2. systemd-sysext never asks for a passphrase, so for it the TPM2 token is the only way in. Build a
# system extension image the same way and merge it into a fake root.
mkdir -p "$WORKDIR/ext/usr/lib/extension-release.d" "$SYSEXT_ROOT/usr/lib" "$SYSEXT_ROOT/var/lib/extensions"
echo "ID=_any" >"$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" <<EOF
[Partition]
Type=root
Format=ext4
CopyFiles=$WORKDIR/ext:/
Encrypt=key-file
EOF
systemd-repart --definitions="$WORKDIR/defs" \
--empty=create \
--size=80M \
--dry-run=no \
--offline=no \
--key-file="$WORKDIR/passphrase" \
"$SYSEXT_ROOT/var/lib/extensions/$SYSEXT_NAME.raw"
enroll_tpm2 "$SYSEXT_ROOT/var/lib/extensions/$SYSEXT_NAME.raw"
systemd-sysext --root="$SYSEXT_ROOT" merge
test -f "$SYSEXT_ROOT/usr/lib/$SYSEXT_NAME-marker"
systemd-sysext --root="$SYSEXT_ROOT" unmerge
test ! -f "$SYSEXT_ROOT/usr/lib/$SYSEXT_NAME-marker"
# 3. Passphrase supplied: must be used exclusively, i.e. the TPM2 is never touched even though the token
# would match
PASSWORD=passphrase dissect_mount
(! grep -q "TPM2 key unsealing" "$LOG")
# 4. TPM2 unlocking turned off via the environment, no passphrase supplied: systemd-dissect must ask for
# one (satisfied via the credential) and not even look at the tokens
SYSTEMD_DISSECT_TPM2_UNLOCK=0 CREDENTIALS_DIRECTORY="$WORKDIR/creds" dissect_mount
(! grep -q "TPM2 token" "$LOG")
# 5. Invalidate the PCR policy: the token no longer matches, systemd-dissect must fall back to asking for a
# passphrase (satisfied via the credential)
tpm2_pcrextend 16:sha256=0000000000000000000000000000000000000000000000000000000000000000
CREDENTIALS_DIRECTORY="$WORKDIR/creds" dissect_mount
grep -q "TPM2 token 0 does not match current system state, skipping" "$LOG"
grep -q "Unable to unlock LUKS device via TPM2, and no passphrase supplied" "$LOG"
# 6. A token written by systemd-repart's Encrypt=tpm2 (not bound to any PCR) must be consumable, too
cat >"$WORKDIR/defs/root.conf" <<EOF
[Partition]
Type=root
Format=ext4
CopyFiles=/usr/lib/os-release:/usr/lib/os-release
Encrypt=tpm2
EOF
# Use --tpm2-public-key-pcrs= and --tpm2-pcrlock= to suppress auto-loading of the system PCR public key or
# pcrlock policy
systemd-repart --definitions="$WORKDIR/defs" \
--empty=create \
--size=80M \
--dry-run=no \
--offline=no \
--tpm2-device=auto \
--tpm2-public-key-pcrs= \
--tpm2-pcrlock= \
--tpm2-pcrs= \
"$IMAGE_REPART"
IMAGE="$IMAGE_REPART" dissect_mount
grep -q "Completed TPM2 key unsealing" "$LOG"
+2
View File
@@ -28,6 +28,8 @@ Conflicts=initrd-switch-root.target
Before=initrd-switch-root.target
Wants=modprobe@loop.service modprobe@dm_mod.service
After=modprobe@loop.service modprobe@dm_mod.service
# Extension images might be LUKS encrypted and sealed to the local TPM2, but ordering after tpm2.target here
# would be a cycle with the software TPM fallback, hence systemd-tpm2-generator adds it where possible
[Service]
Type=oneshot
+2
View File
@@ -23,6 +23,8 @@ Conflicts=shutdown.target
Before=initrd-root-fs.target shutdown.target
Wants=modprobe@loop.service modprobe@dm_mod.service
After=modprobe@loop.service modprobe@dm_mod.service sysroot.mount sysroot-usr.mount systemd-volatile-root.service
# Extension images might be LUKS encrypted and sealed to the local TPM2
After=tpm2.target
[Service]
Type=oneshot
+2
View File
@@ -21,6 +21,8 @@ ConditionKernelCommandLine=!systemd.confext=0
DefaultDependencies=no
After=local-fs.target
# Extension images might be LUKS encrypted and sealed to the local TPM2
After=tpm2.target
Before=sysinit.target systemd-tmpfiles-setup.service
Conflicts=shutdown.target
Before=shutdown.target
+4
View File
@@ -51,5 +51,9 @@ DeviceAllow=block-blkext rw
DeviceAllow=/dev/mapper/control rw
DeviceAllow=block-device-mapper rw
# nspawn unlocks LUKS encrypted images via a TPM2 token enrolled in them, in
# which case it needs access to the TPM2 resource manager device.
DeviceAllow=/dev/tpmrm0 rw
[Install]
WantedBy=machines.target
+2
View File
@@ -22,6 +22,8 @@ ConditionKernelCommandLine=!rd.systemd.sysext=0
DefaultDependencies=no
After=systemd-confext-initrd.service
# Extension images might be LUKS encrypted and sealed to the local TPM2, but ordering after tpm2.target here
# would be a cycle with the software TPM fallback, hence systemd-tpm2-generator adds it where possible
Before=local-fs-pre.target cryptsetup-pre.target systemd-tmpfiles-setup.service
Wants=local-fs-pre.target cryptsetup-pre.target
Conflicts=initrd-switch-root.target
+2
View File
@@ -23,6 +23,8 @@ Conflicts=shutdown.target
Before=initrd-root-fs.target shutdown.target
Wants=modprobe@loop.service modprobe@dm_mod.service
After=modprobe@loop.service modprobe@dm_mod.service sysroot.mount sysroot-usr.mount systemd-volatile-root.service
# Extension images might be LUKS encrypted and sealed to the local TPM2
After=tpm2.target
[Service]
Type=oneshot
+2
View File
@@ -22,6 +22,8 @@ ConditionKernelCommandLine=!systemd.sysext=0
DefaultDependencies=no
After=systemd-confext.service
After=local-fs.target
# Extension images might be LUKS encrypted and sealed to the local TPM2
After=tpm2.target
Before=sysinit.target systemd-tmpfiles-setup.service
Conflicts=shutdown.target
Before=shutdown.target