mirror of
https://github.com/systemd/systemd.git
synced 2026-11-06 05:17:24 +00:00
dissect-image: unlock LUKS partitions in DDIs via enrolled TPM2 tokens (#44132)
This commit is contained in:
44 files changed
+540
-35
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -206,6 +206,7 @@ int load_volume_key_tpm2(
|
||||
cd,
|
||||
UINT32_MAX,
|
||||
token,
|
||||
LOG_ERR,
|
||||
&hash_pcr_mask,
|
||||
&pcr_bank,
|
||||
&pubkey,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -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, "");
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Executable
+188
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user