From: Lennart Poettering Date: Thu, 28 May 2026 10:35:21 +0000 (+0200) Subject: cryptenroll: collect all enrollment parameters in an EnrollContext X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=39d08de81bdb401928cef029375b96cc466db9dc;p=thirdparty%2Fsystemd.git cryptenroll: collect all enrollment parameters in an EnrollContext Introduce an EnrollContext structure that carries everything the enrollment and unlocking helpers need, and route all enroll_*()/load_volume_key_*()/ wipe_slots() calls through it. The command line still populates the existing arg_* globals as before; once parsing is complete they are copied into a self-contained EnrollContext (which owns its strings/arrays) and the rest of the code only ever reads from the context. This is preparation for the upcoming varlinkification of systemd-cryptenroll: a Varlink dispatcher (and later an interactive first-boot wizard) can populate the very same EnrollContext without going through the arg_* parsing layer. To support non-interactive (e.g. Varlink) callers, the context carries an 'interactive' flag: when false, every credential prompt is disabled and the helpers fail with -ENOPKG (the established "querying disabled via headless" code) instead of blocking on a tty. Passwords, FIDO2 PINs and PKCS#11 PINs are all covered, and an optional FIDO2 PIN can be supplied directly via the context. enroll_recovery() additionally grows a quiet mode that returns the recovery key instead of printing it. This also adds one new field to EnrollContext which didn't exist before: the unlock_password is useful for the Varlink hookup later. No change in command line behaviour. --- diff --git a/src/cryptenroll/cryptenroll-fido2.c b/src/cryptenroll/cryptenroll-fido2.c index 600207e947b..7500e84c181 100644 --- a/src/cryptenroll/cryptenroll-fido2.c +++ b/src/cryptenroll/cryptenroll-fido2.c @@ -15,11 +15,9 @@ #include "string-util.h" int load_volume_key_fido2( + const EnrollContext *c, struct crypt_device *cd, - const char *cd_node, - const char *device, - void *ret_vk, - size_t *ret_vks) { + struct iovec *ret_vk) { #if HAVE_LIBFIDO2 _cleanup_(erase_and_freep) void *decrypted_key = NULL; @@ -28,19 +26,19 @@ int load_volume_key_fido2( ssize_t passphrase_size; int r; + assert_se(c); + assert_se(c->node); assert_se(cd); - assert_se(cd_node); assert_se(ret_vk); - assert_se(ret_vks); r = acquire_fido2_key_auto( cd, - cd_node, - cd_node, - device, + c->node, + c->node, + c->unlock_fido2_device, /* until= */ 0, "cryptenroll.fido2-pin", - ASK_PASSWORD_PUSH_CACHE|ASK_PASSWORD_ACCEPT_CACHED, + ASK_PASSWORD_PUSH_CACHE|ASK_PASSWORD_ACCEPT_CACHED|(c->interactive ? 0 : ASK_PASSWORD_HEADLESS), &decrypted_key, &decrypted_key_size); if (r == -EAGAIN) @@ -57,8 +55,8 @@ int load_volume_key_fido2( r = sym_crypt_volume_key_get( cd, CRYPT_ANY_SLOT, - ret_vk, - ret_vks, + ret_vk->iov_base, + &ret_vk->iov_len, passphrase, passphrase_size); if (r < 0) @@ -71,13 +69,9 @@ int load_volume_key_fido2( } int enroll_fido2( + const EnrollContext *c, struct crypt_device *cd, - const struct iovec *volume_key, - const char *device, - Fido2EnrollFlags lock_with, - int cred_alg, - const char *salt_file, - bool parameters_in_header) { + const struct iovec *volume_key) { #if HAVE_LIBFIDO2 _cleanup_(iovec_done_erase) struct iovec salt = {}; @@ -88,20 +82,22 @@ int enroll_fido2( size_t cid_size, secret_size; _cleanup_free_ void *cid = NULL; ssize_t base64_encoded_size; + Fido2EnrollFlags lock_with; /* receives the flags actually locked with, see below */ const char *node, *un; int r, keyslot; + assert_se(c); assert_se(cd); assert_se(iovec_is_set(volume_key)); - assert_se(device); + assert_se(c->fido2_device); assert_se(node = sym_crypt_get_device_name(cd)); un = strempty(sym_crypt_get_uuid(cd)); - if (salt_file) + if (c->fido2_salt_file) r = fido2_read_salt_file( - salt_file, + c->fido2_salt_file, /* offset= */ UINT64_MAX, /* client= */ "cryptenroll", /* node= */ un, @@ -112,7 +108,7 @@ int enroll_fido2( return r; r = fido2_generate_hmac_hash( - device, + c->fido2_device, /* rp_id= */ "io.systemd.cryptsetup", /* rp_name= */ "Encrypted Volume", /* user_id= */ un, strlen(un), /* We pass the user ID and name as the same: the disk's UUID if we have it */ @@ -121,8 +117,10 @@ int enroll_fido2( /* user_icon= */ NULL, /* askpw_icon= */ "drive-harddisk", /* askpw_credential= */ "cryptenroll.fido2-pin", - lock_with, - cred_alg, + c->interactive ? 0 : ASK_PASSWORD_HEADLESS, + c->fido2_pin, + c->fido2_lock_with, + c->fido2_cred_alg != 0 ? c->fido2_cred_alg : COSE_ES256, &salt, &cid, &cid_size, &secret, &secret_size, @@ -150,7 +148,7 @@ int enroll_fido2( if (keyslot < 0) return log_error_errno(keyslot, "Failed to add new FIDO2 key to %s: %m", node); - if (parameters_in_header) { + if (c->fido2_parameters_in_header) { if (asprintf(&keyslot_as_string, "%i", keyslot) < 0) return log_oom(); diff --git a/src/cryptenroll/cryptenroll-fido2.h b/src/cryptenroll/cryptenroll-fido2.h index 7d0c25c02a9..c5d57f12dba 100644 --- a/src/cryptenroll/cryptenroll-fido2.h +++ b/src/cryptenroll/cryptenroll-fido2.h @@ -1,7 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "cryptenroll.h" #include "shared-forward.h" -int load_volume_key_fido2(struct crypt_device *cd, const char *cd_node, const char *device, void *ret_vk, size_t *ret_vks); -int enroll_fido2(struct crypt_device *cd, const struct iovec *volume_key, const char *device, Fido2EnrollFlags lock_with, int cred_alg, const char *salt_file, bool parameters_in_header); +int load_volume_key_fido2(const EnrollContext *c, struct crypt_device *cd, struct iovec *ret_vk); +int enroll_fido2(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key); diff --git a/src/cryptenroll/cryptenroll-password.c b/src/cryptenroll/cryptenroll-password.c index e189cce23ab..6e78d6ab2fe 100644 --- a/src/cryptenroll/cryptenroll-password.c +++ b/src/cryptenroll/cryptenroll-password.c @@ -14,18 +14,17 @@ #include "strv.h" int load_volume_key_password( + const EnrollContext *c, struct crypt_device *cd, - const char *cd_node, - void *ret_vk, - size_t *ret_vks) { + struct iovec *ret_vk) { _cleanup_(erase_and_freep) char *envpw = NULL; int r; + assert_se(c); + assert_se(c->node); assert_se(cd); - assert_se(cd_node); assert_se(ret_vk); - assert_se(ret_vks); r = getenv_steal_erase("PASSWORD", &envpw); if (r < 0) @@ -34,22 +33,27 @@ int load_volume_key_password( r = sym_crypt_volume_key_get( cd, CRYPT_ANY_SLOT, - ret_vk, - ret_vks, + ret_vk->iov_base, + &ret_vk->iov_len, envpw, strlen(envpw)); if (r < 0) return log_error_errno(r, "Password from environment variable $PASSWORD did not work: %m"); } else { AskPasswordFlags ask_password_flags = ASK_PASSWORD_PUSH_CACHE|ASK_PASSWORD_ACCEPT_CACHED; + + if (!c->interactive) + return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), + "Password querying disabled via 'headless' option, but no password provided for disk %s.", + c->node); _cleanup_free_ char *question = NULL, *id = NULL, *disk_path = NULL; unsigned i = 5; - question = strjoin("Please enter current passphrase for disk ", cd_node, ":"); + question = strjoin("Please enter current passphrase for disk ", c->node, ":"); if (!question) return log_oom(); - disk_path = cescape(cd_node); + disk_path = cescape(c->node); if (!disk_path) return log_oom(); @@ -84,8 +88,8 @@ int load_volume_key_password( r = sym_crypt_volume_key_get( cd, CRYPT_ANY_SLOT, - ret_vk, - ret_vks, + ret_vk->iov_base, + &ret_vk->iov_len, *p, strlen(*p)); if (r >= 0) @@ -103,6 +107,7 @@ int load_volume_key_password( } int enroll_password( + const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key) { @@ -111,18 +116,30 @@ int enroll_password( const char *node; int r, keyslot; + assert(c); assert(cd); assert(iovec_is_set(volume_key)); assert_se(node = sym_crypt_get_device_name(cd)); - r = getenv_steal_erase("NEWPASSWORD", &new_password); - if (r < 0) - return log_error_errno(r, "Failed to acquire password from environment: %m"); - if (r == 0) { + if (c->passphrase) { + new_password = memdup_suffix0(c->passphrase, c->passphrase_size); + if (!new_password) + return log_oom(); + } else { + r = getenv_steal_erase("NEWPASSWORD", &new_password); + if (r < 0) + return log_error_errno(r, "Failed to acquire password from environment: %m"); + } + + if (!new_password) { _cleanup_free_ char *disk_path = NULL, *id = NULL; unsigned i = 5; + if (!c->interactive) + return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), + "Password querying disabled via 'headless' option, but no new password provided."); + assert_se(node = sym_crypt_get_device_name(cd)); (void) suggest_passwords(); diff --git a/src/cryptenroll/cryptenroll-password.h b/src/cryptenroll/cryptenroll-password.h index f0c97ced298..897a8a27910 100644 --- a/src/cryptenroll/cryptenroll-password.h +++ b/src/cryptenroll/cryptenroll-password.h @@ -1,7 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "cryptenroll.h" #include "shared-forward.h" -int load_volume_key_password(struct crypt_device *cd, const char* cd_node, void *ret_vk, size_t *ret_vks); -int enroll_password(struct crypt_device *cd, const struct iovec *volume_key); +int load_volume_key_password(const EnrollContext *c, struct crypt_device *cd, struct iovec *ret_vk); +int enroll_password(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key); diff --git a/src/cryptenroll/cryptenroll-pkcs11.c b/src/cryptenroll/cryptenroll-pkcs11.c index 13feda47c4f..c46fcca10a1 100644 --- a/src/cryptenroll/cryptenroll-pkcs11.c +++ b/src/cryptenroll/cryptenroll-pkcs11.c @@ -36,7 +36,7 @@ static int uri_set_private_class(const char *uri, char **ret_uri) { } #endif -int enroll_pkcs11(struct crypt_device *cd, const struct iovec *volume_key,const char *uri) { +int enroll_pkcs11(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key) { #if HAVE_P11KIT && HAVE_OPENSSL _cleanup_(erase_and_freep) void *decrypted_key = NULL; _cleanup_(erase_and_freep) char *base64_encoded = NULL; @@ -50,18 +50,19 @@ int enroll_pkcs11(struct crypt_device *cd, const struct iovec *volume_key,const const char *node; int r; + assert_se(c); assert_se(cd); assert_se(iovec_is_set(volume_key)); - assert_se(uri); + assert_se(c->pkcs11_token_uri); assert_se(node = sym_crypt_get_device_name(cd)); r = pkcs11_acquire_public_key( - uri, + c->pkcs11_token_uri, "volume enrollment operation", "drive-harddisk", "cryptenroll.pkcs11-pin", - /* askpw_flags= */ 0, + c->interactive ? 0 : ASK_PASSWORD_HEADLESS, &pkey, &rsa_padding, /* ret_pin_used= */ NULL); @@ -100,14 +101,14 @@ int enroll_pkcs11(struct crypt_device *cd, const struct iovec *volume_key,const /* Change 'type=cert' or 'type=public' in the provided URI to 'type=private' before storing in a LUKS2 header. This allows users to use output of some PKCS#11 tools directly without modifications. */ - r = uri_set_private_class(uri, &private_uri); + r = uri_set_private_class(c->pkcs11_token_uri, &private_uri); if (r < 0) return r; r = sd_json_buildo(&v, SD_JSON_BUILD_PAIR("type", JSON_BUILD_CONST_STRING("systemd-pkcs11")), SD_JSON_BUILD_PAIR("keyslots", SD_JSON_BUILD_ARRAY(SD_JSON_BUILD_STRING(keyslot_as_string))), - SD_JSON_BUILD_PAIR_STRING("pkcs11-uri", private_uri ?: uri), + SD_JSON_BUILD_PAIR_STRING("pkcs11-uri", private_uri ?: c->pkcs11_token_uri), SD_JSON_BUILD_PAIR_BASE64("pkcs11-key", saved_key, saved_key_size), SD_JSON_BUILD_PAIR_CONDITION(rsa_padding > PKCS11_RSA_PADDING_PKCS1V15, "pkcs11-padding", SD_JSON_BUILD_STRING(pkcs11_rsa_padding_to_string(rsa_padding)))); diff --git a/src/cryptenroll/cryptenroll-pkcs11.h b/src/cryptenroll/cryptenroll-pkcs11.h index b3e6e4584af..614c6d466cd 100644 --- a/src/cryptenroll/cryptenroll-pkcs11.h +++ b/src/cryptenroll/cryptenroll-pkcs11.h @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "cryptenroll.h" #include "shared-forward.h" -int enroll_pkcs11(struct crypt_device *cd, const struct iovec *volume_key, const char *uri); +int enroll_pkcs11(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key); diff --git a/src/cryptenroll/cryptenroll-recovery.c b/src/cryptenroll/cryptenroll-recovery.c index 85ec13f1282..da7b7a1018d 100644 --- a/src/cryptenroll/cryptenroll-recovery.c +++ b/src/cryptenroll/cryptenroll-recovery.c @@ -12,8 +12,10 @@ #include "recovery-key.h" int enroll_recovery( + const EnrollContext *c, struct crypt_device *cd, - const struct iovec *volume_key) { + const struct iovec *volume_key, + char **ret_recovery_key) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; _cleanup_(erase_and_freep) char *password = NULL; @@ -21,6 +23,7 @@ int enroll_recovery( int keyslot, r, q; const char *node; + assert_se(c); assert_se(cd); assert_se(iovec_is_set(volume_key)); @@ -44,31 +47,33 @@ int enroll_recovery( if (keyslot < 0) return log_error_errno(keyslot, "Failed to add new recovery key to %s: %m", node); - fflush(stdout); - fprintf(stderr, - "A secret recovery key has been generated for this volume:\n\n" - " %s%s%s", - emoji_enabled() ? glyph(GLYPH_LOCK_AND_KEY) : "", - emoji_enabled() ? " " : "", - ansi_highlight()); - fflush(stderr); + if (c->interactive) { + fflush(stdout); + fprintf(stderr, + "A secret recovery key has been generated for this volume:\n\n" + " %s%s%s", + emoji_enabled() ? glyph(GLYPH_LOCK_AND_KEY) : "", + emoji_enabled() ? " " : "", + ansi_highlight()); + fflush(stderr); - fputs(password, stdout); - fflush(stdout); + fputs(password, stdout); + fflush(stdout); - fputs(ansi_normal(), stderr); - fflush(stderr); + fputs(ansi_normal(), stderr); + fflush(stderr); - fputc('\n', stdout); - fflush(stdout); + fputc('\n', stdout); + fflush(stdout); - fputs("\nPlease save this secret recovery key at a secure location. It may be used to\n" - "regain access to the volume if the other configured access credentials have\n" - "been lost or forgotten. The recovery key may be entered in place of a password\n" - "whenever authentication is requested.\n", stderr); - fflush(stderr); + fputs("\nPlease save this secret recovery key at a secure location. It may be used to\n" + "regain access to the volume if the other configured access credentials have\n" + "been lost or forgotten. The recovery key may be entered in place of a password\n" + "whenever authentication is requested.\n", stderr); + fflush(stderr); - (void) print_qrcode(stderr, "Optionally scan the recovery key for safekeeping", password); + (void) print_qrcode(stderr, "Optionally scan the recovery key for safekeeping", password); + } if (asprintf(&keyslot_as_string, "%i", keyslot) < 0) { r = log_oom(); @@ -89,7 +94,12 @@ int enroll_recovery( goto rollback; } - log_info("New recovery key enrolled as key slot %i.", keyslot); + if (c->interactive) + log_info("New recovery key enrolled as key slot %i.", keyslot); + + if (ret_recovery_key) + *ret_recovery_key = TAKE_PTR(password); + return keyslot; rollback: diff --git a/src/cryptenroll/cryptenroll-recovery.h b/src/cryptenroll/cryptenroll-recovery.h index 0c76d86108a..1655f714453 100644 --- a/src/cryptenroll/cryptenroll-recovery.h +++ b/src/cryptenroll/cryptenroll-recovery.h @@ -1,6 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "cryptenroll.h" #include "shared-forward.h" -int enroll_recovery(struct crypt_device *cd, const struct iovec *volume_key); +/* When c->interactive is unset, the generated recovery key is returned via ret_recovery_key + * instead of being printed to stdout/stderr + rendered as a QR code. */ +int enroll_recovery(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key, char **ret_recovery_key); diff --git a/src/cryptenroll/cryptenroll-tpm2.c b/src/cryptenroll/cryptenroll-tpm2.c index 2f6c7ecd728..eb08abdec6a 100644 --- a/src/cryptenroll/cryptenroll-tpm2.c +++ b/src/cryptenroll/cryptenroll-tpm2.c @@ -169,11 +169,9 @@ static int get_pin(char **ret_pin_str, TPM2Flags *ret_flags) { #endif int load_volume_key_tpm2( + const EnrollContext *c, struct crypt_device *cd, - const char *cd_node, - const char *device, - void *ret_vk, - size_t *ret_vks) { + struct iovec *ret_vk) { #if HAVE_TPM2 _cleanup_(iovec_done_erase) struct iovec decrypted_key = {}; @@ -181,10 +179,10 @@ int load_volume_key_tpm2( ssize_t passphrase_size; int r; + assert_se(c); + assert_se(c->node); assert_se(cd); - assert_se(cd_node); assert_se(ret_vk); - assert_se(ret_vks); bool found_some = false; int token = 0; /* first token to look at */ @@ -235,8 +233,8 @@ int load_volume_key_tpm2( found_some = true; r = acquire_tpm2_key( - cd_node, - device, + c->node, + c->unlock_tpm2_device, hash_pcr_mask, pcr_bank, &pubkey, @@ -255,7 +253,7 @@ int load_volume_key_tpm2( tpm2_flags, /* until= */ 0, "cryptenroll.tpm2-pin", - /* askpw_flags= */ 0, + c->interactive ? 0 : ASK_PASSWORD_HEADLESS, &decrypted_key); if (IN_SET(r, -EACCES, -ENOLCK)) return log_notice_errno(SYNTHETIC_ERRNO(EAGAIN), "TPM2 PIN unlock failed"); @@ -275,8 +273,8 @@ int load_volume_key_tpm2( r = sym_crypt_volume_key_get( cd, CRYPT_ANY_SLOT, - ret_vk, - ret_vks, + ret_vk->iov_base, + &ret_vk->iov_len, passphrase, passphrase_size); if (r < 0) @@ -288,19 +286,9 @@ int load_volume_key_tpm2( #endif } -int enroll_tpm2(struct crypt_device *cd, +int enroll_tpm2(const EnrollContext *c, + struct crypt_device *cd, const struct iovec *volume_key, - const char *device, - uint32_t seal_key_handle, - const char *device_key, - Tpm2PCRValue *hash_pcr_values, - size_t n_hash_pcr_values, - const char *pcr_pubkey_path, - bool load_pcr_pubkey, - uint32_t pubkey_pcr_mask, - const char *signature_path, - bool use_pin, - const char *pcrlock_path, int *ret_slot_to_wipe) { #if HAVE_TPM2 @@ -314,6 +302,8 @@ int enroll_tpm2(struct crypt_device *cd, int r, keyslot, slot_to_wipe = -1; TPM2Flags flags = 0; uint16_t primary_alg = 0; + /* Mutable copy: cleared on the no-public-key fallback paths below. */ + uint32_t pubkey_pcr_mask = c->tpm2_public_key_pcr_mask; uint8_t binary_salt[SHA256_DIGEST_SIZE] = {}; /* * erase the salt, we'd rather attempt to not have this in a coredump @@ -323,15 +313,16 @@ int enroll_tpm2(struct crypt_device *cd, */ CLEANUP_ERASE(binary_salt); + assert(c); assert(cd); assert(iovec_is_set(volume_key)); - assert(tpm2_pcr_values_valid(hash_pcr_values, n_hash_pcr_values)); - assert(TPM2_PCR_MASK_VALID(pubkey_pcr_mask)); + assert(tpm2_pcr_values_valid(c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values)); + assert(TPM2_PCR_MASK_VALID(c->tpm2_public_key_pcr_mask)); assert(ret_slot_to_wipe); assert_se(node = sym_crypt_get_device_name(cd)); - if (use_pin) { + if (c->tpm2_pin) { r = get_pin(&pin_str, &flags); if (r < 0) return r; @@ -354,10 +345,10 @@ int enroll_tpm2(struct crypt_device *cd, } TPM2B_PUBLIC public = {}; - if (pcr_pubkey_path || load_pcr_pubkey) { - r = tpm2_load_pcr_public_key(pcr_pubkey_path, &pubkey.iov_base, &pubkey.iov_len); + if (c->tpm2_public_key || c->tpm2_load_public_key) { + r = tpm2_load_pcr_public_key(c->tpm2_public_key, &pubkey.iov_base, &pubkey.iov_len); if (r < 0) { - if (pcr_pubkey_path || signature_path || r != -ENOENT) + if (c->tpm2_public_key || c->tpm2_signature || r != -ENOENT) return log_error_errno(r, "Failed to read TPM PCR public key: %m"); log_debug_errno(r, "Failed to read TPM2 PCR public key, proceeding without: %m"); @@ -367,11 +358,11 @@ int enroll_tpm2(struct crypt_device *cd, if (r < 0) return log_error_errno(r, "Could not convert public key to TPM2B_PUBLIC: %m"); - if (signature_path) { + if (c->tpm2_signature) { /* Also try to load the signature JSON object, to verify that our enrollment will work. * This is optional however, skip it if it's not explicitly provided. */ - r = tpm2_load_pcr_signature(signature_path, &signature_json); + r = tpm2_load_pcr_signature(c->tpm2_signature, &signature_json); if (r < 0) return log_error_errno(r, "Failed to read TPM PCR signature: %m"); } @@ -379,15 +370,15 @@ int enroll_tpm2(struct crypt_device *cd, } else pubkey_pcr_mask = 0; - bool any_pcr_value_specified = tpm2_pcr_values_has_any_values(hash_pcr_values, n_hash_pcr_values); + bool any_pcr_value_specified = tpm2_pcr_values_has_any_values(c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values); _cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy pcrlock_policy = {}; - if (pcrlock_path) { - r = tpm2_pcrlock_policy_load(pcrlock_path, &pcrlock_policy); + if (c->tpm2_pcrlock) { + r = tpm2_pcrlock_policy_load(c->tpm2_pcrlock, &pcrlock_policy); if (r < 0) return r; if (r == 0) - return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Couldn't find pcrlock policy %s.", pcrlock_path); + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Couldn't find pcrlock policy %s.", c->tpm2_pcrlock); any_pcr_value_specified = true; flags |= TPM2_FLAGS_USE_PCRLOCK; @@ -395,23 +386,23 @@ int enroll_tpm2(struct crypt_device *cd, _cleanup_(tpm2_context_unrefp) Tpm2Context *tpm2_context = NULL; TPM2B_PUBLIC device_key_public = {}; - if (device_key) { - r = tpm2_load_public_key_file(device_key, &device_key_public); + if (c->tpm2_device_key) { + r = tpm2_load_public_key_file(c->tpm2_device_key, &device_key_public); if (r < 0) return r; - if (!tpm2_pcr_values_has_all_values(hash_pcr_values, n_hash_pcr_values)) + if (!tpm2_pcr_values_has_all_values(c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Must provide all PCR values when using TPM2 device key."); primary_alg = device_key_public.publicArea.type; } else { - r = tpm2_context_new_or_warn(device, &tpm2_context); + r = tpm2_context_new_or_warn(c->tpm2_device, &tpm2_context); if (r < 0) return r; - if (!tpm2_pcr_values_has_all_values(hash_pcr_values, n_hash_pcr_values)) { - r = tpm2_pcr_read_missing_values(tpm2_context, hash_pcr_values, n_hash_pcr_values); + if (!tpm2_pcr_values_has_all_values(c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values)) { + r = tpm2_pcr_read_missing_values(tpm2_context, c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values); if (r < 0) return log_error_errno(r, "Could not read pcr values: %m"); } @@ -420,10 +411,10 @@ int enroll_tpm2(struct crypt_device *cd, uint16_t hash_pcr_bank = 0; uint32_t hash_pcr_mask = 0; - if (n_hash_pcr_values > 0) { + if (c->tpm2_n_hash_pcr_values > 0) { size_t hash_count; - r = tpm2_pcr_values_hash_count(hash_pcr_values, n_hash_pcr_values, &hash_count); + r = tpm2_pcr_values_hash_count(c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values, &hash_count); if (r < 0) return log_error_errno(r, "Could not get hash count: %m"); @@ -431,12 +422,12 @@ int enroll_tpm2(struct crypt_device *cd, return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Multiple PCR banks selected."); /* If we use a literal PCR value policy, derive the bank to use from the algorithm specified on the hash values */ - hash_pcr_bank = hash_pcr_values[0].hash; - r = tpm2_pcr_values_to_mask(hash_pcr_values, n_hash_pcr_values, hash_pcr_bank, &hash_pcr_mask); + hash_pcr_bank = c->tpm2_hash_pcr_values[0].hash; + r = tpm2_pcr_values_to_mask(c->tpm2_hash_pcr_values, c->tpm2_n_hash_pcr_values, hash_pcr_bank, &hash_pcr_mask); if (r < 0) return log_error_errno(r, "Could not get hash mask: %m"); - } else if (pubkey_pcr_mask != 0 && !device_key) { + } else if (pubkey_pcr_mask != 0 && !c->tpm2_device_key) { /* If no literal PCR value policy is used, then let's determine the mask to use automatically * from the measurements of the TPM. */ @@ -461,21 +452,21 @@ int enroll_tpm2(struct crypt_device *cd, /* If both PCR public key unlock and pcrlock unlock is selected, then we create the one for PCR public key unlock first. */ r = tpm2_calculate_sealing_policy( - hash_pcr_values, - n_hash_pcr_values, + c->tpm2_hash_pcr_values, + c->tpm2_n_hash_pcr_values, iovec_is_set(&pubkey) ? &public : NULL, - use_pin, - pcrlock_path && !iovec_is_set(&pubkey) ? &pcrlock_policy : NULL, + c->tpm2_pin, + c->tpm2_pcrlock && !iovec_is_set(&pubkey) ? &pcrlock_policy : NULL, policy_hash + 0); if (r < 0) return r; - if (pcrlock_path && iovec_is_set(&pubkey)) { + if (c->tpm2_pcrlock && iovec_is_set(&pubkey)) { r = tpm2_calculate_sealing_policy( - hash_pcr_values, - n_hash_pcr_values, + c->tpm2_hash_pcr_values, + c->tpm2_n_hash_pcr_values, /* public= */ NULL, /* This one is off now */ - use_pin, + c->tpm2_pin, &pcrlock_policy, /* And this one on instead. */ policy_hash + 1); if (r < 0) @@ -488,7 +479,7 @@ int enroll_tpm2(struct crypt_device *cd, size_t n_blobs = 0; CLEANUP_ARRAY(blobs, n_blobs, iovec_array_free); - if (device_key) { + if (c->tpm2_device_key) { if (n_policy_hash > 1) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Combined signed PCR policies and pcrlock policies cannot be calculated offline, currently."); @@ -500,7 +491,7 @@ int enroll_tpm2(struct crypt_device *cd, n_blobs = 1; r = tpm2_calculate_seal( - seal_key_handle, + c->tpm2_seal_key_handle, &device_key_public, /* attributes= */ NULL, /* secret= */ NULL, @@ -511,7 +502,7 @@ int enroll_tpm2(struct crypt_device *cd, &srk); } else r = tpm2_seal(tpm2_context, - seal_key_handle, + c->tpm2_seal_key_handle, policy_hash, n_policy_hash, pin_str, @@ -534,7 +525,7 @@ int enroll_tpm2(struct crypt_device *cd, log_debug_errno(r, "PCR policy hash not yet enrolled, enrolling now."); else if (r < 0) return r; - else if (use_pin) { + else if (c->tpm2_pin) { log_debug("This PCR set is already enrolled, re-enrolling anyway to update PIN."); slot_to_wipe = r; } else { @@ -544,7 +535,7 @@ int enroll_tpm2(struct crypt_device *cd, } /* If possible, verify the sealed data object. */ - if ((!iovec_is_set(&pubkey) || signature_json) && !any_pcr_value_specified && !device_key) { + if ((!iovec_is_set(&pubkey) || signature_json) && !any_pcr_value_specified && !c->tpm2_device_key) { _cleanup_(iovec_done_erase) struct iovec secret2 = {}; log_debug("Unsealing for verification..."); @@ -555,7 +546,7 @@ int enroll_tpm2(struct crypt_device *cd, pubkey_pcr_mask, signature_json, pin_str, - pcrlock_path ? &pcrlock_policy : NULL, + c->tpm2_pcrlock ? &pcrlock_policy : NULL, primary_alg, blobs, n_blobs, @@ -600,9 +591,9 @@ int enroll_tpm2(struct crypt_device *cd, n_blobs, policy_hash_as_iovec, n_policy_hash, - use_pin ? &IOVEC_MAKE(binary_salt, sizeof(binary_salt)) : NULL, + c->tpm2_pin ? &IOVEC_MAKE(binary_salt, sizeof(binary_salt)) : NULL, &srk, - pcrlock_path ? &pcrlock_policy.nv_handle : NULL, + c->tpm2_pcrlock ? &pcrlock_policy.nv_handle : NULL, flags, &v); if (r < 0) diff --git a/src/cryptenroll/cryptenroll-tpm2.h b/src/cryptenroll/cryptenroll-tpm2.h index 48f6c12b7f1..3d57b8f8f38 100644 --- a/src/cryptenroll/cryptenroll-tpm2.h +++ b/src/cryptenroll/cryptenroll-tpm2.h @@ -1,7 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "cryptenroll.h" #include "shared-forward.h" -int load_volume_key_tpm2(struct crypt_device *cd, const char *cd_node, const char *device, void *ret_vk, size_t *ret_vks); -int enroll_tpm2(struct crypt_device *cd, const struct iovec *volume_key, const char *device, uint32_t seal_key_handle, const char *device_key, Tpm2PCRValue *hash_pcr_values, size_t n_hash_pcr_values, const char *pubkey_path, bool load_pcr_pubkey, uint32_t pubkey_pcr_mask, const char *signature_path, bool use_pin, const char *pcrlock_path, int *ret_slot_to_wipe); +int load_volume_key_tpm2(const EnrollContext *c, struct crypt_device *cd, struct iovec *ret_vk); +int enroll_tpm2(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key, int *ret_slot_to_wipe); diff --git a/src/cryptenroll/cryptenroll-wipe.c b/src/cryptenroll/cryptenroll-wipe.c index a8638d2b3a7..b7d554c3904 100644 --- a/src/cryptenroll/cryptenroll-wipe.c +++ b/src/cryptenroll/cryptenroll-wipe.c @@ -296,12 +296,8 @@ static bool slots_remain(struct crypt_device *cd, Set *wipe_slots, Set *keep_slo return false; } -int wipe_slots(struct crypt_device *cd, - const int explicit_slots[], - size_t n_explicit_slots, - WipeScope by_scope, - unsigned by_mask, - int except_slot) { +int wipe_slots(const EnrollContext *c, + struct crypt_device *cd) { _cleanup_set_free_ Set *wipe_slots = NULL, *wipe_tokens = NULL, *keep_slots = NULL; _cleanup_free_ int *ordered_slots = NULL, *ordered_tokens = NULL; @@ -309,10 +305,11 @@ int wipe_slots(struct crypt_device *cd, int r, slot_max, ret; void *e; + assert_se(c); assert_se(cd); /* Shortcut if nothing to wipe. */ - if (n_explicit_slots == 0 && by_mask == 0 && by_scope == WIPE_EXPLICIT) + if (c->n_wipe_slots == 0 && c->wipe_slots_mask == 0 && c->wipe_slots_scope == WIPE_EXPLICIT) return 0; /* So this is a bit more complicated than I'd wish, but we want support three different axis for wiping slots: @@ -334,23 +331,23 @@ int wipe_slots(struct crypt_device *cd, return log_oom(); /* Let's maintain one set of slots for the slots we definitely want to keep */ - if (except_slot >= 0) - if (set_put(keep_slots, INT_TO_PTR(except_slot)) < 0) + if (c->wipe_except_slot >= 0) + if (set_put(keep_slots, INT_TO_PTR(c->wipe_except_slot)) < 0) return log_oom(); assert_se((slot_max = sym_crypt_keyslot_max(CRYPT_LUKS2)) > 0); /* Maintain another set of the slots we intend to wipe */ - for (size_t i = 0; i < n_explicit_slots; i++) { - if (explicit_slots[i] >= slot_max) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Slot index %i out of range.", explicit_slots[i]); + for (size_t i = 0; i < c->n_wipe_slots; i++) { + if (c->wipe_slots[i] >= slot_max) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Slot index %i out of range.", c->wipe_slots[i]); - if (set_put(wipe_slots, INT_TO_PTR(explicit_slots[i])) < 0) + if (set_put(wipe_slots, INT_TO_PTR(c->wipe_slots[i])) < 0) return log_oom(); } /* Now, handle the "all" and "empty passphrase" cases. */ - switch (by_scope) { + switch (c->wipe_slots_scope) { case WIPE_EXPLICIT: break; /* Nothing to do here */ @@ -373,7 +370,7 @@ int wipe_slots(struct crypt_device *cd, } /* Then add all slots that match a token type */ - r = find_slots_by_mask(cd, wipe_slots, keep_slots, by_mask); + r = find_slots_by_mask(cd, wipe_slots, keep_slots, c->wipe_slots_mask); if (r < 0) return r; @@ -409,7 +406,7 @@ int wipe_slots(struct crypt_device *cd, typesafe_qsort(ordered_tokens, n_ordered_tokens, cmp_int); if (n_ordered_slots == 0 && n_ordered_tokens == 0) { - log_full(except_slot < 0 ? LOG_NOTICE : LOG_DEBUG, + log_full(c->wipe_except_slot < 0 ? LOG_NOTICE : LOG_DEBUG, "No slots to remove selected."); return 0; } diff --git a/src/cryptenroll/cryptenroll-wipe.h b/src/cryptenroll/cryptenroll-wipe.h index aa82f9dbbd3..4e715191818 100644 --- a/src/cryptenroll/cryptenroll-wipe.h +++ b/src/cryptenroll/cryptenroll-wipe.h @@ -1,13 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "cryptenroll.h" #include "shared-forward.h" -typedef enum WipeScope WipeScope; - -int wipe_slots(struct crypt_device *cd, - const int explicit_slots[], - size_t n_explicit_slots, - WipeScope by_scope, - unsigned by_mask, - int except_slot); +/* Wipes the slots selected by c->wipe_slots / c->n_wipe_slots / c->wipe_slots_scope / + * c->wipe_slots_mask, except for c->wipe_except_slot (set to -1 for none). */ +int wipe_slots(const EnrollContext *c, struct crypt_device *cd); diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index eed0a11aeba..0099c3c4a5c 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -3,7 +3,9 @@ #include #include "sd-device.h" +#include "sd-varlink.h" +#include "alloc-util.h" #include "blockdev-list.h" #include "blockdev-util.h" #include "build.h" @@ -22,6 +24,7 @@ #include "libfido2-util.h" #include "log.h" #include "main-func.h" +#include "memory-util.h" #include "options.h" #include "pager.h" #include "parse-argument.h" @@ -110,6 +113,29 @@ static const char *const luks2_token_type_table[_ENROLL_TYPE_MAX] = { DEFINE_STRING_TABLE_LOOKUP(luks2_token_type, EnrollType); +void enroll_context_done(EnrollContext *c) { + if (!c) + return; + + c->node = mfree(c->node); + c->unlock_keyfile = mfree(c->unlock_keyfile); + c->unlock_fido2_device = mfree(c->unlock_fido2_device); + c->unlock_tpm2_device = mfree(c->unlock_tpm2_device); + c->passphrase = erase_and_free(c->passphrase); + c->fido2_device = mfree(c->fido2_device); + c->fido2_salt_file = mfree(c->fido2_salt_file); + c->fido2_pin = erase_and_free(c->fido2_pin); + c->pkcs11_token_uri = mfree(c->pkcs11_token_uri); + c->tpm2_device = mfree(c->tpm2_device); + c->tpm2_device_key = mfree(c->tpm2_device_key); + c->tpm2_hash_pcr_values = mfree(c->tpm2_hash_pcr_values); + c->tpm2_public_key = mfree(c->tpm2_public_key); + c->tpm2_signature = mfree(c->tpm2_signature); + c->tpm2_pcrlock = mfree(c->tpm2_pcrlock); + c->wipe_slots = mfree(c->wipe_slots); + c->link = sd_varlink_unref(c->link); +} + static int determine_default_node(void) { int r; @@ -661,21 +687,21 @@ static int check_for_homed(struct crypt_device *cd) { } static int load_volume_key_keyfile( + const EnrollContext *c, struct crypt_device *cd, - void *ret_vk, - size_t *ret_vks) { + struct iovec *ret_vk) { _cleanup_(erase_and_freep) char *password = NULL; size_t password_len; int r; + assert_se(c); assert_se(cd); assert_se(ret_vk); - assert_se(ret_vks); r = read_full_file_full( AT_FDCWD, - arg_unlock_keyfile, + c->unlock_keyfile, UINT64_MAX, SIZE_MAX, READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET, @@ -683,13 +709,13 @@ static int load_volume_key_keyfile( &password, &password_len); if (r < 0) - return log_error_errno(r, "Reading keyfile %s failed: %m", arg_unlock_keyfile); + return log_error_errno(r, "Reading keyfile %s failed: %m", c->unlock_keyfile); r = sym_crypt_volume_key_get( cd, CRYPT_ANY_SLOT, - ret_vk, - ret_vks, + ret_vk->iov_base, + &ret_vk->iov_len, password, password_len); if (r < 0) @@ -699,15 +725,17 @@ static int load_volume_key_keyfile( } static int prepare_luks( + const EnrollContext *c, struct crypt_device **ret_cd, struct iovec *ret_volume_key) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; int r; + assert(c); assert(ret_cd); - r = sym_crypt_init(&cd, arg_node); + r = sym_crypt_init(&cd, c->node); if (r < 0) return log_error_errno(r, "Failed to allocate libcryptsetup context: %m"); @@ -715,7 +743,7 @@ static int prepare_luks( r = sym_crypt_load(cd, CRYPT_LUKS2, NULL); if (r < 0) - return log_error_errno(r, "Failed to load LUKS2 superblock of %s: %m", arg_node); + return log_error_errno(r, "Failed to load LUKS2 superblock of %s: %m", c->node); r = check_for_homed(cd); if (r < 0) @@ -731,35 +759,33 @@ static int prepare_luks( return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size"); _cleanup_(iovec_done_erase) struct iovec vk = {}; - vk.iov_base = malloc(r); if (!vk.iov_base) return log_oom(); vk.iov_len = (size_t) r; - switch (arg_unlock_type) { + switch (c->unlock_type) { case UNLOCK_PASSWORD: - r = load_volume_key_password(cd, arg_node, vk.iov_base, &vk.iov_len); + r = load_volume_key_password(c, cd, &vk); break; case UNLOCK_KEYFILE: - r = load_volume_key_keyfile(cd, vk.iov_base, &vk.iov_len); + r = load_volume_key_keyfile(c, cd, &vk); break; case UNLOCK_FIDO2: - r = load_volume_key_fido2(cd, arg_node, arg_unlock_fido2_device, vk.iov_base, &vk.iov_len); + r = load_volume_key_fido2(c, cd, &vk); break; case UNLOCK_TPM2: - r = load_volume_key_tpm2(cd, arg_node, arg_unlock_tpm2_device, vk.iov_base, &vk.iov_len); + r = load_volume_key_tpm2(c, cd, &vk); break; default: return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown LUKS unlock method"); } - if (r < 0) return r; @@ -769,9 +795,62 @@ static int prepare_luks( return 0; } +static int enroll_context_from_args(EnrollContext *c) { + assert(c); + + /* Copies the parsed command line parameters from the static arg_* globals into a self-contained + * EnrollContext. The context owns its own copies of all strings/arrays, so it can be torn down + * independently of the arg_* destructors. */ + + *c = ENROLL_CONTEXT_NULL; + + c->enroll_type = arg_enroll_type; + c->unlock_type = arg_unlock_type; + c->fido2_parameters_in_header = arg_fido2_parameters_in_header; + c->fido2_lock_with = arg_fido2_lock_with; + c->fido2_cred_alg = arg_fido2_cred_alg; + c->tpm2_seal_key_handle = arg_tpm2_seal_key_handle; + c->tpm2_pin = arg_tpm2_pin; + c->tpm2_load_public_key = arg_tpm2_load_public_key; + c->tpm2_public_key_pcr_mask = arg_tpm2_public_key_pcr_mask; + c->wipe_slots_scope = arg_wipe_slots_scope; + c->wipe_slots_mask = arg_wipe_slots_mask; + + if (strdup_to(&c->node, arg_node) < 0 || + strdup_to(&c->unlock_keyfile, arg_unlock_keyfile) < 0 || + strdup_to(&c->unlock_fido2_device, arg_unlock_fido2_device) < 0 || + strdup_to(&c->unlock_tpm2_device, arg_unlock_tpm2_device) < 0 || + strdup_to(&c->fido2_device, arg_fido2_device) < 0 || + strdup_to(&c->fido2_salt_file, arg_fido2_salt_file) < 0 || + strdup_to(&c->pkcs11_token_uri, arg_pkcs11_token_uri) < 0 || + strdup_to(&c->tpm2_device, arg_tpm2_device) < 0 || + strdup_to(&c->tpm2_device_key, arg_tpm2_device_key) < 0 || + strdup_to(&c->tpm2_public_key, arg_tpm2_public_key) < 0 || + strdup_to(&c->tpm2_signature, arg_tpm2_signature) < 0 || + strdup_to(&c->tpm2_pcrlock, arg_tpm2_pcrlock) < 0) + return log_oom(); + + if (arg_n_wipe_slots > 0) { + c->wipe_slots = newdup(int, arg_wipe_slots, arg_n_wipe_slots); + if (!c->wipe_slots) + return log_oom(); + c->n_wipe_slots = arg_n_wipe_slots; + } + + if (arg_tpm2_n_hash_pcr_values > 0) { + c->tpm2_hash_pcr_values = newdup(Tpm2PCRValue, arg_tpm2_hash_pcr_values, arg_tpm2_n_hash_pcr_values); + if (!c->tpm2_hash_pcr_values) + return log_oom(); + c->tpm2_n_hash_pcr_values = arg_tpm2_n_hash_pcr_values; + } + + return 0; +} + static int run(int argc, char *argv[]) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; _cleanup_(iovec_done_erase) struct iovec vk = {}; + _cleanup_(enroll_context_done) EnrollContext c = ENROLL_CONTEXT_NULL; int slot, slot_to_wipe, r; log_setup(); @@ -787,45 +866,49 @@ static int run(int argc, char *argv[]) { /* A delicious drop of snake oil */ (void) safe_mlockall(MCL_CURRENT|MCL_FUTURE|MCL_ONFAULT); - if (arg_enroll_type < 0) - r = prepare_luks(&cd, /* ret_volume_key= */ NULL); /* No need to unlock device if we don't need the volume key because we don't need to enroll anything */ + r = enroll_context_from_args(&c); + if (r < 0) + return r; + + if (c.enroll_type < 0) + r = prepare_luks(&c, &cd, /* ret_volume_key= */ NULL); /* No need to unlock device if we don't need the volume key because we don't need to enroll anything */ else - r = prepare_luks(&cd, &vk); + r = prepare_luks(&c, &cd, &vk); if (r < 0) return r; - switch (arg_enroll_type) { + switch (c.enroll_type) { case ENROLL_PASSWORD: - slot = enroll_password(cd, &vk); + slot = enroll_password(&c, cd, &vk); break; case ENROLL_RECOVERY: - slot = enroll_recovery(cd, &vk); + slot = enroll_recovery(&c, cd, &vk, /* ret_recovery_key= */ NULL); break; case ENROLL_PKCS11: - slot = enroll_pkcs11(cd, &vk, arg_pkcs11_token_uri); + slot = enroll_pkcs11(&c, cd, &vk); break; case ENROLL_FIDO2: - slot = enroll_fido2(cd, &vk, arg_fido2_device, arg_fido2_lock_with, arg_fido2_cred_alg, arg_fido2_salt_file, arg_fido2_parameters_in_header); + slot = enroll_fido2(&c, cd, &vk); break; case ENROLL_TPM2: - slot = enroll_tpm2(cd, &vk, arg_tpm2_device, arg_tpm2_seal_key_handle, arg_tpm2_device_key, arg_tpm2_hash_pcr_values, arg_tpm2_n_hash_pcr_values, arg_tpm2_public_key, arg_tpm2_load_public_key, arg_tpm2_public_key_pcr_mask, arg_tpm2_signature, arg_tpm2_pin, arg_tpm2_pcrlock, &slot_to_wipe); + slot = enroll_tpm2(&c, cd, &vk, &slot_to_wipe); if (slot >= 0 && slot_to_wipe >= 0) { assert(slot != slot_to_wipe); - /* Updating PIN on an existing enrollment */ - r = wipe_slots( - cd, - &slot_to_wipe, - /* n_explicit_slots= */ 1, - WIPE_EXPLICIT, - /* by_mask= */ 0, - /* except_slot= */ -1); + /* Updating PIN on an existing enrollment: wipe just that one slot. This is an + * internal one-off wipe that is unrelated to the user's wipe selection, so use a + * throwaway context referencing a single explicit slot. */ + EnrollContext wipe_ctx = ENROLL_CONTEXT_NULL; + wipe_ctx.wipe_slots = &slot_to_wipe; + wipe_ctx.n_wipe_slots = 1; + + r = wipe_slots(&wipe_ctx, cd); if (r < 0) return r; } @@ -836,7 +919,7 @@ static int run(int argc, char *argv[]) { return list_enrolled(cd); /* Only slot wiping selected */ - return wipe_slots(cd, arg_wipe_slots, arg_n_wipe_slots, arg_wipe_slots_scope, arg_wipe_slots_mask, -1); + return wipe_slots(&c, cd); default: return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Operation not implemented yet."); @@ -844,8 +927,9 @@ static int run(int argc, char *argv[]) { if (slot < 0) return slot; - /* After we completed enrolling, remove user selected slots */ - r = wipe_slots(cd, arg_wipe_slots, arg_n_wipe_slots, arg_wipe_slots_scope, arg_wipe_slots_mask, slot); + /* After we completed enrolling, remove user selected slots (keeping the one we just added) */ + c.wipe_except_slot = slot; + r = wipe_slots(&c, cd); if (r < 0) return r; diff --git a/src/cryptenroll/cryptenroll.h b/src/cryptenroll/cryptenroll.h index 1066ecc18d9..3e598487042 100644 --- a/src/cryptenroll/cryptenroll.h +++ b/src/cryptenroll/cryptenroll.h @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "libfido2-util.h" #include "shared-forward.h" typedef enum EnrollType { @@ -32,3 +33,79 @@ typedef enum WipeScope { DECLARE_STRING_TABLE_LOOKUP(enroll_type, EnrollType); DECLARE_STRING_TABLE_LOOKUP(luks2_token_type, EnrollType); + +/* A single bag of parameters consumed by the enrollment helpers. Populated either from the command line + * (see enroll_context_from_args() in cryptenroll.c), from a Varlink request (see cryptenroll-varlink.c), + * or by the interactive wizard (see cryptenroll-interactive.c). Owns all strings/arrays it points to. */ +typedef struct EnrollContext { + EnrollType enroll_type; + UnlockType unlock_type; + + /* Target device */ + char *node; + + /* Unlock side */ + char *unlock_keyfile; + char *unlock_fido2_device; + char *unlock_tpm2_device; + + /* New password to enroll (mechanism == password). When NULL the helpers fall back to + * $NEWPASSWORD / askpw. */ + char *passphrase; + size_t passphrase_size; + + /* FIDO2 */ + char *fido2_device; + char *fido2_salt_file; + bool fido2_parameters_in_header; + Fido2EnrollFlags fido2_lock_with; + int fido2_cred_alg; + char *fido2_pin; /* optional pre-supplied token PIN; NULL means prompt (if interactive) */ + + /* PKCS#11 */ + char *pkcs11_token_uri; + + /* TPM2 */ + char *tpm2_device; + uint32_t tpm2_seal_key_handle; + char *tpm2_device_key; + Tpm2PCRValue *tpm2_hash_pcr_values; + size_t tpm2_n_hash_pcr_values; + bool tpm2_pin; + char *tpm2_public_key; + bool tpm2_load_public_key; + uint32_t tpm2_public_key_pcr_mask; + char *tpm2_signature; + char *tpm2_pcrlock; + + /* Wipe selection */ + int *wipe_slots; + size_t n_wipe_slots; + WipeScope wipe_slots_scope; + unsigned wipe_slots_mask; + int wipe_except_slot; /* slot to never wipe (e.g. the one we just enrolled); -1 for none */ + + /* If false, the enrollment helpers must never prompt the user (no askpw, no terminal I/O, + * no log printing of credential material). They use the fields in this context as the + * sole input, and fail with ENOPKG if a required piece of input is missing. CLI and + * interactive callers set this to true; the Varlink dispatcher sets it to false. */ + bool interactive; + + /* Varlink link the request came in on, if the caller asked for 'more'. NULL otherwise. + * Owned (sd_varlink_ref'd) by the context. */ + sd_varlink *link; +} EnrollContext; + +#define ENROLL_CONTEXT_NULL \ + (EnrollContext) { \ + .enroll_type = _ENROLL_TYPE_INVALID, \ + .unlock_type = UNLOCK_PASSWORD, \ + .fido2_parameters_in_header = true, \ + .fido2_lock_with = FIDO2ENROLL_PIN | FIDO2ENROLL_UP, \ + .tpm2_load_public_key = true, \ + .wipe_slots_scope = WIPE_EXPLICIT, \ + .wipe_except_slot = -1, \ + .interactive = true, \ + } + +void enroll_context_done(EnrollContext *c); diff --git a/src/home/homectl-fido2.c b/src/home/homectl-fido2.c index d351b1db5e1..210c050284d 100644 --- a/src/home/homectl-fido2.c +++ b/src/home/homectl-fido2.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "alloc-util.h" +#include "ask-password-api.h" /* IWYU pragma: keep */ #include "errno-util.h" #include "fido2-util.h" #include "hexdecoct.h" @@ -174,6 +175,8 @@ int identity_add_fido2_parameters( /* user_icon= */ NULL, /* askpw_icon= */ "user-home", /* askpw_credential= */ "home.token-pin", + /* askpw_flags= */ 0, + /* pin= */ NULL, lock_with, cred_alg, &salt, diff --git a/src/shared/libfido2-util.c b/src/shared/libfido2-util.c index 5f69dc89249..a7c776d0ce3 100644 --- a/src/shared/libfido2-util.c +++ b/src/shared/libfido2-util.c @@ -746,6 +746,8 @@ int fido2_generate_hmac_hash( const char *user_icon, const char *askpw_icon, const char *askpw_credential, + AskPasswordFlags askpw_flags, + const char *pin, Fido2EnrollFlags lock_with, int cred_alg, const struct iovec *salt, @@ -920,48 +922,57 @@ int fido2_generate_hmac_hash( return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Token asks for PIN but doesn't advertise 'clientPin' feature."); - AskPasswordFlags askpw_flags = ASK_PASSWORD_ACCEPT_CACHED; + AskPasswordFlags pin_askpw_flags = askpw_flags | ASK_PASSWORD_ACCEPT_CACHED; for (;;) { - _cleanup_strv_free_erase_ char **pin = NULL; - _cleanup_free_ char *ask_pin_msg = NULL; - int pin_retries = -1; - - r = sym_fido_dev_get_retry_count(d, &pin_retries); - if (r != FIDO_OK) { - log_warning("Failed to obtain number of retries before lock-out for PIN " - "authentication, ignoring: %s", sym_fido_strerr(r)); - pin_retries = -1; - } + _cleanup_strv_free_erase_ char **pins = NULL; - if (pin_retries >= 0) { - ask_pin_msg = asprintf_safe(_("Please enter security token PIN " - "(remaining attempts before lock-out: %d):"), - pin_retries); - if (!ask_pin_msg) + if (pin) { + /* A PIN was supplied by the caller: use it directly, don't prompt. */ + pins = strv_new(pin); + if (!pins) return log_oom(); - } - - AskPasswordRequest req = { - .tty_fd = -EBADF, - .message = pin_retries >= 0 - ? ask_pin_msg - : _("Please enter security token PIN:"), - .icon = askpw_icon, - .keyring = "fido2-pin", - .credential = askpw_credential, - .until = USEC_INFINITY, - .hup_fd = -EBADF, - }; + } else if (FLAGS_SET(askpw_flags, ASK_PASSWORD_HEADLESS)) + return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "PIN querying disabled via 'headless' option."); + else { + int pin_retries = -1; + r = sym_fido_dev_get_retry_count(d, &pin_retries); + if (r != FIDO_OK) { + log_warning("Failed to obtain number of retries before lock-out for PIN " + "authentication, ignoring: %s", sym_fido_strerr(r)); + pin_retries = -1; + } - r = ask_password_auto(&req, askpw_flags, &pin); - if (r < 0) - return log_error_errno(r, "Failed to acquire user PIN: %m"); + _cleanup_free_ char *ask_pin_msg = NULL; + if (pin_retries >= 0) { + ask_pin_msg = asprintf_safe(_("Please enter security token PIN " + "(remaining attempts before lock-out: %d):"), + pin_retries); + if (!ask_pin_msg) + return log_oom(); + } - askpw_flags &= ~ASK_PASSWORD_ACCEPT_CACHED; + AskPasswordRequest req = { + .tty_fd = -EBADF, + .message = pin_retries >= 0 + ? ask_pin_msg + : _("Please enter security token PIN:"), + .icon = askpw_icon, + .keyring = "fido2-pin", + .credential = askpw_credential, + .until = USEC_INFINITY, + .hup_fd = -EBADF, + }; + + r = ask_password_auto(&req, pin_askpw_flags, &pins); + if (r < 0) + return log_error_errno(r, "Failed to acquire user PIN: %m"); + + pin_askpw_flags &= ~ASK_PASSWORD_ACCEPT_CACHED; + } r = FIDO_ERR_PIN_INVALID; - STRV_FOREACH(i, pin) { + STRV_FOREACH(i, pins) { if (isempty(*i)) { log_notice("PIN may not be empty."); continue; @@ -981,6 +992,11 @@ int fido2_generate_hmac_hash( if (r != FIDO_ERR_PIN_INVALID) break; + /* A caller-supplied PIN that's wrong won't get better by retrying: fail instead of + * looping forever (we'd never prompt). */ + if (pin) + break; + log_notice("PIN incorrect, please try again."); } } @@ -996,6 +1012,9 @@ int fido2_generate_hmac_hash( if (r == FIDO_ERR_UNSUPPORTED_ALGORITHM) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Token doesn't support credential algorithm %s.", fido2_algorithm_to_string(cred_alg)); + if (r == FIDO_ERR_PIN_INVALID) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "PIN incorrect."); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to generate FIDO2 credential: %s", sym_fido_strerr(r)); diff --git a/src/shared/libfido2-util.h b/src/shared/libfido2-util.h index bbf6d1ca66a..6b23b62688b 100644 --- a/src/shared/libfido2-util.h +++ b/src/shared/libfido2-util.h @@ -117,6 +117,8 @@ int fido2_generate_hmac_hash( const char *user_icon, const char *askpw_icon, const char *askpw_credential, + AskPasswordFlags askpw_flags, + const char *pin, Fido2EnrollFlags lock_with, int cred_alg, const struct iovec *salt,