diff --git a/TODO.md b/TODO.md index d8badaf5ab..d82371bbc9 100644 --- a/TODO.md +++ b/TODO.md @@ -2527,9 +2527,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later the dropped in certs and encrypted with machine pubkey, and pass to machine. Machine is then able to authenticate you, and confidentiality is guaranteed. -- systemd-cryptenroll: add --firstboot or so, that will interactively ask user - whether recovery key shall be enrolled and do so - - systemd-dissect: add --cat switch for dumping files such as /etc/os-release - systemd-dissect: show available versions inside of a disk image, i.e. if @@ -2893,7 +2890,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later - coredumpctl - systemd-bless-boot - systemd-measure - - systemd-cryptenroll (to allow UIs to enroll FIDO2 keys and such) - systemd-dissect - systemd-sysupdate - systemd-analyze diff --git a/man/systemd-cryptenroll.xml b/man/systemd-cryptenroll.xml index 707a8d25e8..9f090ec94b 100644 --- a/man/systemd-cryptenroll.xml +++ b/man/systemd-cryptenroll.xml @@ -270,6 +270,16 @@ The following options are understood that may be used to unlock the device in preparation of the enrollment operations: + + + + Use an empty password/passphrase to unlock the volume, instead of reading one from + stdin. This is useful to unlock volumes that are currently protected by an empty password, in order to + enroll a different, stronger unlock mechanism. + + + + @@ -307,6 +317,18 @@ + + + + + Try the unlock mechanisms suitable for headless operation in turn, without requiring + any interactive input. If a TPM2 device is available the volume is unlocked via TPM2, and otherwise + (or if TPM2 unlocking fails) an empty password is attempted. This is primarily useful in automated + provisioning scenarios where a volume is first set up with a TPM2 enrollment or an empty password and + a stronger unlock mechanism shall be enrolled subsequently. + + + @@ -340,6 +362,65 @@ + + Interactive Enrollment + + The following options are understood that drive an interactive enrollment wizard, intended to be + run on first boot (similar in spirit to + systemd-firstboot1): + + + + + + Run an interactive wizard that lets the user enroll a passphrase, a recovery key, or a + FIDO2 security token (one menu entry per suitable token currently plugged in). If credentials of any + of these types are already enrolled, the wizard additionally offers to wipe them. Pressing enter at + the top-level menu leaves the volume unchanged. This option may not be combined with an explicit + enrollment type or with . As with the other commands, the volume to + operate on is the one backing /var/ if none is specified. Honours the + systemd.firstboot= kernel command line option. + + + + + + + + Takes a comma-separated list of slot types, out of password, + recovery, pkcs11, fido2 and + tpm2. Only useful together with : if a slot of any of + the listed types is already enrolled on the volume, the wizard does nothing and exits successfully. + This is useful to hook the wizard into the boot process while ensuring it stays quiet once suitable + credentials have been set up. + + + + + + + + Takes a boolean argument, defaults to on. Only useful together with + : if enabled the interactive wizard draws a coloured bar across the top and + bottom of the terminal to visually frame the prompt. Pass to disable this + decoration, for example when running on a terminal that does not render it well. + + + + + + + + Takes a boolean argument, defaults to off. Only useful together with + : if enabled the kernel and PID 1 are asked to refrain from writing log + messages to the console while the interactive wizard is shown, so that such output does not interfere + with the prompt. The console is unmuted again once the wizard exits. + + + + + + PKCS#11 Enrollment diff --git a/shell-completion/bash/systemd-cryptenroll b/shell-completion/bash/systemd-cryptenroll index 6ae9bb3840..a24d997802 100644 --- a/shell-completion/bash/systemd-cryptenroll +++ b/shell-completion/bash/systemd-cryptenroll @@ -43,10 +43,14 @@ _systemd_cryptenroll() { local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} words cword local -A OPTS=( [STANDALONE]='-h --help --version - --password --recovery-key --list-devices' + --password --recovery-key --list-devices + --unlock-empty --unlock-headless --firstboot' [ARG]='--unlock-key-file --unlock-fido2-device --unlock-tpm2-device + --prompt-suppress + --chrome + --mute-console --pkcs11-token-uri --fido2-credential-algorithm --fido2-device @@ -99,6 +103,12 @@ _systemd_cryptenroll() { --wipe-slot) comps='all empty password recovery pkcs11 fido2 tpm2' ;; + --prompt-suppress) + comps='password recovery pkcs11 fido2 tpm2' + ;; + --chrome|--mute-console) + comps='yes no' + ;; esac COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) return 0 diff --git a/shell-completion/zsh/_systemd-cryptenroll b/shell-completion/zsh/_systemd-cryptenroll new file mode 100644 index 0000000000..e41b67ffdc --- /dev/null +++ b/shell-completion/zsh/_systemd-cryptenroll @@ -0,0 +1,74 @@ +#compdef systemd-cryptenroll +# SPDX-License-Identifier: LGPL-2.1-or-later + +(( $+functions[_systemd-cryptenroll_devices] )) || +_systemd-cryptenroll_devices() { + local -a devices + devices=( ${(f)"$(_call_program devices systemd-cryptenroll --list-devices 2>/dev/null)"} ) + _describe -t devices 'block device' devices +} + +(( $+functions[_systemd-cryptenroll_fido2_device] )) || +_systemd-cryptenroll_fido2_device() { + _alternative \ + "special:special:($*)" \ + 'devices:FIDO2 device:_files -g "/dev/hidraw*(-c)"' +} + +(( $+functions[_systemd-cryptenroll_tpm2_device] )) || +_systemd-cryptenroll_tpm2_device() { + _alternative \ + "special:special:($*)" \ + 'devices:TPM2 device:_files -g "/dev/tpmrm*(-c)"' +} + +(( $+functions[_systemd-cryptenroll_wipe_slot] )) || +_systemd-cryptenroll_wipe_slot() { + _values -s , 'slot' all empty password recovery pkcs11 fido2 tpm2 +} + +(( $+functions[_systemd-cryptenroll_prompt_suppress] )) || +_systemd-cryptenroll_prompt_suppress() { + _values -s , 'type' password recovery pkcs11 fido2 tpm2 +} + +# Unlock methods are mutually exclusive with each other +local unlock='--unlock-empty --unlock-key-file --unlock-fido2-device --unlock-tpm2-device --unlock-headless' +# Enrollment operations are mutually exclusive with each other +local enroll='--password --recovery-key --pkcs11-token-uri --fido2-device --tpm2-device --tpm2-device-key --firstboot' + +_arguments -s \ + '(- *)'{-h,--help}'[Show this help]' \ + '(- *)--version[Show package version]' \ + '--no-pager[Do not pipe output into a pager]' \ + '(- *)--list-devices[List candidate block devices to operate on]' \ + '--wipe-slot=[Wipe specified slots]:slot:_systemd-cryptenroll_wipe_slot' \ + "($enroll)--firstboot[Interactively enroll a credential (first-boot wizard)]" \ + '--prompt-suppress=[Skip the --firstboot wizard if a slot of any listed type exists]:type:_systemd-cryptenroll_prompt_suppress' \ + '--chrome=[In first-boot mode, do not show colour bar at top and bottom of terminal]:boolean:(yes no)' \ + '--mute-console=[In first-boot mode, tell kernel/PID 1 to not write to the console while running]:boolean:(yes no)' \ + "($unlock)--unlock-empty[Use an empty password to unlock the volume]" \ + "($unlock)--unlock-key-file=[Use a file to unlock the volume]:key file:_files" \ + "($unlock)--unlock-fido2-device=[Use a FIDO2 device to unlock the volume]:FIDO2 device:_systemd-cryptenroll_fido2_device auto" \ + "($unlock)--unlock-tpm2-device=[Use a TPM2 device to unlock the volume]:TPM2 device:_systemd-cryptenroll_tpm2_device auto" \ + "($unlock)--unlock-headless[Try the 'headless' unlock mechanisms in turn]" \ + "($enroll)--password[Enroll a user-supplied password]" \ + "($enroll)--recovery-key[Enroll a recovery key]" \ + "($enroll)--pkcs11-token-uri=[Enroll a PKCS#11 security token or list them]:PKCS#11 token URI:(auto list pkcs11:)" \ + "($enroll)--fido2-device=[Enroll a FIDO2-HMAC security token or list them]:FIDO2 device:_systemd-cryptenroll_fido2_device auto list" \ + '--fido2-salt-file=[Use salt from a file instead of generating one]:salt file:_files' \ + '--fido2-parameters-in-header=[Whether to store FIDO2 parameters in the LUKS2 header]:boolean:(yes no)' \ + '--fido2-credential-algorithm=[Specify COSE algorithm for FIDO2 credential]:algorithm:(es256 rs256 eddsa)' \ + '--fido2-with-client-pin=[Whether to require entering a PIN to unlock the volume]:boolean:(yes no)' \ + '--fido2-with-user-presence=[Whether to require user presence to unlock the volume]:boolean:(yes no)' \ + '--fido2-with-user-verification=[Whether to require user verification to unlock the volume]:boolean:(yes no)' \ + "($enroll)--tpm2-device=[Enroll a TPM2 device or list them]:TPM2 device:_systemd-cryptenroll_tpm2_device auto list" \ + "($enroll)--tpm2-device-key=[Enroll a TPM2 device using its public key]:public key file:_files" \ + '--tpm2-seal-key-handle=[Specify handle of key to use for sealing]:handle:' \ + '--tpm2-pcrs=[Specify TPM2 PCRs to seal against]:PCRs:' \ + '--tpm2-public-key=[Enroll signed TPM2 PCR policy against PEM public key]:public key file:_files' \ + '--tpm2-public-key-pcrs=[Enroll signed TPM2 PCR policy for specified TPM2 PCRs]:PCRs:' \ + '--tpm2-signature=[Validate public key enrollment works with JSON signature file]:signature file:_files' \ + '--tpm2-pcrlock=[Specify pcrlock policy to lock against]:pcrlock file:_files' \ + '--tpm2-with-pin=[Whether to require entering a PIN to unlock the volume]:boolean:(yes no)' \ + '*::block device:_systemd-cryptenroll_devices' diff --git a/shell-completion/zsh/meson.build b/shell-completion/zsh/meson.build index f10ba7be61..edad8bcb32 100644 --- a/shell-completion/zsh/meson.build +++ b/shell-completion/zsh/meson.build @@ -36,6 +36,7 @@ foreach item : [ ['_storagectl', ''], ['_systemd', ''], ['_systemd-analyze', ''], + ['_systemd-cryptenroll', 'HAVE_LIBCRYPTSETUP'], ['_systemd-delta', ''], ['_systemd-hwdb', 'ENABLE_HWDB'], ['_systemd-id128', ''], diff --git a/src/cryptenroll/cryptenroll-fido2.c b/src/cryptenroll/cryptenroll-fido2.c index 600207e947..b315282df1 100644 --- a/src/cryptenroll/cryptenroll-fido2.c +++ b/src/cryptenroll/cryptenroll-fido2.c @@ -3,6 +3,7 @@ #include "alloc-util.h" #include "ask-password-api.h" #include "cryptenroll-fido2.h" +#include "cryptenroll-varlink.h" #include "cryptsetup-fido2.h" #include "cryptsetup-util.h" #include "fido2-util.h" @@ -15,11 +16,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 +27,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 +56,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 +70,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 +83,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, @@ -111,8 +108,14 @@ int enroll_fido2( if (r < 0) return r; + /* If we are operating over a Varlink connection that requested progress reports, let the client + * know a token touch is imminent before we enter the (blocking) FIDO2 operations below. */ + r = enroll_context_notify_state(c, "touch"); + if (r < 0) + 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 +124,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 +155,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 7d0c25c02a..c5d57f12db 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-interactive.c b/src/cryptenroll/cryptenroll-interactive.c new file mode 100644 index 0000000000..6dc584b18b --- /dev/null +++ b/src/cryptenroll/cryptenroll-interactive.c @@ -0,0 +1,243 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "alloc-util.h" +#include "ansi-color.h" +#include "cryptenroll.h" +#include "cryptenroll-interactive.h" +#include "cryptenroll-list.h" +#include "cryptsetup-util.h" +#include "glyph-util.h" +#include "libfido2-util.h" +#include "log.h" +#include "proc-cmdline.h" +#include "prompt-util.h" +#include "string-util.h" +#include "strv.h" +#include "terminal-util.h" + +static int collect_existing_types(struct crypt_device *cd, unsigned *ret_mask) { + _cleanup_free_ EnrolledSlot *slots = NULL; + size_t n_slots; + unsigned mask = 0; + int r; + + assert(cd); + assert(ret_mask); + + r = collect_enrolled_slots(cd, &slots, &n_slots); + if (r < 0) + return r; + + FOREACH_ARRAY(s, slots, n_slots) + if (!s->conflict && s->type >= 0) + mask |= 1U << s->type; + + *ret_mask = mask; + return 0; +} + +static int choose_mechanism(EnrollContext *c) { + int r; + + assert(c); + + /* Top-level menu. Each iteration re-enumerates the FIDO2 tokens, so a token plugged in after the + * menu was first shown appears once the user picks "Rescan". An empty answer leaves the volume + * untouched. */ + + for (;;) { + Fido2DeviceInfo *devices = NULL; + size_t n_devices = 0; + CLEANUP_ARRAY(devices, n_devices, fido2_device_info_free_many); + + /* Best effort: if FIDO2 isn't available we simply offer no token rows. */ + (void) fido2_enumerate_devices(&devices, &n_devices); + + _cleanup_strv_free_ char **menu = strv_new("Enroll a recovery key", "Enroll a passphrase"); + if (!menu) + return log_oom(); + + FOREACH_ARRAY(d, devices, n_devices) { + _cleanup_free_ char *label = NULL; + + label = strjoin("Enroll FIDO2 security token: ", + d->manufacturer ?: "Security Token", + d->product ? " " : "", strempty(d->product), + " (", d->path, ")"); + if (!label) + return log_oom(); + + if (strv_consume(&menu, TAKE_PTR(label)) < 0) + return log_oom(); + } + + if (strv_extend(&menu, "Rescan for FIDO2 security tokens") < 0) + return log_oom(); + + _cleanup_free_ char *choice = NULL; + r = prompt_loop( + "Select enrollment option", + GLYPH_LOCK_AND_KEY, + /* prefill= */ NULL, + menu, + /* accepted= */ menu, /* only accept exact menu entries (or their numbers) */ + /* ellipsize_percentage= */ 60, + /* n_columns= */ 1, + /* column_width= */ SIZE_MAX, /* auto-size to the widest entry */ + /* is_valid= */ NULL, + /* refresh= */ NULL, + /* userdata= */ NULL, + PROMPT_MAY_SKIP|PROMPT_SHOW_MENU|PROMPT_SHOW_MENU_NOW|PROMPT_HIDE_MENU_HINT, + &choice); + if (r < 0) + return r; + if (!choice) { + /* Empty answer: do nothing. */ + log_info("No selection made, leaving volume unchanged."); + return 0; + } + + /* Map the chosen label back to its index. */ + size_t idx = SIZE_MAX; + STRV_FOREACH(m, menu) + if (streq(*m, choice)) { + idx = m - menu; + break; + } + + assert(idx != SIZE_MAX); + + if (idx == 0) + c->enroll_type = ENROLL_RECOVERY; + else if (idx == 1) + c->enroll_type = ENROLL_PASSWORD; + else if (idx + 1 == strv_length(menu)) /* "Rescan" */ + continue; + else { + c->enroll_type = ENROLL_FIDO2; + assert(idx > 1); + assert(idx + 1 < strv_length(menu)); + + if (strdup_to(&c->fido2_device, devices[idx - 2].path) < 0) + return log_oom(); + } + + return 1; + } +} + +static int ask_wipe(EnrollContext *c, unsigned existing_mask) { + static const EnrollType candidates[] = { + ENROLL_PASSWORD, + ENROLL_RECOVERY, + ENROLL_FIDO2, + }; + int r; + + assert(c); + + /* For each already-enrolled type the wizard understands, offer to wipe it alongside the new + * enrollment. */ + + FOREACH_ELEMENT(t, candidates) { + if (!FLAGS_SET(existing_mask, 1U << *t)) + continue; + + _cleanup_free_ char *question = NULL; + question = strjoin("A ", enroll_type_to_string(*t), " slot is already enrolled. Wipe it as part of this enrollment?"); + if (!question) + return log_oom(); + + bool wipe; + r = prompt_loop_yes_no(question, /* def= */ false, &wipe); + if (r < 0) + return r; + + if (wipe) + c->wipe_slots_mask |= 1U << *t; + } + + return 0; +} + +int cryptenroll_run_interactive( + EnrollContext *c, + unsigned prompt_suppress_mask, + bool chrome, + sd_varlink **mute_console_link) { + + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + unsigned existing_mask = 0; + int r; + + assert(c); + assert(c->node); + + /* Honour the systemd.firstboot= kernel command line option, just like systemd-firstboot. */ + bool enabled; + r = proc_cmdline_get_bool("systemd.firstboot", PROC_CMDLINE_TRUE_WHEN_MISSING, &enabled); + if (r < 0) + log_warning_errno(r, "Failed to parse systemd.firstboot= kernel command line option, ignoring: %m"); + else if (!enabled) { + log_debug("systemd.firstboot=no set, skipping interactive enrollment."); + return 0; + } + + /* Open the volume just to inspect its header (no unlocking needed yet). */ + r = prepare_luks(c, &cd, /* ret_volume_key= */ NULL); + if (r < 0) + return r; + + r = collect_existing_types(cd, &existing_mask); + if (r < 0) + return r; + + /* If a credential of a suppressed type is already enrolled, do nothing. This lets the wizard be + * wired into first boot but stay quiet once the system has been set up. */ + if ((existing_mask & prompt_suppress_mask) != 0) { + log_debug("A credential of a suppressed type is already enrolled, skipping interactive setup."); + return 0; + } + + if (existing_mask == 0) { + log_debug("No recognized LUKS slots, we're unlikely able to unlock, skipping interactive setup."); + return 0; + } + + (void) mute_console(mute_console_link); + + /* Draw the installer-style chrome (blue bars at the top and bottom) around the wizard, matching + * systemd-sysinstall. The caller hides it again via a deferred chrome_hide() once enrollment is + * complete. */ + (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0); + + if (chrome) + (void) chrome_show("Additional Disk Encryption Key Enrollment", /* bottom= */ NULL); + + printf("%s%s%sLet's enroll additional disk encryption mechanisms for recovering access to the system.%s\n\n", + emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : "", + ansi_highlight(), ansi_normal()); + + _cleanup_free_ char *s = NULL; + for (EnrollType t = 0; t < _ENROLL_TYPE_MAX; t++) { + if (!FLAGS_SET(existing_mask, 1 << t)) + continue; + + if (!strextend_with_separator(&s, ", ", enroll_type_to_string(t))) + return log_oom(); + } + + printf("Currently enrolled mechanisms: %s\n\n", s); + + r = choose_mechanism(c); + if (r <= 0) + return r; + + r = ask_wipe(c, existing_mask); + if (r < 0) + return r; + + return 1; +} diff --git a/src/cryptenroll/cryptenroll-interactive.h b/src/cryptenroll/cryptenroll-interactive.h new file mode 100644 index 0000000000..413e0d8b93 --- /dev/null +++ b/src/cryptenroll/cryptenroll-interactive.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "cryptenroll.h" + +/* Runs the interactive first-boot enrollment wizard, populating *c (c->enroll_type, c->fido2_device, + * c->wipe_slots_mask). On return c->enroll_type is set to the chosen mechanism, or left + * _ENROLL_TYPE_INVALID if the user skipped or the wizard was suppressed. + * + * prompt_suppress_mask is a bitmask of (1U << EnrollType): if a slot of any such type already exists on the + * volume, the wizard does nothing (so it can be hooked into first boot but stay quiet on later boots). */ +int cryptenroll_run_interactive(EnrollContext *c, unsigned prompt_suppress_mask, bool chrome, sd_varlink **mute_console_link); diff --git a/src/cryptenroll/cryptenroll-list.c b/src/cryptenroll/cryptenroll-list.c index ab7637e616..21d768b1a8 100644 --- a/src/cryptenroll/cryptenroll-list.c +++ b/src/cryptenroll/cryptenroll-list.c @@ -11,21 +11,17 @@ #include "log.h" #include "parse-util.h" -struct keyslot_metadata { - int slot; - const char *type; -}; - -int list_enrolled(struct crypt_device *cd) { - _cleanup_free_ struct keyslot_metadata *keyslot_metadata = NULL; - _cleanup_(table_unrefp) Table *t = NULL; - size_t n_keyslot_metadata = 0; +int collect_enrolled_slots(struct crypt_device *cd, EnrolledSlot **ret, size_t *ret_n) { + _cleanup_free_ EnrolledSlot *slots = NULL; + size_t n_slots = 0; int slot_max, r; - TableCell *cell; assert(cd); + assert(ret); + assert(ret_n); - /* First step, find out all currently used slots */ + /* First step, find out all currently used slots. A slot without an associated token is a bare + * passphrase slot, hence default to ENROLL_PASSWORD. */ assert_se((slot_max = sym_crypt_keyslot_max(CRYPT_LUKS2)) > 0); for (int slot = 0; slot < slot_max; slot++) { crypt_keyslot_info status; @@ -34,11 +30,12 @@ int list_enrolled(struct crypt_device *cd) { if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST)) continue; - if (!GREEDY_REALLOC(keyslot_metadata, n_keyslot_metadata+1)) + if (!GREEDY_REALLOC(slots, n_slots + 1)) return log_oom(); - keyslot_metadata[n_keyslot_metadata++] = (struct keyslot_metadata) { + slots[n_slots++] = (EnrolledSlot) { .slot = slot, + .type = ENROLL_PASSWORD, }; } @@ -46,7 +43,6 @@ int list_enrolled(struct crypt_device *cd) { * token they are assigned to */ for (int token = 0; token < sym_crypt_token_max(CRYPT_LUKS2); token++) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; - const char *type; sd_json_variant *w, *z; EnrollType et; @@ -64,11 +60,7 @@ int list_enrolled(struct crypt_device *cd) { continue; } - et = luks2_token_type_from_string(sd_json_variant_string(w)); - if (et < 0) - type = "other"; - else - type = enroll_type_to_string(et); + et = luks2_token_type_from_string(sd_json_variant_string(w)); /* _ENROLL_TYPE_INVALID for unrecognized type */ w = sd_json_variant_by_key(v, "keyslots"); if (!w || !sd_json_variant_is_array(w)) { @@ -90,19 +82,40 @@ int list_enrolled(struct crypt_device *cd) { continue; } - for (size_t i = 0; i < n_keyslot_metadata; i++) { - if ((unsigned) keyslot_metadata[i].slot != u) + FOREACH_ARRAY(s, slots, n_slots) { + if ((unsigned) s->slot != u) continue; - if (keyslot_metadata[i].type) /* Slot claimed multiple times? */ - keyslot_metadata[i].type = POINTER_MAX; + if (s->conflict) /* Already marked as claimed multiple times. */ + break; + + if (s->type != ENROLL_PASSWORD) /* Slot already claimed by another token? */ + s->conflict = true; else - keyslot_metadata[i].type = type; + s->type = et; } } } - /* Finally, create a table out of it all */ + *ret = TAKE_PTR(slots); + *ret_n = n_slots; + return 0; +} + +int list_enrolled(struct crypt_device *cd) { + _cleanup_free_ EnrolledSlot *slots = NULL; + _cleanup_(table_unrefp) Table *t = NULL; + size_t n_slots; + int r; + TableCell *cell; + + assert(cd); + + r = collect_enrolled_slots(cd, &slots, &n_slots); + if (r < 0) + return r; + + /* Create a table out of it all */ t = table_new("slot", "type"); if (!t) return log_oom(); @@ -110,12 +123,20 @@ int list_enrolled(struct crypt_device *cd) { assert_se(cell = table_get_cell(t, 0, 0)); (void) table_set_align_percent(t, cell, 100); - for (size_t i = 0; i < n_keyslot_metadata; i++) { + FOREACH_ARRAY(s, slots, n_slots) { + const char *type; + + if (s->conflict) + type = "conflict"; + else if (s->type < 0) + type = "other"; /* token of unrecognized type */ + else + type = enroll_type_to_string(s->type); + r = table_add_many( t, - TABLE_INT, keyslot_metadata[i].slot, - TABLE_STRING, keyslot_metadata[i].type == POINTER_MAX ? "conflict" : - keyslot_metadata[i].type ?: "password"); + TABLE_INT, s->slot, + TABLE_STRING, type); if (r < 0) return table_log_add_error(r); } diff --git a/src/cryptenroll/cryptenroll-list.h b/src/cryptenroll/cryptenroll-list.h index 9097bc42c1..3366336713 100644 --- a/src/cryptenroll/cryptenroll-list.h +++ b/src/cryptenroll/cryptenroll-list.h @@ -1,6 +1,19 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "cryptenroll.h" #include "shared-forward.h" +typedef struct EnrolledSlot { + int slot; + /* The token type associated with the slot. ENROLL_PASSWORD means a bare passphrase slot without + * any token. _ENROLL_TYPE_INVALID means either a token of an unrecognized type, or a slot claimed + * by more than one token (see 'conflict'). */ + EnrollType type; + bool conflict; +} EnrolledSlot; + +/* Enumerates the active keyslots and classifies each by token type. Returns a newly allocated array. */ +int collect_enrolled_slots(struct crypt_device *cd, EnrolledSlot **ret, size_t *ret_n); + int list_enrolled(struct crypt_device *cd); diff --git a/src/cryptenroll/cryptenroll-password.c b/src/cryptenroll/cryptenroll-password.c index e189cce23a..22b64e3229 100644 --- a/src/cryptenroll/cryptenroll-password.c +++ b/src/cryptenroll/cryptenroll-password.c @@ -7,25 +7,101 @@ #include "env-util.h" #include "errno-util.h" #include "escape.h" +#include "fileio.h" #include "iovec-util.h" #include "log.h" #include "password-quality-util.h" #include "string-util.h" #include "strv.h" -int load_volume_key_password( +int load_volume_key_empty( + const EnrollContext *c, struct crypt_device *cd, - const char *cd_node, - void *ret_vk, - size_t *ret_vks) { + struct iovec *ret_vk) { + + int r; + + assert_se(c); + assert_se(cd); + assert_se(ret_vk); + + r = sym_crypt_volume_key_get( + cd, + CRYPT_ANY_SLOT, + ret_vk->iov_base, + &ret_vk->iov_len, + "", + 0); + if (r < 0) + return log_error_errno(r, "Provided empty password did not work: %m"); + + return r; +} + +int load_volume_key_keyfile( + const EnrollContext *c, + struct crypt_device *cd, + 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); + + r = read_full_file_full( + AT_FDCWD, + c->unlock_keyfile, + UINT64_MAX, + 4U * U64_MB, /* safety net */ + READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET|READ_FULL_FILE_FAIL_WHEN_LARGER, + NULL, + &password, + &password_len); + if (r < 0) + 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->iov_base, + &ret_vk->iov_len, + password, + password_len); + if (r < 0) + return log_error_errno(r, "Unlocking via keyfile failed: %m"); + + return r; +} + +int load_volume_key_password( + const EnrollContext *c, + struct crypt_device *cd, + 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); + + if (c->unlock_password) { + r = sym_crypt_volume_key_get( + cd, + CRYPT_ANY_SLOT, + ret_vk->iov_base, + &ret_vk->iov_len, + c->unlock_password, + strlen(c->unlock_password)); + if (r < 0) + return log_error_errno(r, "Provided unlock password did not work: %m"); + + return r; + } r = getenv_steal_erase("PASSWORD", &envpw); if (r < 0) @@ -34,22 +110,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 +165,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 +184,7 @@ int load_volume_key_password( } int enroll_password( + const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key) { @@ -111,18 +193,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 f0c97ced29..035a1becc5 100644 --- a/src/cryptenroll/cryptenroll-password.h +++ b/src/cryptenroll/cryptenroll-password.h @@ -1,7 +1,11 @@ /* 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_empty(const EnrollContext *c, struct crypt_device *cd, struct iovec *ret_vk); +int load_volume_key_keyfile(const EnrollContext *c, struct crypt_device *cd, struct iovec *ret_vk); + +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 13feda47c4..c46fcca10a 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 b3e6e4584a..614c6d466c 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 85ec13f128..da7b7a1018 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 0c76d86108..1655f71445 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 2f6c7ecd72..eb08abdec6 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 48f6c12b7f..3d57b8f8f3 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-varlink.c b/src/cryptenroll/cryptenroll-varlink.c new file mode 100644 index 0000000000..77ead09673 --- /dev/null +++ b/src/cryptenroll/cryptenroll-varlink.c @@ -0,0 +1,481 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-varlink.h" + +#include "alloc-util.h" +#include "bus-polkit.h" +#include "cryptenroll.h" +#include "cryptenroll-list.h" +#include "cryptenroll-varlink.h" +#include "cryptenroll-wipe.h" +#include "cryptsetup-util.h" +#include "fd-util.h" +#include "hashmap.h" +#include "iovec-util.h" +#include "json-util.h" +#include "libfido2-util.h" +#include "path-util.h" +#include "string-util.h" +#include "varlink-io.systemd.CryptEnroll.h" +#include "varlink-util.h" + +int enroll_context_notify_state(const EnrollContext *c, const char *state) { + int r; + + assert(c); + assert(state); + + /* Only relevant when invoked over a Varlink connection that asked for progress ('more'). */ + if (!c->link) + return 0; + + r = sd_varlink_notifybo(c->link, SD_JSON_BUILD_PAIR_STRING("state", state)); + if (r < 0) + return r; + + return sd_varlink_flush(c->link); +} + +static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_enroll_type, EnrollType, enroll_type_from_string); + +typedef struct MethodEnrollParameters { + char *node; + EnrollType mechanism; + char *unlock_password; + char *unlock_keyfile; + int64_t unlock_keyfile_fd_idx; + char *unlock_fido2_device; + char *unlock_tpm2_device; + char *password; + char *fido2_device; + char *fido2_pin; + int fido2_with_client_pin; + int fido2_with_user_presence; + int fido2_with_user_verification; + sd_json_variant *wipe_slots; + sd_json_variant *wipe_types; +} MethodEnrollParameters; + +static void method_enroll_parameters_done(MethodEnrollParameters *p) { + assert(p); + + free(p->node); + erase_and_free(p->unlock_password); + free(p->unlock_keyfile); + free(p->unlock_fido2_device); + free(p->unlock_tpm2_device); + erase_and_free(p->password); + free(p->fido2_device); + erase_and_free(p->fido2_pin); + sd_json_variant_unref(p->wipe_slots); + sd_json_variant_unref(p->wipe_types); +} + +static int parse_wipe_slots(sd_json_variant *v, EnrollContext *c) { + sd_json_variant *e; + + assert(c); + + JSON_VARIANT_ARRAY_FOREACH(e, v) { + if (!sd_json_variant_is_unsigned(e)) + return -EINVAL; + + uint64_t u = sd_json_variant_unsigned(e); + if (u > INT_MAX) + return -ERANGE; + + if (!GREEDY_REALLOC(c->wipe_slots, c->n_wipe_slots + 1)) + return -ENOMEM; + + c->wipe_slots[c->n_wipe_slots++] = (int) u; + } + + return 0; +} + +static int parse_wipe_types(sd_json_variant *v, EnrollContext *c) { + sd_json_variant *e; + + assert(c); + + JSON_VARIANT_ARRAY_FOREACH(e, v) { + if (!sd_json_variant_is_string(e)) + return -EINVAL; + + /* Translate the Varlink (underscore) spelling to the internal (dash) one before lookup. */ + _cleanup_free_ char *s = strdup(sd_json_variant_string(e)); + if (!s) + return -ENOMEM; + + EnrollType t = enroll_type_from_string(json_dashify(s)); + if (t < 0) + return -EINVAL; + + c->wipe_slots_mask |= 1U << t; + } + + return 0; +} + +static int varlink_error_for_enroll(sd_varlink *link, int error) { + assert(link); + assert(error < 0); + + /* Translates the errnos the enrollment/unlock helpers return into the interface's named errors, + * falling back to a plain errno error. */ + + switch (error) { + + case -EHOSTDOWN: /* check_for_homed() */ + return sd_varlink_error(link, "io.systemd.CryptEnroll.VolumeUnderForeignManagement", NULL); + + case -ENOPKG: /* credential querying disabled in headless mode but none provided */ + return sd_varlink_error(link, "io.systemd.CryptEnroll.PasswordRequired", NULL); + + case -EPERM: + case -ENOKEY: /* provided password/key did not unlock the volume */ + return sd_varlink_error(link, "io.systemd.CryptEnroll.PasswordIncorrect", NULL); + + case -ENODEV: + case -ENOTUNIQ: /* no (or no unique) FIDO2 device found */ + return sd_varlink_error(link, "io.systemd.CryptEnroll.FidoDeviceNotFound", NULL); + + case -ENOSTR: /* FIDO_ERR_ACTION_TIMEOUT */ + return sd_varlink_error(link, "io.systemd.CryptEnroll.FidoActionTimeout", NULL); + + default: + return sd_varlink_error_errno(link, error); + } +} + +static int vl_method_enroll( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + static const sd_json_dispatch_field dispatch_table[] = { + { "node", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(MethodEnrollParameters, node), SD_JSON_MANDATORY|SD_JSON_STRICT }, + { "mechanism", SD_JSON_VARIANT_STRING, json_dispatch_enroll_type, offsetof(MethodEnrollParameters, mechanism), SD_JSON_MANDATORY }, + { "unlockPassword", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MethodEnrollParameters, unlock_password), SD_JSON_NULLABLE }, + { "unlockKeyFile", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(MethodEnrollParameters, unlock_keyfile), SD_JSON_NULLABLE|SD_JSON_STRICT }, + { "unlockKeyFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_int64, offsetof(MethodEnrollParameters, unlock_keyfile_fd_idx), SD_JSON_NULLABLE }, + { "unlockFido2Device", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MethodEnrollParameters, unlock_fido2_device), SD_JSON_NULLABLE }, + { "unlockTpm2Device", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MethodEnrollParameters, unlock_tpm2_device), SD_JSON_NULLABLE }, + { "password", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MethodEnrollParameters, password), SD_JSON_NULLABLE }, + { "fido2Device", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MethodEnrollParameters, fido2_device), SD_JSON_NULLABLE }, + { "fido2Pin", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MethodEnrollParameters, fido2_pin), SD_JSON_NULLABLE }, + { "fido2WithClientPin", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodEnrollParameters, fido2_with_client_pin), SD_JSON_NULLABLE }, + { "fido2WithUserPresence", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodEnrollParameters, fido2_with_user_presence), SD_JSON_NULLABLE }, + { "fido2WithUserVerification", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodEnrollParameters, fido2_with_user_verification), SD_JSON_NULLABLE }, + { "wipeSlots", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_variant, offsetof(MethodEnrollParameters, wipe_slots), SD_JSON_NULLABLE }, + { "wipeTypes", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_variant, offsetof(MethodEnrollParameters, wipe_types), SD_JSON_NULLABLE }, + VARLINK_DISPATCH_POLKIT_FIELD, + {} + }; + + _cleanup_(method_enroll_parameters_done) MethodEnrollParameters p = { + .mechanism = _ENROLL_TYPE_INVALID, + .unlock_keyfile_fd_idx = -1, + .fido2_with_client_pin = -1, + .fido2_with_user_presence = -1, + .fido2_with_user_verification = -1, + }; + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_(iovec_done_erase) struct iovec vk = {}; + _cleanup_close_ int keyfile_fd = -EBADF; + Hashmap **polkit_registry = ASSERT_PTR(userdata); + int slot, r; + + assert(link); + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (p.mechanism < 0) + return sd_varlink_error_invalid_parameter_name(link, "mechanism"); + + r = varlink_verify_polkit_async( + link, + /* bus= */ NULL, + "io.systemd.cryptenroll.enroll", + /* details= */ NULL, + polkit_registry); + if (r <= 0) + return r; + + /* Populate the context. This is the Varlink equivalent of enroll_context_from_args(). */ + _cleanup_(enroll_context_done) EnrollContext c = ENROLL_CONTEXT_NULL; + c.interactive = false; + c.enroll_type = p.mechanism; + c.unlock_type = _UNLOCK_TYPE_INVALID; + + if (strdup_to(&c.node, p.node) < 0) + return -ENOMEM; + + if (p.unlock_password) { + if (c.unlock_type >= 0) + return sd_varlink_error_invalid_parameter_name(link, "unlockPassword"); + + c.unlock_password = TAKE_PTR(p.unlock_password); + c.unlock_type = UNLOCK_PASSWORD; + } + + if (p.unlock_keyfile || p.unlock_keyfile_fd_idx >= 0) { + if (c.unlock_type >= 0) + return sd_varlink_error_invalid_parameter_name(link, p.unlock_keyfile ? "unlockKeyFile" : "unlockKeyFileDescriptor"); + + if (p.unlock_keyfile && p.unlock_keyfile_fd_idx >= 0) + return sd_varlink_error_invalid_parameter_name(link, "unlockKeyFileDescriptor"); + + if (p.unlock_keyfile_fd_idx >= 0) { + keyfile_fd = sd_varlink_peek_dup_fd(link, p.unlock_keyfile_fd_idx); + if (keyfile_fd < 0) + return sd_varlink_error_invalid_parameter_name(link, "unlockKeyFileDescriptor"); + + if (asprintf(&c.unlock_keyfile, "/proc/self/fd/%i", keyfile_fd) < 0) + return -ENOMEM; + } else + c.unlock_keyfile = TAKE_PTR(p.unlock_keyfile); + + c.unlock_type = UNLOCK_KEYFILE; + } + + if (p.unlock_fido2_device) { + if (c.unlock_type >= 0) + return sd_varlink_error_invalid_parameter_name(link, "unlockFido2Device"); + + if (!streq(p.unlock_fido2_device, "auto")) { + if (!path_is_normalized(p.unlock_fido2_device) || !path_is_absolute(p.unlock_fido2_device)) + return sd_varlink_error_invalid_parameter_name(link, "unlockFido2Device"); + + if (strdup_to(&c.unlock_fido2_device, p.unlock_fido2_device) < 0) + return -ENOMEM; + } + + c.unlock_type = UNLOCK_FIDO2; + } + + if (p.unlock_tpm2_device) { + if (c.unlock_type >= 0) + return sd_varlink_error_invalid_parameter_name(link, "unlockTpm2Device"); + + if (!streq(p.unlock_tpm2_device, "auto")) { + if (!path_is_normalized(p.unlock_tpm2_device) || !path_is_absolute(p.unlock_tpm2_device)) + return sd_varlink_error_invalid_parameter_name(link, "unlockTpm2Device"); + + if (strdup_to(&c.unlock_tpm2_device, p.unlock_tpm2_device) < 0) + return -ENOMEM; + } + + c.unlock_type = UNLOCK_TPM2; + } + + /* If no unlock method is specified, return a recognizable error. We generate invalid parameter name + * for "unlockPassword", simply because it is the best-known unlock method */ + if (c.unlock_type < 0) + return sd_varlink_error_invalid_parameter_name(link, "unlockPassword"); + + /* Mechanism-specific parameters */ + switch (c.enroll_type) { + + case ENROLL_PASSWORD: + if (p.password) { + c.passphrase = TAKE_PTR(p.password); + c.passphrase_size = strlen(c.passphrase); + } + break; + + case ENROLL_RECOVERY: + break; + + case ENROLL_FIDO2: + /* enroll_fido2() requires a concrete device, so if none was given (NULL), discover one. */ + + if (streq_ptr(p.fido2_device, "auto")) + p.fido2_device = mfree(p.fido2_device); + + if (p.fido2_device) { + if (!path_is_normalized(p.fido2_device) || !path_is_absolute(p.fido2_device)) + return sd_varlink_error_invalid_parameter_name(link, "fido2Device"); + + r = strdup_to(&c.fido2_device, p.fido2_device); + if (r < 0) + return r; + } + + if (!c.fido2_device) { + r = fido2_find_device_auto(&c.fido2_device); + if (r < 0) + return varlink_error_for_enroll(link, r); + } + + if (p.fido2_with_client_pin >= 0) + SET_FLAG(c.fido2_lock_with, FIDO2ENROLL_PIN, p.fido2_with_client_pin); + if (p.fido2_with_user_presence >= 0) + SET_FLAG(c.fido2_lock_with, FIDO2ENROLL_UP, p.fido2_with_user_presence); + if (p.fido2_with_user_verification >= 0) + SET_FLAG(c.fido2_lock_with, FIDO2ENROLL_UV, p.fido2_with_user_verification); + + c.fido2_pin = TAKE_PTR(p.fido2_pin); + break; + + default: + return sd_varlink_error_invalid_parameter_name(link, "mechanism"); + } + + if (p.wipe_slots) { + r = parse_wipe_slots(p.wipe_slots, &c); + if (r == -ENOMEM) + return r; + if (r < 0) + return sd_varlink_error_invalid_parameter_name(link, "wipeSlots"); + } + if (p.wipe_types) { + r = parse_wipe_types(p.wipe_types, &c); + if (r == -ENOMEM) + return r; + if (r < 0) + return sd_varlink_error_invalid_parameter_name(link, "wipeTypes"); + } + + /* If the caller asked for 'more', remember the link so we can send progress (e.g. FIDO2 touch). */ + if (FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)) + c.link = sd_varlink_ref(link); + + r = DLOPEN_CRYPTSETUP(LOG_DEBUG, SD_ELF_NOTE_DLOPEN_PRIORITY_REQUIRED); + if (r < 0) + return r; + + r = prepare_luks(&c, &cd, &vk); + if (r < 0) + return varlink_error_for_enroll(link, r); + + _cleanup_(erase_and_freep) char *recovery_key = NULL; + slot = enroll_now(&c, cd, &vk, &recovery_key); + if (slot < 0) + return varlink_error_for_enroll(link, slot); + + /* Wipe any slots the caller selected, keeping the one we just enrolled. */ + c.wipe_except_slot = slot; + _cleanup_free_ int *wiped_slots = NULL; + size_t n_wiped_slots = 0; + r = wipe_slots(&c, cd, &wiped_slots, &n_wiped_slots); + if (r < 0) + return varlink_error_for_enroll(link, r); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *wiped_slots_json = NULL; + FOREACH_ARRAY(s, wiped_slots, n_wiped_slots) { + r = sd_json_variant_append_arrayb(&wiped_slots_json, SD_JSON_BUILD_INTEGER(*s)); + if (r < 0) + return r; + } + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *rk = NULL; + if (recovery_key) { + r = sd_json_variant_new_string(&rk, recovery_key); + if (r < 0) + return r; + + sd_json_variant_sensitive(rk); + } + + return sd_varlink_replybo( + link, + SD_JSON_BUILD_PAIR_INTEGER("slot", slot), + JSON_BUILD_PAIR_VARIANT_NON_NULL("wipedSlots", wiped_slots_json), + JSON_BUILD_PAIR_VARIANT_NON_NULL("recoveryKey", rk)); +} + +static int vl_method_list_slots( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + static const sd_json_dispatch_field dispatch_table[] = { + { "node", SD_JSON_VARIANT_STRING, json_dispatch_const_path, 0, SD_JSON_MANDATORY|SD_JSON_STRICT }, + {} + }; + + _cleanup_(enroll_context_done) EnrollContext c = ENROLL_CONTEXT_NULL; + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_free_ EnrolledSlot *slots = NULL; + const char *node = NULL; + size_t n_slots; + int r; + + assert(link); + + if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)) + return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, NULL); + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &node); + if (r != 0) + return r; + + /* NB: No polkit authentication here for now, given this is read access only. */ + + c.interactive = false; + if (strdup_to(&c.node, node) < 0) + return -ENOMEM; + + r = DLOPEN_CRYPTSETUP(LOG_DEBUG, SD_ELF_NOTE_DLOPEN_PRIORITY_REQUIRED); + if (r < 0) + return r; + + /* We only need to read the LUKS2 header, no volume key required. */ + r = prepare_luks(&c, &cd, /* ret_volume_key= */ NULL); + if (r < 0) + return varlink_error_for_enroll(link, r); + + r = collect_enrolled_slots(cd, &slots, &n_slots); + if (r < 0) + return r; + + FOREACH_ARRAY(s, slots, n_slots) { + /* enroll_type_to_string() returns NULL for unrecognized types; conflicts are reported + * with no type too. The dash spelling is underscorified for the wire by the build macro. */ + const char *type = s->conflict ? NULL : enroll_type_to_string(s->type); + + r = sd_varlink_notifybo( + link, + SD_JSON_BUILD_PAIR_INTEGER("slot", s->slot), + JSON_BUILD_PAIR_STRING_NON_EMPTY_UNDERSCORIFY("type", type)); + if (r < 0) + return r; + } + + return sd_varlink_reply(link, NULL); +} + +int cryptenroll_varlink_server(void) { + _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL; + _cleanup_hashmap_free_ Hashmap *polkit_registry = NULL; + int r; + + r = varlink_server_new( + &varlink_server, + SD_VARLINK_SERVER_ROOT_ONLY|SD_VARLINK_SERVER_MYSELF_ONLY|SD_VARLINK_SERVER_INPUT_SENSITIVE|SD_VARLINK_SERVER_INHERIT_USERDATA|SD_VARLINK_SERVER_ALLOW_FD_PASSING_INPUT|SD_VARLINK_SERVER_HANDLE_SIGINT|SD_VARLINK_SERVER_HANDLE_SIGTERM, + &polkit_registry); + if (r < 0) + return log_error_errno(r, "Failed to allocate Varlink server: %m"); + + r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_CryptEnroll); + if (r < 0) + return log_error_errno(r, "Failed to add Varlink interface: %m"); + + r = sd_varlink_server_bind_method_many( + varlink_server, + "io.systemd.CryptEnroll.Enroll", vl_method_enroll, + "io.systemd.CryptEnroll.ListSlots", vl_method_list_slots); + if (r < 0) + return log_error_errno(r, "Failed to bind Varlink methods: %m"); + + r = sd_varlink_server_loop_auto(varlink_server); + if (r < 0) + return log_error_errno(r, "Failed to run Varlink event loop: %m"); + + return 0; +} diff --git a/src/cryptenroll/cryptenroll-varlink.h b/src/cryptenroll/cryptenroll-varlink.h new file mode 100644 index 0000000000..89efede84b --- /dev/null +++ b/src/cryptenroll/cryptenroll-varlink.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "cryptenroll.h" + +/* Sends a progress 'state' notification over c->link, if (and only if) the enrollment was triggered via a + * Varlink call with the 'more' flag set. A no-op (returning 0) otherwise. */ +int enroll_context_notify_state(const EnrollContext *c, const char *state); + +int cryptenroll_varlink_server(void); diff --git a/src/cryptenroll/cryptenroll-wipe.c b/src/cryptenroll/cryptenroll-wipe.c index a8638d2b3a..39e8593973 100644 --- a/src/cryptenroll/cryptenroll-wipe.c +++ b/src/cryptenroll/cryptenroll-wipe.c @@ -296,24 +296,29 @@ 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, + int **ret_wiped_slots, + size_t *ret_n_wiped_slots) { _cleanup_set_free_ Set *wipe_slots = NULL, *wipe_tokens = NULL, *keep_slots = NULL; - _cleanup_free_ int *ordered_slots = NULL, *ordered_tokens = NULL; - size_t n_ordered_slots = 0, n_ordered_tokens = 0; + _cleanup_free_ int *ordered_slots = NULL, *ordered_tokens = NULL, *wiped_slots = NULL; + size_t n_ordered_slots = 0, n_ordered_tokens = 0, n_wiped_slots = 0; int r, slot_max, ret; void *e; + assert_se(c); assert_se(cd); + assert_se(!!ret_wiped_slots == !!ret_n_wiped_slots); /* 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) { + if (ret_wiped_slots) + *ret_wiped_slots = NULL; + if (ret_n_wiped_slots) + *ret_n_wiped_slots = 0; 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 +339,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 +378,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,11 +414,22 @@ 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."); + if (ret_wiped_slots) + *ret_wiped_slots = NULL; + if (ret_n_wiped_slots) + *ret_n_wiped_slots = 0; return 0; } + /* Remember which slots we actually managed to wipe, so we can report them back to the caller. */ + if (ret_wiped_slots) { + wiped_slots = new(int, n_ordered_slots); + if (!wiped_slots) + return log_oom(); + } + if (DEBUG_LOGGING) { for (size_t i = 0; i < n_ordered_slots; i++) log_debug("Going to wipe slot %i.", ordered_slots[i]); @@ -433,8 +449,11 @@ int wipe_slots(struct crypt_device *cd, log_warning_errno(r, "Failed to wipe slot %i, continuing: %m", ordered_slots[i - 1]); if (ret == 0) ret = r; - } else + } else { log_info("Wiped slot %i.", ordered_slots[i - 1]); + if (wiped_slots) + wiped_slots[n_wiped_slots++] = ordered_slots[i - 1]; + } } for (size_t i = n_ordered_tokens; i > 0; i--) { @@ -446,5 +465,13 @@ int wipe_slots(struct crypt_device *cd, } } + if (ret_wiped_slots) { + /* We wiped from back to front, restore ascending order before handing the list out. */ + typesafe_qsort(wiped_slots, n_wiped_slots, cmp_int); + *ret_wiped_slots = TAKE_PTR(wiped_slots); + } + if (ret_n_wiped_slots) + *ret_n_wiped_slots = n_wiped_slots; + return ret; } diff --git a/src/cryptenroll/cryptenroll-wipe.h b/src/cryptenroll/cryptenroll-wipe.h index aa82f9dbbd..cb3dd71933 100644 --- a/src/cryptenroll/cryptenroll-wipe.h +++ b/src/cryptenroll/cryptenroll-wipe.h @@ -1,13 +1,11 @@ /* 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). If ret_wiped_slots/ + * ret_n_wiped_slots are non-NULL they receive the (ascendingly sorted) list of slots that were actually + * wiped. */ +int wipe_slots(const EnrollContext *c, struct crypt_device *cd, int **ret_wiped_slots, size_t *ret_n_wiped_slots); diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index eed0a11aeb..67386f31d8 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -3,34 +3,42 @@ #include #include "sd-device.h" +#include "sd-varlink.h" +#include "alloc-util.h" #include "blockdev-list.h" #include "blockdev-util.h" #include "build.h" +#include "cleanup-util.h" #include "cryptenroll.h" #include "cryptenroll-fido2.h" +#include "cryptenroll-interactive.h" #include "cryptenroll-list.h" #include "cryptenroll-password.h" #include "cryptenroll-pkcs11.h" #include "cryptenroll-recovery.h" #include "cryptenroll-tpm2.h" +#include "cryptenroll-varlink.h" #include "cryptenroll-wipe.h" #include "cryptsetup-util.h" #include "extract-word.h" -#include "fileio.h" #include "format-table.h" +#include "help-util.h" +#include "initrd-util.h" #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" #include "parse-util.h" #include "pkcs11-util.h" -#include "pretty-print.h" #include "process-util.h" +#include "prompt-util.h" #include "string-table.h" #include "string-util.h" +#include "terminal-util.h" #include "tpm2-pcr.h" #include "tpm2-util.h" @@ -60,6 +68,10 @@ static int *arg_wipe_slots = NULL; static size_t arg_n_wipe_slots = 0; static WipeScope arg_wipe_slots_scope = WIPE_EXPLICIT; static unsigned arg_wipe_slots_mask = 0; /* Bitmask of (1U << EnrollType), for wiping all slots of specific types */ +static bool arg_firstboot = false; +static bool arg_chrome = true; +static bool arg_mute_console = false; +static unsigned arg_prompt_suppress_mask = 0; /* Bitmask of (1U << EnrollType): if any such slot exists, --firstboot does nothing */ static Fido2EnrollFlags arg_fido2_lock_with = FIDO2ENROLL_PIN | FIDO2ENROLL_UP; #if HAVE_LIBFIDO2 static int arg_fido2_cred_alg = COSE_ES256; @@ -110,6 +122,91 @@ static const char *const luks2_token_type_table[_ENROLL_TYPE_MAX] = { DEFINE_STRING_TABLE_LOOKUP(luks2_token_type, EnrollType); +static int enroll_type_mask_from_string(const char *name) { + assert(name); + + /* Maps an enroll type name (command line spelling) to its (1U << EnrollType) bitmask, or returns a + * negative errno if the name is not a known type. Callers merge the returned mask into their own + * accumulator. Shared by the various places that parse type lists into a bitmask. */ + + EnrollType t = enroll_type_from_string(name); + if (t < 0) + return -EINVAL; + + return 1 << t; +} + +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->unlock_password = erase_and_free(c->unlock_password); + 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 resolve_default_node(const char *path, char **ret) { + int r; + + /* Resolves the path of the underlying LUKS2 block device of the file system at the given mount + * point, i.e. the raw partition behind the dm-crypt mapping that file system sits on. Returns + * -ENXIO if the file system is not backed by a (single) LUKS2 device. Logs only at debug level, so + * the caller can try the next candidate path. */ + + assert(path); + assert(ret); + + dev_t devno; + r = get_block_device(path, &devno); + if (r < 0) + return log_debug_errno(r, "Failed to determine block device backing %s: %m", path); + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(ENXIO), + "File system %s is not backed by a (single) whole block device.", path); + + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + r = sd_device_new_from_devnum(&dev, 'b', devno); + if (r < 0) + return log_debug_errno(r, "Unable to access backing block device for %s: %m", path); + + const char *dm_uuid; + r = sd_device_get_property_value(dev, "DM_UUID", &dm_uuid); + if (r == -ENOENT) + return log_debug_errno(SYNTHETIC_ERRNO(ENXIO), "Backing block device of %s is not a DM device.", path); + if (r < 0) + return log_debug_errno(r, "Unable to query DM_UUID udev property of backing block device for %s: %m", path); + + if (!startswith(dm_uuid, "CRYPT-LUKS2-")) + return log_debug_errno(SYNTHETIC_ERRNO(ENXIO), "Block device backing %s is not a LUKS2 device.", path); + + _cleanup_(sd_device_unrefp) sd_device *origin = NULL; + r = block_device_get_originating(dev, &origin, /* recursive= */ false); + if (r < 0) + return log_debug_errno(r, "Failed to get originating device of LUKS2 device backing %s: %m", path); + + const char *dp; + r = sd_device_get_devname(origin, &dp); + if (r < 0) + return log_debug_errno(r, "Failed to get device path for LUKS2 device backing %s: %m", path); + + return strdup_to(ret, dp); +} + static int determine_default_node(void) { int r; @@ -127,47 +224,30 @@ static int determine_default_node(void) { * Or to say this differently: it makes sense to support well systems with /var/ being on /. It also * makes sense to support well systems with them being separate, and /var/ being variable and * persistent. But any other kind of system appears much less interesting to support, and in that - * case people should just specify the device name explicitly. */ + * case people should just specify the device name explicitly. + * + * When invoked from the initrd the host's file systems are not mounted at their final location yet, + * but below /sysroot/, hence look there instead. */ - dev_t devno; - r = get_block_device("/var", &devno); - if (r < 0) - return log_error_errno(r, "Failed to determine block device backing /var/: %m"); - if (r == 0) - return log_error_errno(SYNTHETIC_ERRNO(ENXIO), - "File system /var/ is on not backed by a (single) whole block device."); + const char *candidates[2]; + size_t n_candidates = 0; - _cleanup_(sd_device_unrefp) sd_device *dev = NULL; - r = sd_device_new_from_devnum(&dev, 'b', devno); - if (r < 0) - return log_error_errno(r, "Unable to access backing block device for /var/: %m"); + if (in_initrd()) { + candidates[n_candidates++] = "/sysroot/var"; + candidates[n_candidates++] = "/sysroot"; + } else + candidates[n_candidates++] = "/var"; - const char *dm_uuid; - r = sd_device_get_property_value(dev, "DM_UUID", &dm_uuid); - if (r == -ENOENT) - return log_error_errno(r, "Backing block device of /var/ is not a DM device: %m"); - if (r < 0) - return log_error_errno(r, "Unable to query DM_UUID udev property of backing block device for /var/: %m"); + FOREACH_ARRAY(path, candidates, n_candidates) { + r = resolve_default_node(*path, &arg_node); + if (r >= 0) { + log_info("No device specified, defaulting to '%s' (backing %s).", arg_node, *path); + return 0; + } + } - if (!startswith(dm_uuid, "CRYPT-LUKS2-")) - return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "Block device backing /var/ is not a LUKS2 device."); - - _cleanup_(sd_device_unrefp) sd_device *origin = NULL; - r = block_device_get_originating(dev, &origin, /* recursive= */ false); - if (r < 0) - return log_error_errno(r, "Failed to get originating device of LUKS2 device backing /var/: %m"); - - const char *dp; - r = sd_device_get_devname(origin, &dp); - if (r < 0) - return log_error_errno(r, "Failed to get device path for LUKS2 device backing /var/: %m"); - - r = free_and_strdup_warn(&arg_node, dp); - if (r < 0) - return r; - - log_info("No device specified, defaulting to '%s'.", arg_node); - return 0; + return log_error_errno(SYNTHETIC_ERRNO(ENXIO), + "Failed to automatically determine a LUKS2 block device to operate on, please specify one explicitly."); } static int parse_wipe_slot(const char *arg) { @@ -190,21 +270,15 @@ static int parse_wipe_slot(const char *arg) { if (r < 0) return log_error_errno(r, "Failed to parse slot list: %s", arg); + int mask; + if (streq(slot, "all")) arg_wipe_slots_scope = WIPE_ALL; else if (streq(slot, "empty")) { if (arg_wipe_slots_scope != WIPE_ALL) /* if "all" was specified before, that wins */ arg_wipe_slots_scope = WIPE_EMPTY_PASSPHRASE; - } else if (streq(slot, "password")) - arg_wipe_slots_mask |= 1U << ENROLL_PASSWORD; - else if (streq(slot, "recovery")) - arg_wipe_slots_mask |= 1U << ENROLL_RECOVERY; - else if (streq(slot, "pkcs11")) - arg_wipe_slots_mask |= 1U << ENROLL_PKCS11; - else if (streq(slot, "fido2")) - arg_wipe_slots_mask |= 1U << ENROLL_FIDO2; - else if (streq(slot, "tpm2")) - arg_wipe_slots_mask |= 1U << ENROLL_TPM2; + } else if ((mask = enroll_type_mask_from_string(slot)) >= 0) + arg_wipe_slots_mask |= mask; else { unsigned n; @@ -222,15 +296,34 @@ static int parse_wipe_slot(const char *arg) { } } -static int help(void) { - _cleanup_free_ char *link = NULL; +static int parse_prompt_suppress(const char *arg) { int r; - pager_open(arg_pager_flags); + assert(arg); - r = terminal_urlify_man("systemd-cryptenroll", "1", &link); - if (r < 0) - return log_oom(); + /* Parses a comma-separated list of the slot types the --firstboot wizard knows how to enroll. If a + * slot of any listed type already exists on the volume, the wizard does nothing. */ + + for (const char *p = arg;;) { + _cleanup_free_ char *type = NULL; + int mask; + + r = extract_first_word(&p, &type, ",", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == 0) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to parse type list: %s", arg); + + mask = enroll_type_mask_from_string(type); + if (mask < 0) + return log_error_errno(mask, "Unknown slot type: %s", type); + + arg_prompt_suppress_mask |= mask; + } +} + +static int help(void) { + int r; static const char* const groups[] = { NULL, @@ -252,21 +345,20 @@ static int help(void) { (void) table_sync_column_widths(0, tables[0], tables[1], tables[2], tables[3], tables[4], tables[5]); - printf("%s [OPTIONS...] [BLOCK-DEVICE]\n\n" - "%sEnroll a security token or authentication credential to a LUKS volume.%s\n", - program_invocation_short_name, - ansi_highlight(), - ansi_normal()); + pager_open(arg_pager_flags); + + help_cmdline("[OPTIONS...] [BLOCK-DEVICE]"); + help_abstract("Enroll a security token or authentication credential to a LUKS volume."); for (size_t i = 0; i < ELEMENTSOF(groups); i++) { - printf("\n%s%s:%s\n", ansi_underline(), groups[i] ?: "Options", ansi_normal()); + help_section(groups[i] ?: "Options"); r = table_print_or_warn(tables[i]); if (r < 0) return r; } - printf("\nSee the %s for details.\n", link); + help_man_page_reference("systemd-cryptenroll", "1"); return 0; } @@ -305,8 +397,42 @@ static int parse_argv(int argc, char *argv[]) { return r; break; + OPTION_LONG("firstboot", NULL, + "Interactively enroll a credential (first-boot wizard)"): + arg_firstboot = true; + break; + + OPTION_LONG("prompt-suppress", "TYPE1,TYPE2,…", + "Skip the --firstboot wizard if a slot of any listed type exists"): + r = parse_prompt_suppress(opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("chrome", "BOOL", + "In first-boot mode: if false don't show colour bar at top and bottom of terminal"): + r = parse_boolean_argument("--chrome=", opts.arg, &arg_chrome); + if (r < 0) + return r; + break; + + OPTION_LONG("mute-console", "BOOL", + "In first-boot mode, tell kernel/PID 1 to not write to the console while running"): + r = parse_boolean_argument("--mute-console=", opts.arg, &arg_mute_console); + if (r < 0) + return r; + break; + OPTION_GROUP("Unlocking"): {} + OPTION_LONG("unlock-empty", NULL, "Use an empty password to unlock the volume"): + if (arg_unlock_type != UNLOCK_PASSWORD) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Multiple unlock methods specified at once, refusing."); + + arg_unlock_type = UNLOCK_EMPTY; + break; + OPTION_LONG("unlock-key-file", "PATH", "Use a file to unlock the volume"): if (arg_unlock_type != UNLOCK_PASSWORD) @@ -362,6 +488,14 @@ static int parse_argv(int argc, char *argv[]) { break; } + OPTION_LONG("unlock-headless", NULL, "Try the 'headless' unlock mechanisms in turn"): + if (arg_unlock_type != UNLOCK_PASSWORD) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Multiple unlock methods specified at once, refusing."); + + arg_unlock_type = UNLOCK_HEADLESS; + break; + OPTION_GROUP("Simple Enrollment"): {} OPTION_LONG("password", NULL, @@ -582,6 +716,17 @@ static int parse_argv(int argc, char *argv[]) { if (option_parser_get_n_args(&opts) > 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments, refusing."); + if (arg_firstboot) { + if (arg_enroll_type >= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--firstboot may not be combined with an explicit enrollment type, refusing."); + if (wipe_requested()) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--firstboot may not be combined with --wipe-slot=, refusing."); + } else if (arg_prompt_suppress_mask != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--prompt-suppress= is only useful together with --firstboot, refusing."); + const char *arg = option_parser_get_arg(&opts, 0); if (arg) r = parse_path_argument(arg, false, &arg_node); @@ -660,54 +805,18 @@ static int check_for_homed(struct crypt_device *cd) { return 0; } -static int load_volume_key_keyfile( - struct crypt_device *cd, - void *ret_vk, - size_t *ret_vks) { - - _cleanup_(erase_and_freep) char *password = NULL; - size_t password_len; - int r; - - assert_se(cd); - assert_se(ret_vk); - assert_se(ret_vks); - - r = read_full_file_full( - AT_FDCWD, - arg_unlock_keyfile, - UINT64_MAX, - SIZE_MAX, - READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET, - NULL, - &password, - &password_len); - if (r < 0) - return log_error_errno(r, "Reading keyfile %s failed: %m", arg_unlock_keyfile); - - r = sym_crypt_volume_key_get( - cd, - CRYPT_ANY_SLOT, - ret_vk, - ret_vks, - password, - password_len); - if (r < 0) - return log_error_errno(r, "Unlocking via keyfile failed: %m"); - - return r; -} - -static int prepare_luks( +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 +824,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 +840,52 @@ 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_EMPTY: + r = load_volume_key_empty(c, cd, &vk); + break; 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; + + case UNLOCK_HEADLESS: + if (tpm2_is_mostly_supported()) { + log_info("TPM2 support available, trying unlocking via TPM2…"); + + r = load_volume_key_tpm2(c, cd, &vk); + if (r >= 0) + break; + + log_info("TPM2 unlocking didn't work, trying unlocking via empty password…"); + } else + log_info("TPM2 support not available, trying unlocking via empty password…"); + + r = load_volume_key_empty(c, cd, &vk); break; default: return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown LUKS unlock method"); } - if (r < 0) return r; @@ -769,13 +895,133 @@ 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; +} + +int enroll_now( + const EnrollContext *c, + struct crypt_device *cd, + const struct iovec *volume_key, + char **ret_recovery_key) { + + int slot, slot_to_wipe = -1, r; + + assert(c); + assert(cd); + assert(iovec_is_set(volume_key)); + + switch (c->enroll_type) { + + case ENROLL_PASSWORD: + return enroll_password(c, cd, volume_key); + + case ENROLL_RECOVERY: + return enroll_recovery(c, cd, volume_key, ret_recovery_key); + + case ENROLL_PKCS11: + return enroll_pkcs11(c, cd, volume_key); + + case ENROLL_FIDO2: + return enroll_fido2(c, cd, volume_key); + + case ENROLL_TPM2: + slot = enroll_tpm2(c, cd, volume_key, &slot_to_wipe); + if (slot < 0) + return slot; + + if (slot_to_wipe >= 0) { + assert(slot != slot_to_wipe); + + /* Updating the PIN on an existing enrollment: wipe just that one slot. This is an + * internal one-off wipe, unrelated to the user's wipe selection, so use a throwaway + * context referencing a single explicit slot. */ + _cleanup_(enroll_context_done) EnrollContext wipe_ctx = ENROLL_CONTEXT_NULL; + wipe_ctx.wipe_slots = newdup(int, &slot_to_wipe, 1); + if (!wipe_ctx.wipe_slots) + return log_oom(); + + wipe_ctx.n_wipe_slots = 1; + + r = wipe_slots(&wipe_ctx, cd, /* ret_wiped_slots= */ NULL, /* ret_n_wiped_slots= */ NULL); + if (r < 0) + return r; + } + + return slot; + + default: + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Operation not implemented yet."); + } +} + static int run(int argc, char *argv[]) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; _cleanup_(iovec_done_erase) struct iovec vk = {}; - int slot, slot_to_wipe, r; + _cleanup_(enroll_context_done) EnrollContext c = ENROLL_CONTEXT_NULL; + int slot, r; log_setup(); + /* A delicious drop of snake oil */ + (void) safe_mlockall(MCL_CURRENT|MCL_FUTURE|MCL_ONFAULT); + + /* If invoked as a Varlink service, hand off to the Varlink server and don't process the command + * line any further. */ + r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT); + if (r < 0) + return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); + if (r > 0) + return cryptenroll_varlink_server(); + r = parse_argv(argc, argv); if (r <= 0) return r; @@ -784,72 +1030,66 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; - /* 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 */ - else - r = prepare_luks(&cd, &vk); + r = enroll_context_from_args(&c); if (r < 0) return r; - switch (arg_enroll_type) { + _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *mute_console_link = NULL; - case ENROLL_PASSWORD: - slot = enroll_password(cd, &vk); - break; + /* Ensure the interactive chrome (drawn by cryptenroll_run_interactive() in --firstboot mode) is + * always torn down on exit; chrome_hide() is a no-op if no chrome was shown. */ + DEFER_VOID_CALL(chrome_hide); - case ENROLL_RECOVERY: - slot = enroll_recovery(cd, &vk); - break; + if (arg_firstboot) { + assert(c.enroll_type < 0); - case ENROLL_PKCS11: - slot = enroll_pkcs11(cd, &vk, arg_pkcs11_token_uri); - break; + r = cryptenroll_run_interactive( + &c, + arg_prompt_suppress_mask, + arg_chrome, + arg_mute_console ? &mute_console_link : NULL); + if (r <= 0) + return r; - 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); - break; + assert(c.enroll_type >= 0); - 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); + } else if (c.enroll_type < 0) { + /* If we are called without anything to enroll, we just need the LUKS device, not the volume key. */ + r = prepare_luks(&c, &cd, /* ret_volume_key= */ NULL); + if (r < 0) + return r; - 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); - if (r < 0) - return r; - } - break; - case _ENROLL_TYPE_INVALID: /* List enrolled slots if we are called without anything to enroll or wipe */ if (!wipe_requested()) 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); - - default: - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Operation not implemented yet."); + return wipe_slots(&c, cd, /* ret_wiped_slots= */ NULL, /* ret_n_wiped_slots= */ NULL); } - 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); + r = prepare_luks(&c, &cd, &vk); if (r < 0) - return r; + goto finish; - return 0; + slot = enroll_now(&c, cd, &vk, /* ret_recovery_key= */ NULL); + if (slot < 0) { + r = slot; + goto finish; + } + + /* After we completed enrolling, remove user selected slots (keeping the one we just added) */ + c.wipe_except_slot = slot; + r = wipe_slots(&c, cd, /* ret_wiped_slots= */ NULL, /* ret_n_wiped_slots= */ NULL); + if (r < 0) + goto finish; + + r = 0; + +finish: + if (arg_firstboot) + (void) any_key_to_proceed(); + + return r; } DEFINE_MAIN_FUNCTION(run); diff --git a/src/cryptenroll/cryptenroll.h b/src/cryptenroll/cryptenroll.h index 1066ecc18d..5fdb4e713c 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 { @@ -18,6 +19,8 @@ typedef enum UnlockType { UNLOCK_KEYFILE, UNLOCK_FIDO2, UNLOCK_TPM2, + UNLOCK_EMPTY, + UNLOCK_HEADLESS, _UNLOCK_TYPE_MAX, _UNLOCK_TYPE_INVALID = -EINVAL, } UnlockType; @@ -32,3 +35,89 @@ 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; + char *unlock_password; /* used by Varlink; NULL on CLI path */ + + /* 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); + +/* Opens & loads the LUKS2 superblock of c->node, refuses homed-managed volumes, and (if ret_volume_key is + * non-NULL) unlocks it according to c->unlock_type, returning the volume key. */ +int prepare_luks(const EnrollContext *c, struct crypt_device **ret_cd, struct iovec *ret_volume_key); + +/* Dispatches to the enroll_*() helper matching c->enroll_type and returns the keyslot the new credential + * was added to. For ENROLL_RECOVERY the generated key is returned via ret_recovery_key (if non-NULL). + * Defined in cryptenroll.c, shared by the command line and Varlink code paths. */ +int enroll_now(const EnrollContext *c, struct crypt_device *cd, const struct iovec *volume_key, char **ret_recovery_key); diff --git a/src/cryptenroll/io.systemd.cryptenroll.policy b/src/cryptenroll/io.systemd.cryptenroll.policy new file mode 100644 index 0000000000..80efa19957 --- /dev/null +++ b/src/cryptenroll/io.systemd.cryptenroll.policy @@ -0,0 +1,30 @@ + + + + + + + + The systemd Project + https://systemd.io + + + Enroll an unlock mechanism into an encrypted volume + Authentication is required to enroll a new unlock mechanism into an encrypted volume. + + auth_admin + auth_admin + auth_admin_keep + + + diff --git a/src/cryptenroll/meson.build b/src/cryptenroll/meson.build index aa789c0a07..3fbb5bf080 100644 --- a/src/cryptenroll/meson.build +++ b/src/cryptenroll/meson.build @@ -7,11 +7,13 @@ endif systemd_cryptenroll_sources = files( 'cryptenroll.c', 'cryptenroll-fido2.c', + 'cryptenroll-interactive.c', 'cryptenroll-list.c', 'cryptenroll-password.c', 'cryptenroll-pkcs11.c', 'cryptenroll-recovery.c', 'cryptenroll-tpm2.c', + 'cryptenroll-varlink.c', 'cryptenroll-wipe.c', ) @@ -28,3 +30,6 @@ executables += [ ], }, ] + +install_data('io.systemd.cryptenroll.policy', + install_dir : polkitpolicydir) diff --git a/src/home/homectl-fido2.c b/src/home/homectl-fido2.c index d351b1db5e..210c050284 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/libsystemd/sd-varlink/test-varlink-idl.c b/src/libsystemd/sd-varlink/test-varlink-idl.c index a1ed9a0f42..ae4a6d21b8 100644 --- a/src/libsystemd/sd-varlink/test-varlink-idl.c +++ b/src/libsystemd/sd-varlink/test-varlink-idl.c @@ -21,6 +21,7 @@ #include "varlink-io.systemd.AskPassword.h" #include "varlink-io.systemd.BootControl.h" #include "varlink-io.systemd.Credentials.h" +#include "varlink-io.systemd.CryptEnroll.h" #include "varlink-io.systemd.FactoryReset.h" #include "varlink-io.systemd.Hostname.h" #include "varlink-io.systemd.Import.h" @@ -199,6 +200,7 @@ TEST(parse_format) { &vl_interface_io_systemd_AskPassword, &vl_interface_io_systemd_BootControl, &vl_interface_io_systemd_Credentials, + &vl_interface_io_systemd_CryptEnroll, &vl_interface_io_systemd_FactoryReset, &vl_interface_io_systemd_Hostname, &vl_interface_io_systemd_Import, diff --git a/src/shared/libfido2-util.c b/src/shared/libfido2-util.c index 5f69dc8924..4a0d7e6d64 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; + _cleanup_strv_free_erase_ char **pins = NULL; - 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; - } - - 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(); + } 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; + } + + _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(); + } + + 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; } - 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, askpw_flags, &pin); - if (r < 0) - return log_error_errno(r, "Failed to acquire user PIN: %m"); - - 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)); @@ -1300,6 +1319,109 @@ finish: #endif } +void fido2_device_info_done(Fido2DeviceInfo *d) { + assert(d); + + d->path = mfree(d->path); + d->manufacturer = mfree(d->manufacturer); + d->product = mfree(d->product); +} + +void fido2_device_info_free_many(Fido2DeviceInfo *a, size_t n) { + FOREACH_ARRAY(i, a, n) + fido2_device_info_done(i); + + free(a); +} + +int fido2_enumerate_devices(Fido2DeviceInfo **ret, size_t *ret_n) { +#if HAVE_LIBFIDO2 + Fido2DeviceInfo *devices = NULL; + size_t n_devices = 0, allocated = 64, found = 0; + fido_dev_info_t *di = NULL; + int r; + + CLEANUP_ARRAY(devices, n_devices, fido2_device_info_free_many); + + assert(ret); + assert(ret_n); + + r = dlopen_libfido2(LOG_ERR); + if (r < 0) + return r; + + di = sym_fido_dev_info_new(allocated); + if (!di) + return log_oom(); + + r = sym_fido_dev_info_manifest(di, allocated, &found); + if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) { + /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ + r = 0; + goto finish; + } + if (r != FIDO_OK) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); + goto finish; + } + + for (size_t i = 0; i < found; i++) { + bool has_rk, has_client_pin, has_up, has_uv, has_always_uv; + const fido_dev_info_t *entry; + const char *path; + + entry = sym_fido_dev_info_ptr(di, i); + if (!entry) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to get device information for FIDO device %zu.", i); + goto finish; + } + + path = sym_fido_dev_info_path(entry); + + r = check_device_is_fido2_with_hmac_secret(path, &has_rk, &has_client_pin, &has_up, &has_uv, &has_always_uv); + if (r < 0) + goto finish; + if (r == 0) /* Not a FIDO2 device suitable for enrollment, skip it */ + continue; + + if (!GREEDY_REALLOC(devices, n_devices + 1)) { + r = log_oom(); + goto finish; + } + + Fido2DeviceInfo *d = devices + n_devices; + *d = (Fido2DeviceInfo) {}; + + /* The manufacturer / product strings are optional (NULL when the device doesn't report + * them), but a failure to duplicate any string is a genuine OOM and thus fatal. */ + if (strdup_to(&d->path, path) < 0 || + strdup_to(&d->manufacturer, empty_to_null(sym_fido_dev_info_manufacturer_string(entry))) < 0 || + strdup_to(&d->product, empty_to_null(sym_fido_dev_info_product_string(entry))) < 0) { + fido2_device_info_done(d); + r = log_oom(); + goto finish; + } + + n_devices++; + } + + r = 0; + +finish: + sym_fido_dev_info_free(&di, allocated); + if (r < 0) + return r; + + *ret = TAKE_PTR(devices); + *ret_n = n_devices; + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "FIDO2 tokens not supported on this build."); +#endif +} + int fido2_find_device_auto(char **ret) { #if HAVE_LIBFIDO2 _cleanup_free_ char *copy = NULL; diff --git a/src/shared/libfido2-util.h b/src/shared/libfido2-util.h index bbf6d1ca66..12a0201332 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, @@ -135,4 +137,17 @@ static inline int parse_fido2_algorithm(const char *s, int *ret) { int fido2_list_devices(void); int fido2_find_device_auto(char **ret); +typedef struct Fido2DeviceInfo { + char *path; + char *manufacturer; /* may be NULL if the device doesn't report it */ + char *product; /* may be NULL if the device doesn't report it */ +} Fido2DeviceInfo; + +void fido2_device_info_done(Fido2DeviceInfo *d); +void fido2_device_info_free_many(Fido2DeviceInfo *a, size_t n); + +/* Enumerates the FIDO2 tokens that implement the 'hmac-secret' extension (i.e. are suitable for + * enrollment), returning a newly allocated array. */ +int fido2_enumerate_devices(Fido2DeviceInfo **ret, size_t *ret_n); + int fido2_have_device(const char *device); diff --git a/src/shared/meson.build b/src/shared/meson.build index 1b1ab668b2..7c6505e015 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -224,6 +224,7 @@ shared_sources = files( 'varlink-io.systemd.AskPassword.c', 'varlink-io.systemd.BootControl.c', 'varlink-io.systemd.Credentials.c', + 'varlink-io.systemd.CryptEnroll.c', 'varlink-io.systemd.FactoryReset.c', 'varlink-io.systemd.Hostname.c', 'varlink-io.systemd.Import.c', diff --git a/src/shared/prompt-util.c b/src/shared/prompt-util.c index 69728e0cb4..a7bcfe32e3 100644 --- a/src/shared/prompt-util.c +++ b/src/shared/prompt-util.c @@ -213,6 +213,47 @@ int prompt_loop( } } +static int boolean_is_valid(const char *name, void *userdata) { + return parse_boolean(name) >= 0; +} + +int prompt_loop_yes_no(const char *question, bool def, bool *ret) { + int r; + + assert(question); + assert(ret); + + char **l = STRV_MAKE("yes", "no"); + + _cleanup_free_ char *reply = NULL; + r = prompt_loop(question, + GLYPH_WARNING_SIGN, + /* prefill= */ def ? "yes" : "no", + /* menu= */ l, + /* accepted= */ NULL, + /* ellipsize_percentage= */ 20, + /* n_columns= */ 2, + /* column_width= */ 40, + /* is_valid= */ boolean_is_valid, + /* refresh= */ NULL, + /* userdata= */ NULL, + PROMPT_SHOW_MENU|PROMPT_MAY_SKIP|PROMPT_HIDE_MENU_HINT|PROMPT_HIDE_SKIP_HINT, + &reply); + if (r < 0) + return r; + if (r == 0) { /* Skipped (empty input): fall back to the default. */ + *ret = def; + return 0; + } + + r = parse_boolean(reply); + if (r < 0) + return log_error_errno(r, "Failed to parse reply: %s", reply); + + *ret = r; + return 1; +} + /* Default: bright white on blue background */ #define ANSI_COLOR_CHROME "\x1B[0;44;1;37m" @@ -371,11 +412,12 @@ static int vl_on_reply(sd_varlink *link, sd_json_variant *parameters, const char int mute_console(sd_varlink **ret_link) { int r; - assert(ret_link); - /* Talks to the MuteConsole service, and asks for output to the console to be muted, as long as the * connection is retained */ + if (!ret_link) + return 0; + _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *link = NULL; r = sd_varlink_connect_address(&link, "/run/systemd/io.systemd.MuteConsole"); if (r < 0) diff --git a/src/shared/prompt-util.h b/src/shared/prompt-util.h index 7afccd1b3d..255433b671 100644 --- a/src/shared/prompt-util.h +++ b/src/shared/prompt-util.h @@ -26,6 +26,8 @@ int prompt_loop(const char *text, PromptFlags flags, char **ret); +int prompt_loop_yes_no(const char *question, bool def, bool *ret); + int chrome_show(const char *top, const char *bottom); void chrome_hide(void); diff --git a/src/shared/varlink-io.systemd.CryptEnroll.c b/src/shared/varlink-io.systemd.CryptEnroll.c new file mode 100644 index 0000000000..6855299e76 --- /dev/null +++ b/src/shared/varlink-io.systemd.CryptEnroll.c @@ -0,0 +1,108 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "bus-polkit.h" +#include "varlink-io.systemd.CryptEnroll.h" + +/* The credential types this interface knows about. The same enum is used for the 'mechanism' to enroll, for + * the slot types to wipe, and for the slot types reported by ListSlots. Note that only password, recovery key + * and fido2 may actually be *enrolled* via the Enroll() method; pkcs11 and tpm2 slots can be listed and wiped, + * but enrolling them requires the systemd-cryptenroll command line. Enroll() rejects them with an + * InvalidParameter error rather than via interface validation, so they are part of this enum. */ +static SD_VARLINK_DEFINE_ENUM_TYPE( + EnrollMechanism, + SD_VARLINK_FIELD_COMMENT("A regular passphrase"), + SD_VARLINK_DEFINE_ENUM_VALUE(password), + SD_VARLINK_FIELD_COMMENT("A randomly generated recovery key"), + SD_VARLINK_DEFINE_ENUM_VALUE(recovery), + SD_VARLINK_FIELD_COMMENT("A PKCS#11 security token (not enrollable via this interface)"), + SD_VARLINK_DEFINE_ENUM_VALUE(pkcs11), + SD_VARLINK_FIELD_COMMENT("A FIDO2 security token"), + SD_VARLINK_DEFINE_ENUM_VALUE(fido2), + SD_VARLINK_FIELD_COMMENT("A TPM2 device (not enrollable via this interface)"), + SD_VARLINK_DEFINE_ENUM_VALUE(tpm2)); + +static SD_VARLINK_DEFINE_METHOD_FULL( + Enroll, + SD_VARLINK_SUPPORTS_MORE, + SD_VARLINK_FIELD_COMMENT("Path to the LUKS2 block device or image to operate on."), + SD_VARLINK_DEFINE_INPUT(node, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("Which kind of credential to enroll. Only 'password', 'recovery' and 'fido2' may be enrolled via this interface for now; 'pkcs11' and 'tpm2' are rejected with an InvalidParameter error, currently."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(mechanism, EnrollMechanism, 0), + + SD_VARLINK_FIELD_COMMENT("How to unlock the volume for the enrollment operation is inferred from which of the following fields are set: setting unlockPassword unlocks via that password, unlockKeyFile/unlockKeyFileDescriptor via a key file, unlockFido2Device via FIDO2, unlockTpm2Device via TPM2. Exactly one must be set."), + SD_VARLINK_DEFINE_INPUT(unlockPassword, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Path to a key file to unlock the volume with."), + SD_VARLINK_DEFINE_INPUT(unlockKeyFile, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Index into the file descriptors passed along with this call, identifying a key file to unlock the volume with."), + SD_VARLINK_DEFINE_INPUT(unlockKeyFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Path to a FIDO2 device to unlock the volume with. Leave the path empty to automatically discover a suitable device."), + SD_VARLINK_DEFINE_INPUT(unlockFido2Device, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Path to a TPM2 device to unlock the volume with. Leave the path empty to automatically discover a suitable device."), + SD_VARLINK_DEFINE_INPUT(unlockTpm2Device, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + + SD_VARLINK_FIELD_COMMENT("The passphrase to enroll, when mechanism is 'password'. Contains key material."), + SD_VARLINK_DEFINE_INPUT(password, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + + SD_VARLINK_FIELD_COMMENT("Path to the FIDO2 device to enroll, when mechanism is 'fido2'. Leave empty to automatically discover a suitable device."), + SD_VARLINK_DEFINE_INPUT(fido2Device, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The FIDO2 token PIN to use for enrollment. Contains key material."), + SD_VARLINK_DEFINE_INPUT(fido2Pin, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Whether unlocking the FIDO2 enrollment shall require a client PIN. Defaults to true."), + SD_VARLINK_DEFINE_INPUT(fido2WithClientPin, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Whether unlocking the FIDO2 enrollment shall require user presence (touch). Defaults to true."), + SD_VARLINK_DEFINE_INPUT(fido2WithUserPresence, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Whether unlocking the FIDO2 enrollment shall require user verification. Defaults to false."), + SD_VARLINK_DEFINE_INPUT(fido2WithUserVerification, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + + SD_VARLINK_FIELD_COMMENT("Explicit list of keyslot indexes to wipe after enrolling."), + SD_VARLINK_DEFINE_INPUT(wipeSlots, SD_VARLINK_INT, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Wipe all already-enrolled slots of these types after enrolling."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(wipeTypes, EnrollMechanism, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE), + + VARLINK_DEFINE_POLKIT_INPUT, + + SD_VARLINK_FIELD_COMMENT("The keyslot the new credential was enrolled into. Set on the terminating reply only."), + SD_VARLINK_DEFINE_OUTPUT(slot, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The keyslots wiped as part of this call, if any."), + SD_VARLINK_DEFINE_OUTPUT(wipedSlots, SD_VARLINK_INT, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The generated recovery key, when mechanism is 'recoveryKey'. Contains key material."), + SD_VARLINK_DEFINE_OUTPUT(recoveryKey, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Progress indicator, only sent on intermediate replies when 'more' was set. Currently the only value is 'touch', signalling that the FIDO2 token is waiting for a touch. Unset on the terminating reply."), + SD_VARLINK_DEFINE_OUTPUT(state, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_METHOD_FULL( + ListSlots, + SD_VARLINK_REQUIRES_MORE, + SD_VARLINK_FIELD_COMMENT("Path to the LUKS2 block device or image to operate on."), + SD_VARLINK_DEFINE_INPUT(node, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("A currently enrolled keyslot index."), + SD_VARLINK_DEFINE_OUTPUT(slot, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The type of the keyslot."), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(type, EnrollMechanism, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_ERROR(VolumeUnderForeignManagement); +static SD_VARLINK_DEFINE_ERROR(PasswordRequired); +static SD_VARLINK_DEFINE_ERROR(PasswordIncorrect); +static SD_VARLINK_DEFINE_ERROR(FidoDeviceNotFound); +static SD_VARLINK_DEFINE_ERROR(FidoActionTimeout); + +SD_VARLINK_DEFINE_INTERFACE( + io_systemd_CryptEnroll, + "io.systemd.CryptEnroll", + SD_VARLINK_INTERFACE_COMMENT("API for enrolling credentials into LUKS2 volumes via systemd-cryptenroll."), + SD_VARLINK_SYMBOL_COMMENT("The credential types this interface knows about, used both as the enrollment mechanism and as the slot type reported by ListSlots."), + &vl_type_EnrollMechanism, + SD_VARLINK_SYMBOL_COMMENT("Enroll a credential into a LUKS2 volume. When enrolling a FIDO2 token that requires user presence, the call must be made with the 'more' flag, so that the server can report when a touch is required via a 'touch' state notification."), + &vl_method_Enroll, + SD_VARLINK_SYMBOL_COMMENT("Enumerate the keyslots currently enrolled in a LUKS2 volume, one per reply. Must be called with the 'more' flag."), + &vl_method_ListSlots, + SD_VARLINK_SYMBOL_COMMENT("The volume is managed by another subsystem (e.g. systemd-homed) and may not be enrolled into directly."), + &vl_error_VolumeUnderForeignManagement, + SD_VARLINK_SYMBOL_COMMENT("A password is required to unlock the volume, but none was provided."), + &vl_error_PasswordRequired, + SD_VARLINK_SYMBOL_COMMENT("The provided password did not unlock the volume."), + &vl_error_PasswordIncorrect, + SD_VARLINK_SYMBOL_COMMENT("No matching FIDO2 device was found."), + &vl_error_FidoDeviceNotFound, + SD_VARLINK_SYMBOL_COMMENT("The FIDO2 token was not interacted with in time."), + &vl_error_FidoActionTimeout); diff --git a/src/shared/varlink-io.systemd.CryptEnroll.h b/src/shared/varlink-io.systemd.CryptEnroll.h new file mode 100644 index 0000000000..dcb626a47b --- /dev/null +++ b/src/shared/varlink-io.systemd.CryptEnroll.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-varlink-idl.h" + +extern const sd_varlink_interface vl_interface_io_systemd_CryptEnroll; diff --git a/src/sysinstall/sysinstall.c b/src/sysinstall/sysinstall.c index 01e8cd0483..5d684cb0ab 100644 --- a/src/sysinstall/sysinstall.c +++ b/src/sysinstall/sysinstall.c @@ -613,29 +613,14 @@ static int prompt_confirm(void) { putchar('\n'); - char **l = STRV_MAKE("yes", "no"); - - _cleanup_free_ char *reply = NULL; - r = prompt_loop(arg_summary ? "Please type 'yes' to confirm the choices above and begin the installation" : - "Please type 'yes' to begin the installation", - GLYPH_WARNING_SIGN, - /* prefill= */ NULL, - /* menu= */ l, - /* accepted= */ l, - /* ellipsize_percentage= */ 20, - /* n_columns= */ 2, - /* column_width= */ 40, - /* is_valid= */ NULL, - /* refresh= */ NULL, - /* userdata= */ NULL, - PROMPT_SHOW_MENU|PROMPT_MAY_SKIP|PROMPT_HIDE_MENU_HINT|PROMPT_HIDE_SKIP_HINT, - &reply); + bool yes; + r = prompt_loop_yes_no(arg_summary ? "Please type 'yes' to confirm the choices above and begin the installation" : + "Please type 'yes' to begin the installation", + /* def= */ false, + &yes); if (r < 0) return r; - if (r == 0) - return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation cancelled."); - - if (!streq(reply, "yes")) + if (!yes) return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation not confirmed, cancelling."); return 0; diff --git a/test/units/TEST-70-TPM2.cryptenroll.sh b/test/units/TEST-70-TPM2.cryptenroll.sh index 07309429d7..684ff418f1 100755 --- a/test/units/TEST-70-TPM2.cryptenroll.sh +++ b/test/units/TEST-70-TPM2.cryptenroll.sh @@ -12,7 +12,7 @@ cryptenroll_wipe_and_check() {( )} at_exit() { - rm -f "${IMAGE:-}" /tmp/cryptenroll.out /tmp/password + rm -f "${IMAGE:-}" "${VL_IMAGE:-}" /tmp/cryptenroll.out /tmp/password } trap at_exit EXIT @@ -101,3 +101,43 @@ PIN=4321 systemd-cryptenroll --unlock-tpm2-device=auto --recovery-key "$IMAGE" (! systemd-cryptenroll --fido2-device=auto --unlock-fido2-device=auto "$IMAGE") rm -f "$IMAGE" + +# Exercise the io.systemd.CryptEnroll Varlink interface with calls equivalent to the command line ones above. +CRYPTENROLL="$(command -v systemd-cryptenroll)" +VL_ADDRESS="exec:$CRYPTENROLL" +VL_IMAGE="$(mktemp /tmp/systemd-cryptenroll-varlink-XXX.image)" +truncate -s 20M "$VL_IMAGE" +cryptsetup luksFormat -q --pbkdf pbkdf2 --pbkdf-force-iterations 1000 --use-urandom "$VL_IMAGE" /tmp/password + +# Enroll a recovery key, unlocking via key file (cf. systemd-cryptenroll --unlock-key-file= --recovery-key) +varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \ + "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"recovery\",\"unlockKeyFile\":\"/tmp/password\"}" | grep recoveryKey >/dev/null + +# Enroll a password, unlocking via key file (cf. NEWPASSWORD=… systemd-cryptenroll --unlock-key-file= --password) +varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \ + "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"password\",\"unlockKeyFile\":\"/tmp/password\",\"password\":\"varlinkpassword\"}" + +# Enroll a password, unlocking via the key file passed as a file descriptor instead of a path +varlinkctl --push-fd=3 call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \ + "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"password\",\"unlockKeyFileDescriptor\":0,\"password\":\"fdpassword\"}" 3/dev/null +varlinkctl call --more "$VL_ADDRESS" io.systemd.CryptEnroll.ListSlots "{\"node\":\"$VL_IMAGE\"}" | grep '"type":"password"' >/dev/null + +# Enroll combined with a wipe of the recovery key slot (cf. systemd-cryptenroll --wipe-slot=recovery --password). +# The recovery key slot just got wiped, so it should be reported back in the (non-empty) wipedSlots output. +varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \ + "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"password\",\"unlockKeyFile\":\"/tmp/password\",\"password\":\"wipepassword\",\"wipeTypes\":[\"recovery\"]}" | grep -E '"wipedSlots":\[[0-9]+(,[0-9]+)*\]' >/dev/null +(! varlinkctl call --more "$VL_ADDRESS" io.systemd.CryptEnroll.ListSlots "{\"node\":\"$VL_IMAGE\"}" | grep '"type":"recovery"' >/dev/null) + +# ListSlots without 'more' is refused +(! varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.ListSlots "{\"node\":\"$VL_IMAGE\"}") + +# PKCS#11 and TPM2 cannot be enrolled via this interface, so the Enroll() handler rejects them as invalid parameters +(! varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \ + "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"tpm2\",\"unlockKeyFile\":\"/tmp/password\"}") +(! varlinkctl call "$VL_ADDRESS" io.systemd.CryptEnroll.Enroll \ + "{\"node\":\"$VL_IMAGE\",\"mechanism\":\"pkcs11\",\"unlockKeyFile\":\"/tmp/password\"}") + +rm -f "$VL_IMAGE" diff --git a/units/meson.build b/units/meson.build index 569aa2e2a2..60ce3bf0ca 100644 --- a/units/meson.build +++ b/units/meson.build @@ -338,6 +338,20 @@ units = [ 'symlinks' : ['sockets.target.wants/'], }, { 'file' : 'systemd-creds@.service' }, + { + 'file' : 'systemd-cryptenroll.socket', + 'conditions' : ['HAVE_LIBCRYPTSETUP'], + 'symlinks' : ['sockets.target.wants/'], + }, + { + 'file' : 'systemd-cryptenroll@.service', + 'conditions' : ['HAVE_LIBCRYPTSETUP'], + }, + { + 'file' : 'systemd-cryptenroll-firstboot.service', + 'conditions' : ['HAVE_LIBCRYPTSETUP', 'ENABLE_INITRD'], + 'symlinks' : ['initrd.target.wants/'], + }, { 'file' : 'systemd-exit.service' }, { 'file' : 'systemd-factory-reset@.service.in' }, { diff --git a/units/systemd-cryptenroll-firstboot.service b/units/systemd-cryptenroll-firstboot.service new file mode 100644 index 0000000000..ccd69ef60f --- /dev/null +++ b/units/systemd-cryptenroll-firstboot.service @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +# Runs from the initrd: once systemd-repart has created/grown the partitions +# (including any encrypted ones) on first boot, but before we transition into +# the host, give the user a chance to interactively enroll a disk encryption +# credential. + +[Unit] +Description=Interactive Disk Encryption Setup +Documentation=man:systemd-cryptenroll(1) + +ConditionPathExists=/etc/initrd-release + +# We want a first boot check, but in the initrd that's not a defined +# state. Hence let's look into the mounted image directly for the primary +# indicator for the first boot state +ConditionFileNotEmpty=!/sysroot/etc/machine-id +ConditionPathIsEncrypted=/sysroot/var + +DefaultDependencies=no +After=systemd-repart.service systemd-vconsole-setup.service systemd-mute-console.socket initrd-fs.target +Conflicts=shutdown.target initrd-switch-root.target +Before=shutdown.target initrd-switch-root.target initrd.target + +[Service] +Type=oneshot +RemainAfterExit=yes +# Ignore failures: there might not be an encrypted volume to enroll into, or the +# user might decline, neither of which should hold up the boot. +ExecStart=-systemd-cryptenroll --firstboot --prompt-suppress=password,recovery,fido2 --unlock-headless --mute-console=yes +StandardOutput=tty +StandardInput=tty +StandardError=tty +TTYReset=yes diff --git a/units/systemd-cryptenroll.socket b/units/systemd-cryptenroll.socket new file mode 100644 index 0000000000..0ffcdb4c8a --- /dev/null +++ b/units/systemd-cryptenroll.socket @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Disk Encryption Enrollment Service Socket +Documentation=man:systemd-cryptenroll(1) +DefaultDependencies=no +Before=sockets.target + +[Socket] +ListenStream=/run/systemd/io.systemd.CryptEnroll +Symlinks=/run/varlink/registry/io.systemd.CryptEnroll +FileDescriptorName=varlink +SocketMode=0644 +Accept=yes +MaxConnectionsPerSource=16 +XAttrEntryPoint=user.varlink=entrypoint +XAttrListen=user.varlink=listen +XAttrAccept=user.varlink=server diff --git a/units/systemd-cryptenroll@.service b/units/systemd-cryptenroll@.service new file mode 100644 index 0000000000..31f42cc458 --- /dev/null +++ b/units/systemd-cryptenroll@.service @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Disk Encryption Enrollment Service +Documentation=man:systemd-cryptenroll(1) +DefaultDependencies=no +Conflicts=shutdown.target initrd-switch-root.target +Before=shutdown.target initrd-switch-root.target + +[Service] +ExecStart=-systemd-cryptenroll