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" <