]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
cryptenroll: add new "systemd-cryptenroll" tool for enrolling FIDO2+PKCS#11 security...
authorLennart Poettering <lennart@poettering.net>
Tue, 24 Nov 2020 12:41:47 +0000 (13:41 +0100)
committerLennart Poettering <lennart@poettering.net>
Thu, 17 Dec 2020 19:00:51 +0000 (20:00 +0100)
meson.build
src/cryptenroll/cryptenroll-fido2.c [new file with mode: 0644]
src/cryptenroll/cryptenroll-fido2.h [new file with mode: 0644]
src/cryptenroll/cryptenroll-password.c [new file with mode: 0644]
src/cryptenroll/cryptenroll-password.h [new file with mode: 0644]
src/cryptenroll/cryptenroll-pkcs11.c [new file with mode: 0644]
src/cryptenroll/cryptenroll-pkcs11.h [new file with mode: 0644]
src/cryptenroll/cryptenroll-recovery.c [new file with mode: 0644]
src/cryptenroll/cryptenroll-recovery.h [new file with mode: 0644]
src/cryptenroll/cryptenroll.c [new file with mode: 0644]

index c64edda8e31ba69059c9687d097228e21d73c457..f434d685422d2e88fa3958c621ca09cdf3c2a9c2 100644 (file)
@@ -2420,6 +2420,36 @@ if conf.get('HAVE_LIBCRYPTSETUP') == 1
                 install_rpath : rootlibexecdir,
                 install : true,
                 install_dir : systemgeneratordir)
+
+        systemd_cryptenroll_sources = files('''
+                src/cryptenroll/cryptenroll-fido2.h
+                src/cryptenroll/cryptenroll-password.c
+                src/cryptenroll/cryptenroll-password.h
+                src/cryptenroll/cryptenroll-pkcs11.h
+                src/cryptenroll/cryptenroll-recovery.c
+                src/cryptenroll/cryptenroll-recovery.h
+                src/cryptenroll/cryptenroll.c
+'''.split())
+
+        if conf.get('HAVE_P11KIT') == 1 and conf.get('HAVE_OPENSSL') == 1
+                systemd_cryptenroll_sources += files('src/cryptenroll/cryptenroll-pkcs11.c')
+        endif
+
+        if conf.get('HAVE_LIBFIDO2') == 1
+                systemd_cryptenroll_sources += files('src/cryptenroll/cryptenroll-fido2.c')
+        endif
+
+        executable(
+                'systemd-cryptenroll',
+                systemd_cryptenroll_sources,
+                include_directories : includes,
+                link_with : [libshared],
+                dependencies : [libcryptsetup,
+                                libopenssl,
+                                libp11kit],
+                install_rpath : rootlibexecdir,
+                install : true,
+                install_dir : bindir)
 endif
 
 if conf.get('HAVE_SYSV_COMPAT') == 1
diff --git a/src/cryptenroll/cryptenroll-fido2.c b/src/cryptenroll/cryptenroll-fido2.c
new file mode 100644 (file)
index 0000000..1b3ae8d
--- /dev/null
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "cryptenroll-fido2.h"
+#include "hexdecoct.h"
+#include "json.h"
+#include "libfido2-util.h"
+#include "memory-util.h"
+#include "random-util.h"
+
+int enroll_fido2(
+                struct crypt_device *cd,
+                const void *volume_key,
+                size_t volume_key_size,
+                const char *device) {
+
+        _cleanup_(erase_and_freep) void *salt = NULL, *secret = NULL;
+        _cleanup_(erase_and_freep) char *base64_encoded = NULL;
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        _cleanup_free_ char *keyslot_as_string = NULL;
+        size_t cid_size, salt_size, secret_size;
+        _cleanup_free_ void *cid = NULL;
+        const char *node, *un;
+        int r, keyslot;
+
+        assert_se(cd);
+        assert_se(volume_key);
+        assert_se(volume_key_size > 0);
+        assert_se(device);
+
+        assert_se(node = crypt_get_device_name(cd));
+
+        un = strempty(crypt_get_uuid(cd));
+
+        r = fido2_generate_hmac_hash(
+                        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 */
+                        /* user_name= */ un,
+                        /* user_display_name= */ node,
+                        /* user_icon_name= */ NULL,
+                        /* askpw_icon_name= */ "drive-harddisk",
+                        &cid, &cid_size,
+                        &salt, &salt_size,
+                        &secret, &secret_size,
+                        NULL);
+        if (r < 0)
+                return r;
+
+        /* Before we use the secret, we base64 encode it, for compat with homed, and to make it easier to type in manually */
+        r = base64mem(secret, secret_size, &base64_encoded);
+        if (r < 0)
+                return log_error_errno(r, "Failed to base64 encode secret key: %m");
+
+        r = cryptsetup_set_minimal_pbkdf(cd);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set minimal PBKDF: %m");
+
+        keyslot = crypt_keyslot_add_by_volume_key(
+                        cd,
+                        CRYPT_ANY_SLOT,
+                        volume_key,
+                        volume_key_size,
+                        base64_encoded,
+                        strlen(base64_encoded));
+        if (keyslot < 0)
+                return log_error_errno(keyslot, "Failed to add new PKCS#11 key to %s: %m", node);
+
+        if (asprintf(&keyslot_as_string, "%i", keyslot) < 0)
+                return log_oom();
+
+        r = json_build(&v,
+                       JSON_BUILD_OBJECT(
+                                       JSON_BUILD_PAIR("type", JSON_BUILD_STRING("systemd-fido2")),
+                                       JSON_BUILD_PAIR("keyslots", JSON_BUILD_ARRAY(JSON_BUILD_STRING(keyslot_as_string))),
+                                       JSON_BUILD_PAIR("fido2-credential", JSON_BUILD_BASE64(cid, cid_size)),
+                                       JSON_BUILD_PAIR("fido2-salt", JSON_BUILD_BASE64(salt, salt_size)),
+                                       JSON_BUILD_PAIR("fido2-rp", JSON_BUILD_STRING("io.systemd.cryptsetup"))));
+        if (r < 0)
+                return log_error_errno(r, "Failed to prepare PKCS#11 JSON token object: %m");
+
+        r = cryptsetup_add_token_json(cd, v);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add FIDO2 JSON token to LUKS2 header: %m");
+
+        log_info("New FIDO2 token enrolled as key slot %i.", keyslot);
+        return keyslot;
+}
diff --git a/src/cryptenroll/cryptenroll-fido2.h b/src/cryptenroll/cryptenroll-fido2.h
new file mode 100644 (file)
index 0000000..9367920
--- /dev/null
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sys/types.h>
+
+#include "cryptsetup-util.h"
+#include "log.h"
+
+#if HAVE_LIBFIDO2
+int enroll_fido2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device);
+#else
+static inline int enroll_fido2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device) {
+        return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+                               "FIDO2 key enrollment not supported.");
+}
+#endif
diff --git a/src/cryptenroll/cryptenroll-password.c b/src/cryptenroll/cryptenroll-password.c
new file mode 100644 (file)
index 0000000..e08f564
--- /dev/null
@@ -0,0 +1,105 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "ask-password-api.h"
+#include "cryptenroll-password.h"
+#include "escape.h"
+#include "memory-util.h"
+#include "pwquality-util.h"
+#include "strv.h"
+
+int enroll_password(
+                struct crypt_device *cd,
+                const void *volume_key,
+                size_t volume_key_size) {
+
+        _cleanup_(erase_and_freep) char *new_password = NULL;
+        _cleanup_free_ char *error = NULL;
+        const char *node;
+        int r, keyslot;
+        char *e;
+
+        assert_se(node = crypt_get_device_name(cd));
+
+        e = getenv("NEWPASSWORD");
+        if (e) {
+
+                new_password = strdup(e);
+                if (!new_password)
+                        return log_oom();
+
+                string_erase(e);
+                assert_se(unsetenv("NEWPASSWORD") == 0);
+
+        } else {
+                _cleanup_free_ char *disk_path = NULL;
+                unsigned i = 5;
+                const char *id;
+
+                assert_se(node = crypt_get_device_name(cd));
+
+                (void) suggest_passwords();
+
+                disk_path = cescape(node);
+                if (!disk_path)
+                        return log_oom();
+
+                id = strjoina("cryptsetup:", disk_path);
+
+                for (;;) {
+                        _cleanup_strv_free_erase_ char **passwords = NULL, **passwords2 = NULL;
+                        _cleanup_free_ char *question = NULL;
+
+                        if (--i == 0)
+                                return log_error_errno(SYNTHETIC_ERRNO(ENOKEY),
+                                                       "Too many attempts, giving up:");
+
+                        question = strjoin("Please enter new passphrase for disk ", node, ":");
+                        if (!question)
+                                return log_oom();
+
+                        r = ask_password_auto(question, "drive-harddisk", id, "cryptenroll", USEC_INFINITY, 0, &passwords);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to query password: %m");
+
+                        assert(strv_length(passwords) == 1);
+
+                        free(question);
+                        question = strjoin("Please enter new passphrase for disk ", node, " (repeat):");
+                        if (!question)
+                                return log_oom();
+
+                        r = ask_password_auto(question, "drive-harddisk", id, "cryptenroll", USEC_INFINITY, 0, &passwords2);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to query password: %m");
+
+                        assert(strv_length(passwords2) == 1);
+
+                        if (strv_equal(passwords, passwords2)) {
+                                new_password = passwords2[0];
+                                passwords2 = mfree(passwords2);
+                                break;
+                        }
+
+                        log_error("Password didn't match, try again.");
+                }
+        }
+
+        r = quality_check_password(new_password, NULL, &error);
+        if (r < 0)
+                return log_error_errno(r, "Failed to check password for quality: %m");
+        if (r == 0)
+                log_warning_errno(r, "Specified password does not pass quality checks (%s), proceeding anyway.", error);
+
+        keyslot = crypt_keyslot_add_by_volume_key(
+                        cd,
+                        CRYPT_ANY_SLOT,
+                        volume_key,
+                        volume_key_size,
+                        new_password,
+                        strlen(new_password));
+        if (keyslot < 0)
+                return log_error_errno(keyslot, "Failed to add new password to %s: %m", node);
+
+        log_info("New password enrolled as key slot %i.", keyslot);
+        return keyslot;
+}
diff --git a/src/cryptenroll/cryptenroll-password.h b/src/cryptenroll/cryptenroll-password.h
new file mode 100644 (file)
index 0000000..ddeee13
--- /dev/null
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sys/types.h>
+
+#include "cryptsetup-util.h"
+
+int enroll_password(struct crypt_device *cd, const void *volume_key, size_t volume_key_size);
diff --git a/src/cryptenroll/cryptenroll-pkcs11.c b/src/cryptenroll/cryptenroll-pkcs11.c
new file mode 100644 (file)
index 0000000..15ae6c9
--- /dev/null
@@ -0,0 +1,99 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "cryptenroll-pkcs11.h"
+#include "hexdecoct.h"
+#include "json.h"
+#include "memory-util.h"
+#include "openssl-util.h"
+#include "pkcs11-util.h"
+#include "random-util.h"
+
+int enroll_pkcs11(
+                struct crypt_device *cd,
+                const void *volume_key,
+                size_t volume_key_size,
+                const char *uri) {
+
+        _cleanup_(erase_and_freep) void *decrypted_key = NULL;
+        _cleanup_(erase_and_freep) char *base64_encoded = NULL;
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        _cleanup_free_ char *keyslot_as_string = NULL;
+        size_t decrypted_key_size, encrypted_key_size;
+        _cleanup_free_ void *encrypted_key = NULL;
+        _cleanup_(X509_freep) X509 *cert = NULL;
+        const char *node;
+        EVP_PKEY *pkey;
+        int keyslot, r;
+
+        assert_se(cd);
+        assert_se(volume_key);
+        assert_se(volume_key_size > 0);
+        assert_se(uri);
+
+        assert_se(node = crypt_get_device_name(cd));
+
+        r = pkcs11_acquire_certificate(uri, "volume enrollment operation", "drive-harddisk", &cert, NULL);
+        if (r < 0)
+                return r;
+
+        pkey = X509_get0_pubkey(cert);
+        if (!pkey)
+                return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to extract public key from X.509 certificate.");
+
+        r = rsa_pkey_to_suitable_key_size(pkey, &decrypted_key_size);
+        if (r < 0)
+                return log_error_errno(r, "Failed to determine RSA public key size.");
+
+        log_debug("Generating %zu bytes random key.", decrypted_key_size);
+
+        decrypted_key = malloc(decrypted_key_size);
+        if (!decrypted_key)
+                return log_oom();
+
+        r = genuine_random_bytes(decrypted_key, decrypted_key_size, RANDOM_BLOCK);
+        if (r < 0)
+                return log_error_errno(r, "Failed to generate random key: %m");
+
+        r = rsa_encrypt_bytes(pkey, decrypted_key, decrypted_key_size, &encrypted_key, &encrypted_key_size);
+        if (r < 0)
+                return log_error_errno(r, "Failed to encrypt key: %m");
+
+        /* Let's base64 encode the key to use, for compat with homed (and it's easier to type it in by
+         * keyboard, if that might ever end up being necessary.) */
+        r = base64mem(decrypted_key, decrypted_key_size, &base64_encoded);
+        if (r < 0)
+                return log_error_errno(r, "Failed to base64 encode secret key: %m");
+
+        r = cryptsetup_set_minimal_pbkdf(cd);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set minimal PBKDF: %m");
+
+        keyslot = crypt_keyslot_add_by_volume_key(
+                        cd,
+                        CRYPT_ANY_SLOT,
+                        volume_key,
+                        volume_key_size,
+                        base64_encoded,
+                        strlen(base64_encoded));
+        if (keyslot < 0)
+                return log_error_errno(keyslot, "Failed to add new PKCS#11 key to %s: %m", node);
+
+        if (asprintf(&keyslot_as_string, "%i", keyslot) < 0)
+                return log_oom();
+
+        r = json_build(&v,
+                       JSON_BUILD_OBJECT(
+                                       JSON_BUILD_PAIR("type", JSON_BUILD_STRING("systemd-pkcs11")),
+                                       JSON_BUILD_PAIR("keyslots", JSON_BUILD_ARRAY(JSON_BUILD_STRING(keyslot_as_string))),
+                                       JSON_BUILD_PAIR("pkcs11-uri", JSON_BUILD_STRING(uri)),
+                                       JSON_BUILD_PAIR("pkcs11-key", JSON_BUILD_BASE64(encrypted_key, encrypted_key_size))));
+        if (r < 0)
+                return log_error_errno(r, "Failed to prepare PKCS#11 JSON token object: %m");
+
+        r = cryptsetup_add_token_json(cd, v);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add PKCS#11 JSON token to LUKS2 header: %m");
+
+        log_info("New PKCS#11 token enrolled as key slot %i.", keyslot);
+        return keyslot;
+}
diff --git a/src/cryptenroll/cryptenroll-pkcs11.h b/src/cryptenroll/cryptenroll-pkcs11.h
new file mode 100644 (file)
index 0000000..b6d28bd
--- /dev/null
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sys/types.h>
+
+#include "cryptsetup-util.h"
+#include "log.h"
+
+#if HAVE_P11KIT && HAVE_OPENSSL
+int enroll_pkcs11(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *uri);
+#else
+static inline int enroll_pkcs11(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *uri) {
+        return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+                               "PKCS#11 key enrollment not supported.");
+}
+#endif
diff --git a/src/cryptenroll/cryptenroll-recovery.c b/src/cryptenroll/cryptenroll-recovery.c
new file mode 100644 (file)
index 0000000..3204c46
--- /dev/null
@@ -0,0 +1,101 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "cryptenroll-recovery.h"
+#include "json.h"
+#include "locale-util.h"
+#include "memory-util.h"
+#include "qrcode-util.h"
+#include "recovery-key.h"
+#include "terminal-util.h"
+
+int enroll_recovery(
+                struct crypt_device *cd,
+                const void *volume_key,
+                size_t volume_key_size) {
+
+        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+        _cleanup_(erase_and_freep) char *password = NULL;
+        _cleanup_free_ char *keyslot_as_string = NULL;
+        int keyslot, r, q;
+        const char *node;
+
+        assert_se(cd);
+        assert_se(volume_key);
+        assert_se(volume_key_size > 0);
+
+        assert_se(node = crypt_get_device_name(cd));
+
+        r = make_recovery_key(&password);
+        if (r < 0)
+                return log_error_errno(r, "Failed to generate recovery key: %m");
+
+        r = cryptsetup_set_minimal_pbkdf(cd);
+        if (r < 0)
+                return log_error_errno(r, "Failed to set minimal PBKDF: %m");
+
+        keyslot = crypt_keyslot_add_by_volume_key(
+                        cd,
+                        CRYPT_ANY_SLOT,
+                        volume_key,
+                        volume_key_size,
+                        password,
+                        strlen(password));
+        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() ? special_glyph(SPECIAL_GLYPH_LOCK_AND_KEY) : "",
+                emoji_enabled() ? " " : "",
+                ansi_highlight());
+        fflush(stderr);
+
+        fputs(password, stdout);
+        fflush(stdout);
+
+        fputs(ansi_normal(), stderr);
+        fflush(stderr);
+
+        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);
+
+        (void) print_qrcode(stderr, "You may optionally scan the recovery key off screen", password);
+
+        if (asprintf(&keyslot_as_string, "%i", keyslot) < 0) {
+                r = log_oom();
+                goto rollback;
+        }
+
+        r = json_build(&v,
+                       JSON_BUILD_OBJECT(
+                                       JSON_BUILD_PAIR("type", JSON_BUILD_STRING("systemd-recovery")),
+                                       JSON_BUILD_PAIR("keyslots", JSON_BUILD_ARRAY(JSON_BUILD_STRING(keyslot_as_string)))));
+        if (r < 0) {
+                log_error_errno(r, "Failed to prepare recovery key JSON token object: %m");
+                goto rollback;
+        }
+
+        r = cryptsetup_add_token_json(cd, v);
+        if (r < 0) {
+                log_error_errno(r, "Failed to add recovery JSON token to LUKS2 header: %m");
+                goto rollback;
+        }
+
+        log_info("New recovery key enrolled as key slot %i.", keyslot);
+        return keyslot;
+
+rollback:
+        q = crypt_keyslot_destroy(cd, keyslot);
+        if (q < 0)
+                log_debug_errno(q, "Unable to remove key slot we just added again, can't rollback, sorry: %m");
+
+        return r;
+}
diff --git a/src/cryptenroll/cryptenroll-recovery.h b/src/cryptenroll/cryptenroll-recovery.h
new file mode 100644 (file)
index 0000000..9bf4f2e
--- /dev/null
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sys/types.h>
+
+#include "cryptsetup-util.h"
+
+int enroll_recovery(struct crypt_device *cd, const void *volume_key, size_t volume_key_size);
diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c
new file mode 100644 (file)
index 0000000..e08f810
--- /dev/null
@@ -0,0 +1,358 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+
+#include "ask-password-api.h"
+#include "cryptenroll-fido2.h"
+#include "cryptenroll-password.h"
+#include "cryptenroll-pkcs11.h"
+#include "cryptenroll-recovery.h"
+#include "cryptsetup-util.h"
+#include "escape.h"
+#include "libfido2-util.h"
+#include "main-func.h"
+#include "memory-util.h"
+#include "path-util.h"
+#include "pkcs11-util.h"
+#include "pretty-print.h"
+#include "strv.h"
+#include "terminal-util.h"
+
+typedef enum EnrollType {
+        ENROLL_PASSWORD,
+        ENROLL_RECOVERY,
+        ENROLL_PKCS11,
+        ENROLL_FIDO2,
+        _ENROLL_TYPE_MAX,
+        _ENROLL_TYPE_INVALID = -1,
+} EnrollType;
+
+static EnrollType arg_enroll_type = _ENROLL_TYPE_INVALID;
+static char *arg_pkcs11_token_uri = NULL;
+static char *arg_fido2_device = NULL;
+static char *arg_tpm2_device = NULL;
+static char *arg_node = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_node, freep);
+
+static int help(void) {
+        _cleanup_free_ char *link = NULL;
+        int r;
+
+        r = terminal_urlify_man("systemd-cryptenroll", "1", &link);
+        if (r < 0)
+                return log_oom();
+
+        printf("%s [OPTIONS...] BLOCK-DEVICE\n"
+               "\n%sEnroll a security token or authentication credential to a LUKS volume.%s\n\n"
+               "  -h --help            Show this help\n"
+               "     --version         Show package version\n"
+               "     --password        Enroll a user-supplied password\n"
+               "     --recovery-key    Enroll a recovery key\n"
+               "     --pkcs11-token-uri=URI\n"
+               "                       Specify PKCS#11 security token URI\n"
+               "     --fido2-device=PATH\n"
+               "                       Enroll a FIDO2-HMAC security token\n"
+               "\nSee the %s for details.\n"
+               , program_invocation_short_name
+               , ansi_highlight(), ansi_normal()
+               , link
+        );
+
+        return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+        enum {
+                ARG_VERSION = 0x100,
+                ARG_PASSWORD,
+                ARG_RECOVERY_KEY,
+                ARG_PKCS11_TOKEN_URI,
+                ARG_FIDO2_DEVICE,
+        };
+
+        static const struct option options[] = {
+                { "help",             no_argument,       NULL, 'h'                  },
+                { "version",          no_argument,       NULL, ARG_VERSION          },
+                { "password",         no_argument,       NULL, ARG_PASSWORD         },
+                { "recovery-key",     no_argument,       NULL, ARG_RECOVERY_KEY     },
+                { "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI },
+                { "fido2-device",     required_argument, NULL, ARG_FIDO2_DEVICE     },
+                {}
+        };
+
+        int c, r;
+
+        assert(argc >= 0);
+        assert(argv);
+
+        while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) {
+
+                switch (c) {
+
+                case 'h':
+                        return help();
+
+                case ARG_VERSION:
+                        return version();
+
+                case ARG_PASSWORD:
+                        if (arg_enroll_type >= 0)
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Multiple operations specified at once, refusing.");
+
+                        arg_enroll_type = ENROLL_PASSWORD;
+                        break;
+
+                case ARG_RECOVERY_KEY:
+                        if (arg_enroll_type >= 0)
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Multiple operations specified at once, refusing.");
+
+                        arg_enroll_type = ENROLL_RECOVERY;
+                        break;
+
+                case ARG_PKCS11_TOKEN_URI: {
+                        _cleanup_free_ char *uri = NULL;
+
+                        if (streq(optarg, "list"))
+                                return pkcs11_list_tokens();
+
+                        if (arg_enroll_type >= 0 || arg_pkcs11_token_uri)
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Multiple operations specified at once, refusing.");
+
+                        if (streq(optarg, "auto")) {
+                                r = pkcs11_find_token_auto(&uri);
+                                if (r < 0)
+                                        return r;
+                        } else {
+                                if (!pkcs11_uri_valid(optarg))
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid PKCS#11 URI: %s", optarg);
+
+                                uri = strdup(optarg);
+                                if (!uri)
+                                        return log_oom();
+                        }
+
+                        arg_enroll_type = ENROLL_PKCS11;
+                        arg_pkcs11_token_uri = TAKE_PTR(uri);
+                        break;
+                }
+
+                case ARG_FIDO2_DEVICE: {
+                        _cleanup_free_ char *device = NULL;
+
+                        if (streq(optarg, "list"))
+                                return fido2_list_devices();
+
+                        if (arg_enroll_type >= 0 || arg_fido2_device)
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Multiple operations specified at once, refusing.");
+
+                        if (streq(optarg, "auto")) {
+                                r = fido2_find_device_auto(&device);
+                                if (r < 0)
+                                        return r;
+                        } else {
+                                device = strdup(optarg);
+                                if (!device)
+                                        return log_oom();
+                        }
+
+                        arg_enroll_type = ENROLL_FIDO2;
+                        arg_fido2_device = TAKE_PTR(device);
+                        break;
+                }
+
+                case '?':
+                        return -EINVAL;
+
+                default:
+                        assert_not_reached("Unhandled option");
+                }
+        }
+
+        if (arg_enroll_type < 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "No operation specified, refusing.");
+
+        if (optind >= argc)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "No block device node specified, refusing.");
+
+        if (argc > optind+1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "Too many arguments, refusing.");
+
+        r = parse_path_argument_and_warn(argv[optind], false, &arg_node);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+static int prepare_luks(
+                struct crypt_device **ret_cd,
+                void **ret_volume_key,
+                size_t *ret_volume_key_size) {
+
+        _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+        _cleanup_(erase_and_freep) void *vk = NULL;
+        char *e = NULL;
+        size_t vks;
+        int r;
+
+        assert(ret_cd);
+        assert(!ret_volume_key == !ret_volume_key_size);
+
+        r = crypt_init(&cd, arg_node);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate libcryptsetup context: %m");
+
+        cryptsetup_enable_logging(cd);
+
+        r = crypt_load(cd, CRYPT_LUKS2, NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to load LUKS2 superblock: %m");
+
+        if (!ret_volume_key) {
+                *ret_cd = TAKE_PTR(cd);
+                return 0;
+        }
+
+        r = crypt_get_volume_key_size(cd);
+        if (r <= 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size");
+        vks = (size_t) r;
+
+        vk = malloc(vks);
+        if (!vk)
+                return log_oom();
+
+        e = getenv("PASSWORD");
+        if (e) {
+                _cleanup_(erase_and_freep) char *password = NULL;
+
+                password = strdup(e);
+                if (!password)
+                        return log_oom();
+
+                string_erase(e);
+                assert_se(unsetenv("PASSWORD") >= 0);
+
+                r = crypt_volume_key_get(
+                                cd,
+                                CRYPT_ANY_SLOT,
+                                vk,
+                                &vks,
+                                password,
+                                strlen(password));
+                if (r < 0)
+                        return log_error_errno(r, "Password from environent variable $PASSWORD did not work.");
+        } else {
+                AskPasswordFlags ask_password_flags = ASK_PASSWORD_PUSH_CACHE|ASK_PASSWORD_ACCEPT_CACHED;
+                _cleanup_free_ char *question = NULL, *disk_path = NULL;
+                unsigned i = 5;
+                const char *id;
+
+                question = strjoin("Please enter current passphrase for disk ", arg_node, ":");
+                if (!question)
+                        return log_oom();
+
+                disk_path = cescape(arg_node);
+                if (!disk_path)
+                        return log_oom();
+
+                id = strjoina("cryptsetup:", disk_path);
+
+                for (;;) {
+                        _cleanup_strv_free_erase_ char **passwords = NULL;
+                        char **p;
+
+                        if (--i == 0)
+                                return log_error_errno(SYNTHETIC_ERRNO(ENOKEY),
+                                                       "Too many attempts, giving up:");
+
+                        r = ask_password_auto(
+                                        question, "drive-harddisk", id, "cryptenroll", USEC_INFINITY,
+                                        ask_password_flags,
+                                        &passwords);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to query password: %m");
+
+                        r = -EPERM;
+                        STRV_FOREACH(p, passwords) {
+                                r = crypt_volume_key_get(
+                                                cd,
+                                                CRYPT_ANY_SLOT,
+                                                vk,
+                                                &vks,
+                                                *p,
+                                                strlen(*p));
+                                if (r >= 0)
+                                        break;
+                        }
+                        if (r >= 0)
+                                break;
+
+                        log_error_errno(r, "Password not correct, please try again.");
+                        ask_password_flags &= ~ASK_PASSWORD_ACCEPT_CACHED;
+                }
+        }
+
+        *ret_cd = TAKE_PTR(cd);
+        *ret_volume_key = TAKE_PTR(vk);
+        *ret_volume_key_size = vks;
+
+        return 0;
+}
+
+static int run(int argc, char *argv[]) {
+        _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+        _cleanup_(erase_and_freep) void *vk = NULL;
+        size_t vks;
+        int r;
+
+        log_show_color(true);
+        log_parse_environment();
+        log_open();
+
+        r = parse_argv(argc, argv);
+        if (r <= 0)
+                return r;
+
+        r = prepare_luks(&cd, &vk, &vks);
+        if (r < 0)
+                return r;
+
+        switch (arg_enroll_type) {
+
+        case ENROLL_PASSWORD:
+                r = enroll_password(cd, vk, vks);
+                break;
+
+        case ENROLL_RECOVERY:
+                r = enroll_recovery(cd, vk, vks);
+                break;
+
+        case ENROLL_PKCS11:
+                r = enroll_pkcs11(cd, vk, vks, arg_pkcs11_token_uri);
+                break;
+
+        case ENROLL_FIDO2:
+                r = enroll_fido2(cd, vk, vks, arg_fido2_device);
+                break;
+
+        default:
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Operation not implemented yet.");
+        }
+
+        return r;
+}
+
+DEFINE_MAIN_FUNCTION(run);