diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
commit | 55944e5e40b1be2afc4855d8d2baf4b73d1876b5 (patch) | |
tree | 33f869f55a1b149e9b7c2b7e201867ca5dd52992 /src/cryptenroll | |
parent | Initial commit. (diff) | |
download | systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.tar.xz systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.zip |
Adding upstream version 255.4.upstream/255.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/cryptenroll')
-rw-r--r-- | src/cryptenroll/cryptenroll-fido2.c | 151 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll-fido2.h | 24 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll-list.c | 127 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll-list.h | 6 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll-password.c | 181 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll-password.h | 9 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll-pkcs11.c | 100 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll-pkcs11.h | 16 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll-recovery.c | 101 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll-recovery.h | 8 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll-tpm2.c | 383 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll-tpm2.h | 17 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll-wipe.c | 445 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll-wipe.h | 12 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll.c | 762 | ||||
-rw-r--r-- | src/cryptenroll/cryptenroll.h | 36 | ||||
-rw-r--r-- | src/cryptenroll/meson.build | 36 |
17 files changed, 2414 insertions, 0 deletions
diff --git a/src/cryptenroll/cryptenroll-fido2.c b/src/cryptenroll/cryptenroll-fido2.c new file mode 100644 index 0000000..2baeb92 --- /dev/null +++ b/src/cryptenroll/cryptenroll-fido2.c @@ -0,0 +1,151 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "ask-password-api.h" +#include "cryptenroll-fido2.h" +#include "cryptsetup-fido2.h" +#include "hexdecoct.h" +#include "json.h" +#include "libfido2-util.h" +#include "memory-util.h" +#include "random-util.h" + +int load_volume_key_fido2( + struct crypt_device *cd, + const char *cd_node, + const char *device, + void *ret_vk, + size_t *ret_vks) { + + _cleanup_(erase_and_freep) void *decrypted_key = NULL; + _cleanup_(erase_and_freep) char *passphrase = NULL; + size_t decrypted_key_size; + ssize_t passphrase_size; + int r; + + 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, + /* until= */ 0, + /* headless= */ false, + &decrypted_key, + &decrypted_key_size, + ASK_PASSWORD_PUSH_CACHE|ASK_PASSWORD_ACCEPT_CACHED); + if (r == -EAGAIN) + return log_error_errno(r, "FIDO2 token does not exist, or UV is blocked. Please try again."); + if (r < 0) + return r; + + /* Because cryptenroll requires a LUKS header, we can assume that this device is not + * a PLAIN device. In this case, we need to base64 encode the secret to use as the passphrase */ + passphrase_size = base64mem(decrypted_key, decrypted_key_size, &passphrase); + if (passphrase_size < 0) + return log_oom(); + + r = crypt_volume_key_get( + cd, + CRYPT_ANY_SLOT, + ret_vk, + ret_vks, + passphrase, + passphrase_size); + if (r < 0) + return log_error_errno(r, "Unlocking via FIDO2 device failed: %m"); + + return r; +} + +int enroll_fido2( + struct crypt_device *cd, + const void *volume_key, + size_t volume_key_size, + const char *device, + Fido2EnrollFlags lock_with, + int cred_alg) { + + _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; + ssize_t base64_encoded_size; + 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", + lock_with, + cred_alg, + &cid, &cid_size, + &salt, &salt_size, + &secret, &secret_size, + NULL, + &lock_with); + 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 */ + base64_encoded_size = base64mem(secret, secret_size, &base64_encoded); + if (base64_encoded_size < 0) + return log_error_errno(base64_encoded_size, "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, + base64_encoded_size); + if (keyslot < 0) + return log_error_errno(keyslot, "Failed to add new FIDO2 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_CONST_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_CONST_STRING("io.systemd.cryptsetup")), + JSON_BUILD_PAIR("fido2-clientPin-required", JSON_BUILD_BOOLEAN(FLAGS_SET(lock_with, FIDO2ENROLL_PIN))), + JSON_BUILD_PAIR("fido2-up-required", JSON_BUILD_BOOLEAN(FLAGS_SET(lock_with, FIDO2ENROLL_UP))), + JSON_BUILD_PAIR("fido2-uv-required", JSON_BUILD_BOOLEAN(FLAGS_SET(lock_with, FIDO2ENROLL_UV))))); + if (r < 0) + return log_error_errno(r, "Failed to prepare FIDO2 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 index 0000000..3315308 --- /dev/null +++ b/src/cryptenroll/cryptenroll-fido2.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <sys/types.h> + +#include "cryptsetup-util.h" +#include "libfido2-util.h" +#include "log.h" + +#if HAVE_LIBFIDO2 +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 void *volume_key, size_t volume_key_size, const char *device, Fido2EnrollFlags lock_with, int cred_alg); + +#else +static inline int load_volume_key_fido2(struct crypt_device *cd, const char *cd_node, const char *device, void *ret_vk, size_t *ret_vks) { + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "FIDO2 unlocking not supported."); +} + +static inline int enroll_fido2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, Fido2EnrollFlags lock_with, int cred_alg) { + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "FIDO2 key enrollment not supported."); +} +#endif diff --git a/src/cryptenroll/cryptenroll-list.c b/src/cryptenroll/cryptenroll-list.c new file mode 100644 index 0000000..d21df71 --- /dev/null +++ b/src/cryptenroll/cryptenroll-list.c @@ -0,0 +1,127 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cryptenroll-list.h" +#include "cryptenroll.h" +#include "format-table.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 slot_max, r; + TableCell *cell; + + assert(cd); + + /* First step, find out all currently used slots */ + assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0); + for (int slot = 0; slot < slot_max; slot++) { + crypt_keyslot_info status; + + status = crypt_keyslot_status(cd, slot); + if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST)) + continue; + + if (!GREEDY_REALLOC(keyslot_metadata, n_keyslot_metadata+1)) + return log_oom(); + + keyslot_metadata[n_keyslot_metadata++] = (struct keyslot_metadata) { + .slot = slot, + }; + } + + /* Second step, enumerate through all tokens, and update the slot table, indicating what kind of + * token they are assigned to */ + for (int token = 0; token < sym_crypt_token_max(CRYPT_LUKS2); token++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + const char *type; + JsonVariant *w, *z; + EnrollType et; + + r = cryptsetup_get_token_as_json(cd, token, NULL, &v); + if (IN_SET(r, -ENOENT, -EINVAL)) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to read JSON token data off disk, ignoring: %m"); + continue; + } + + w = json_variant_by_key(v, "type"); + if (!w || !json_variant_is_string(w)) { + log_warning("Token JSON data lacks type field, ignoring."); + continue; + } + + et = luks2_token_type_from_string(json_variant_string(w)); + if (et < 0) + type = "other"; + else + type = enroll_type_to_string(et); + + w = json_variant_by_key(v, "keyslots"); + if (!w || !json_variant_is_array(w)) { + log_warning("Token JSON data lacks keyslots field, ignoring."); + continue; + } + + JSON_VARIANT_ARRAY_FOREACH(z, w) { + unsigned u; + + if (!json_variant_is_string(z)) { + log_warning("Token JSON data's keyslot field is not an array of strings, ignoring."); + continue; + } + + r = safe_atou(json_variant_string(z), &u); + if (r < 0) { + log_warning_errno(r, "Token JSON data's keyslot field is not an integer formatted as string, ignoring."); + continue; + } + + for (size_t i = 0; i < n_keyslot_metadata; i++) { + if ((unsigned) keyslot_metadata[i].slot != u) + continue; + + if (keyslot_metadata[i].type) /* Slot claimed multiple times? */ + keyslot_metadata[i].type = POINTER_MAX; + else + keyslot_metadata[i].type = type; + } + } + } + + /* Finally, create a table out of it all */ + t = table_new("slot", "type"); + if (!t) + return log_oom(); + + 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++) { + 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"); + if (r < 0) + return table_log_add_error(r); + } + + if (table_get_rows(t) <= 1) { + log_info("No slots found."); + return 0; + } + + r = table_print(t, stdout); + if (r < 0) + return log_error_errno(r, "Failed to show slot table: %m"); + + return 0; +} diff --git a/src/cryptenroll/cryptenroll-list.h b/src/cryptenroll/cryptenroll-list.h new file mode 100644 index 0000000..d322988 --- /dev/null +++ b/src/cryptenroll/cryptenroll-list.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "cryptsetup-util.h" + +int list_enrolled(struct crypt_device *cd); diff --git a/src/cryptenroll/cryptenroll-password.c b/src/cryptenroll/cryptenroll-password.c new file mode 100644 index 0000000..c35b609 --- /dev/null +++ b/src/cryptenroll/cryptenroll-password.c @@ -0,0 +1,181 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "ask-password-api.h" +#include "cryptenroll-password.h" +#include "env-util.h" +#include "errno-util.h" +#include "escape.h" +#include "memory-util.h" +#include "password-quality-util.h" +#include "strv.h" + +int load_volume_key_password( + struct crypt_device *cd, + const char *cd_node, + void *ret_vk, + size_t *ret_vks) { + + _cleanup_(erase_and_freep) char *envpw = NULL; + int r; + + assert_se(cd); + assert_se(cd_node); + assert_se(ret_vk); + assert_se(ret_vks); + + r = getenv_steal_erase("PASSWORD", &envpw); + if (r < 0) + return log_error_errno(r, "Failed to acquire password from environment: %m"); + if (r > 0) { + r = crypt_volume_key_get( + cd, + CRYPT_ANY_SLOT, + ret_vk, + ret_vks, + 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; + _cleanup_free_ char *question = NULL, *disk_path = NULL; + unsigned i = 5; + const char *id; + + question = strjoin("Please enter current passphrase for disk ", cd_node, ":"); + if (!question) + return log_oom(); + + disk_path = cescape(cd_node); + if (!disk_path) + return log_oom(); + + id = strjoina("cryptsetup:", disk_path); + + for (;;) { + _cleanup_strv_free_erase_ char **passwords = NULL; + + if (--i == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), + "Too many attempts, giving up."); + + r = ask_password_auto( + question, "drive-harddisk", id, "cryptenroll", "cryptenroll.passphrase", 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, + ret_vk, + ret_vks, + *p, + strlen(*p)); + if (r >= 0) + break; + } + if (r >= 0) + break; + + log_error_errno(r, "Password not correct, please try again: %m"); + ask_password_flags &= ~ASK_PASSWORD_ACCEPT_CACHED; + } + } + + return r; +} + +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; + + assert_se(node = 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) { + _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", "cryptenroll.new-passphrase", 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", "cryptenroll.new-passphrase", 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 = check_password_quality(new_password, /* old */ NULL, /* user */ NULL, &error); + if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) + log_warning("Password quality check is not supported, proceeding anyway."); + else if (r < 0) + return log_error_errno(r, "Failed to check password quality: %m"); + else if (r == 0) + log_warning("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 index 0000000..aa07a6f --- /dev/null +++ b/src/cryptenroll/cryptenroll-password.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <sys/types.h> + +#include "cryptsetup-util.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 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 index 0000000..54b6b86 --- /dev/null +++ b/src/cryptenroll/cryptenroll-pkcs11.c @@ -0,0 +1,100 @@ +/* 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; + ssize_t base64_encoded_size; + 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 = crypto_random_bytes(decrypted_key, decrypted_key_size); + 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.) */ + base64_encoded_size = base64mem(decrypted_key, decrypted_key_size, &base64_encoded); + if (base64_encoded_size < 0) + return log_error_errno(base64_encoded_size, "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, + base64_encoded_size); + 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_CONST_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 index 0000000..b6d28bd --- /dev/null +++ b/src/cryptenroll/cryptenroll-pkcs11.h @@ -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 index 0000000..7c170f2 --- /dev/null +++ b/src/cryptenroll/cryptenroll-recovery.c @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cryptenroll-recovery.h" +#include "glyph-util.h" +#include "json.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_CONST_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 index 0000000..9bf4f2e --- /dev/null +++ b/src/cryptenroll/cryptenroll-recovery.h @@ -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-tpm2.c b/src/cryptenroll/cryptenroll-tpm2.c new file mode 100644 index 0000000..653ad44 --- /dev/null +++ b/src/cryptenroll/cryptenroll-tpm2.c @@ -0,0 +1,383 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "ask-password-api.h" +#include "cryptenroll-tpm2.h" +#include "env-util.h" +#include "fileio.h" +#include "hexdecoct.h" +#include "json.h" +#include "memory-util.h" +#include "random-util.h" +#include "sha256.h" +#include "tpm2-util.h" + +static int search_policy_hash( + struct crypt_device *cd, + const void *hash, + size_t hash_size) { + + int r; + + assert(cd); + assert(hash || hash_size == 0); + + if (hash_size == 0) + return 0; + + for (int token = 0; token < sym_crypt_token_max(CRYPT_LUKS2); token ++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_free_ void *thash = NULL; + size_t thash_size = 0; + int keyslot; + JsonVariant *w; + + r = cryptsetup_get_token_as_json(cd, token, "systemd-tpm2", &v); + if (IN_SET(r, -ENOENT, -EINVAL, -EMEDIUMTYPE)) + continue; + if (r < 0) + return log_error_errno(r, "Failed to read JSON token data off disk: %m"); + + keyslot = cryptsetup_get_keyslot_from_token(v); + if (keyslot < 0) { + /* Handle parsing errors of the keyslots field gracefully, since it's not 'owned' by + * us, but by the LUKS2 spec */ + log_warning_errno(keyslot, "Failed to determine keyslot of JSON token %i, skipping: %m", token); + continue; + } + + w = json_variant_by_key(v, "tpm2-policy-hash"); + if (!w || !json_variant_is_string(w)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "TPM2 token data lacks 'tpm2-policy-hash' field."); + + r = unhexmem(json_variant_string(w), SIZE_MAX, &thash, &thash_size); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid base64 data in 'tpm2-policy-hash' field."); + + if (memcmp_nn(hash, hash_size, thash, thash_size) == 0) + return keyslot; /* Found entry with same hash. */ + } + + return -ENOENT; /* Not found */ +} + +static int get_pin(char **ret_pin_str, TPM2Flags *ret_flags) { + _cleanup_(erase_and_freep) char *pin_str = NULL; + TPM2Flags flags = 0; + int r; + + assert(ret_pin_str); + assert(ret_flags); + + r = getenv_steal_erase("NEWPIN", &pin_str); + if (r < 0) + return log_error_errno(r, "Failed to acquire PIN from environment: %m"); + if (r > 0) + flags |= TPM2_FLAGS_USE_PIN; + else { + for (size_t i = 5;; i--) { + _cleanup_strv_free_erase_ char **pin = NULL, **pin2 = NULL; + + if (i <= 0) + return log_error_errno( + SYNTHETIC_ERRNO(ENOKEY), "Too many attempts, giving up."); + + pin = strv_free_erase(pin); + r = ask_password_auto( + "Please enter TPM2 PIN:", + "drive-harddisk", + NULL, + "tpm2-pin", + "cryptenroll.tpm2-pin", + USEC_INFINITY, + 0, + &pin); + if (r < 0) + return log_error_errno(r, "Failed to ask for user pin: %m"); + assert(strv_length(pin) == 1); + + r = ask_password_auto( + "Please enter TPM2 PIN (repeat):", + "drive-harddisk", + NULL, + "tpm2-pin", + "cryptenroll.tpm2-pin", + USEC_INFINITY, + 0, + &pin2); + if (r < 0) + return log_error_errno(r, "Failed to ask for user pin: %m"); + assert(strv_length(pin) == 1); + + if (strv_equal(pin, pin2)) { + pin_str = strdup(*pin); + if (!pin_str) + return log_oom(); + flags |= TPM2_FLAGS_USE_PIN; + break; + } + + log_error("PINs didn't match, please try again!"); + } + } + + *ret_flags = flags; + *ret_pin_str = TAKE_PTR(pin_str); + + return 0; +} + +int enroll_tpm2(struct crypt_device *cd, + const void *volume_key, + size_t volume_key_size, + 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, + uint32_t pubkey_pcr_mask, + const char *signature_path, + bool use_pin, + const char *pcrlock_path) { + + _cleanup_(erase_and_freep) void *secret = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *signature_json = NULL; + _cleanup_(erase_and_freep) char *base64_encoded = NULL; + _cleanup_free_ void *srk_buf = NULL; + size_t secret_size, blob_size, pubkey_size = 0, srk_buf_size = 0; + _cleanup_free_ void *blob = NULL, *pubkey = NULL; + const char *node; + _cleanup_(erase_and_freep) char *pin_str = NULL; + ssize_t base64_encoded_size; + int r, keyslot; + TPM2Flags flags = 0; + uint8_t binary_salt[SHA256_DIGEST_SIZE] = {}; + /* + * erase the salt, we'd rather attempt to not have this in a coredump + * as an attacker would have all the parameters but pin used to create + * the session key. This problem goes away when we move to a trusted + * primary key, aka the SRK. + */ + CLEANUP_ERASE(binary_salt); + + assert(cd); + assert(volume_key); + assert(volume_key_size > 0); + assert(tpm2_pcr_values_valid(hash_pcr_values, n_hash_pcr_values)); + assert(TPM2_PCR_MASK_VALID(pubkey_pcr_mask)); + + assert_se(node = crypt_get_device_name(cd)); + + if (use_pin) { + r = get_pin(&pin_str, &flags); + if (r < 0) + return r; + + r = crypto_random_bytes(binary_salt, sizeof(binary_salt)); + if (r < 0) + return log_error_errno(r, "Failed to acquire random salt: %m"); + + uint8_t salted_pin[SHA256_DIGEST_SIZE] = {}; + CLEANUP_ERASE(salted_pin); + r = tpm2_util_pbkdf2_hmac_sha256(pin_str, strlen(pin_str), binary_salt, sizeof(binary_salt), salted_pin); + if (r < 0) + return log_error_errno(r, "Failed to perform PBKDF2: %m"); + + pin_str = erase_and_free(pin_str); + /* re-stringify pin_str */ + base64_encoded_size = base64mem(salted_pin, sizeof(salted_pin), &pin_str); + if (base64_encoded_size < 0) + return log_error_errno(base64_encoded_size, "Failed to base64 encode salted pin: %m"); + } + + TPM2B_PUBLIC public = {}; + r = tpm2_load_pcr_public_key(pubkey_path, &pubkey, &pubkey_size); + if (r < 0) { + if (pubkey_path || signature_path || 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"); + pubkey_pcr_mask = 0; + } else { + r = tpm2_tpm2b_public_from_pem(pubkey, pubkey_size, &public); + if (r < 0) + return log_error_errno(r, "Could not convert public key to TPM2B_PUBLIC: %m"); + + if (signature_path) { + /* 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); + if (r < 0) + return log_debug_errno(r, "Failed to read TPM PCR signature: %m"); + } + } + + bool any_pcr_value_specified = tpm2_pcr_values_has_any_values(hash_pcr_values, 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 (r < 0) + return r; + + any_pcr_value_specified = true; + flags |= TPM2_FLAGS_USE_PCRLOCK; + } + + _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 (r < 0) + return r; + + if (!tpm2_pcr_values_has_all_values(hash_pcr_values, n_hash_pcr_values)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Must provide all PCR values when using TPM2 device key."); + } else { + r = tpm2_context_new(device, &tpm2_context); + if (r < 0) + return log_error_errno(r, "Failed to create TPM2 context: %m"); + + 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 (r < 0) + return log_error_errno(r, "Could not read pcr values: %m"); + } + } + + uint16_t hash_pcr_bank = 0; + uint32_t hash_pcr_mask = 0; + if (n_hash_pcr_values > 0) { + size_t hash_count; + r = tpm2_pcr_values_hash_count(hash_pcr_values, n_hash_pcr_values, &hash_count); + if (r < 0) + return log_error_errno(r, "Could not get hash count: %m"); + + if (hash_count > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Multiple PCR banks selected."); + + 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); + if (r < 0) + return log_error_errno(r, "Could not get hash mask: %m"); + } + + TPM2B_DIGEST policy = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); + r = tpm2_calculate_sealing_policy( + hash_pcr_values, + n_hash_pcr_values, + pubkey ? &public : NULL, + use_pin, + pcrlock_path ? &pcrlock_policy : NULL, + &policy); + if (r < 0) + return r; + + if (device_key) + r = tpm2_calculate_seal( + seal_key_handle, + &device_key_public, + /* attributes= */ NULL, + /* secret= */ NULL, /* secret_size= */ 0, + &policy, + pin_str, + &secret, &secret_size, + &blob, &blob_size, + &srk_buf, &srk_buf_size); + else + r = tpm2_seal(tpm2_context, + seal_key_handle, + &policy, + pin_str, + &secret, &secret_size, + &blob, &blob_size, + /* ret_primary_alg= */ NULL, + &srk_buf, &srk_buf_size); + if (r < 0) + return log_error_errno(r, "Failed to seal to TPM2: %m"); + + /* Let's see if we already have this specific PCR policy hash enrolled, if so, exit early. */ + r = search_policy_hash(cd, policy.buffer, policy.size); + if (r == -ENOENT) + log_debug_errno(r, "PCR policy hash not yet enrolled, enrolling now."); + else if (r < 0) + return r; + else { + log_info("This PCR set is already enrolled, executing no operation."); + return r; /* return existing keyslot, so that wiping won't kill it */ + } + + /* If possible, verify the sealed data object. */ + if ((!pubkey || signature_json) && !any_pcr_value_specified && !device_key) { + _cleanup_(erase_and_freep) void *secret2 = NULL; + size_t secret2_size; + + log_debug("Unsealing for verification..."); + r = tpm2_unseal(tpm2_context, + hash_pcr_mask, + hash_pcr_bank, + pubkey, pubkey_size, + pubkey_pcr_mask, + signature_json, + pin_str, + pcrlock_path ? &pcrlock_policy : NULL, + /* primary_alg= */ 0, + blob, blob_size, + policy.buffer, policy.size, + srk_buf, srk_buf_size, + &secret2, &secret2_size); + if (r < 0) + return log_error_errno(r, "Failed to unseal secret using TPM2: %m"); + + if (memcmp_nn(secret, secret_size, secret2, secret2_size) != 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "TPM2 seal/unseal verification failed."); + } + + /* let's base64 encode the key to use, for compat with homed (and it's easier to every type it in by keyboard, if that might end up being necessary. */ + base64_encoded_size = base64mem(secret, secret_size, &base64_encoded); + if (base64_encoded_size < 0) + return log_error_errno(base64_encoded_size, "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, + base64_encoded_size); + if (keyslot < 0) + return log_error_errno(keyslot, "Failed to add new TPM2 key to %s: %m", node); + + r = tpm2_make_luks2_json( + keyslot, + hash_pcr_mask, + hash_pcr_bank, + pubkey, pubkey_size, + pubkey_pcr_mask, + /* primary_alg= */ 0, + blob, blob_size, + policy.buffer, policy.size, + use_pin ? binary_salt : NULL, + use_pin ? sizeof(binary_salt) : 0, + srk_buf, srk_buf_size, + flags, + &v); + if (r < 0) + return log_error_errno(r, "Failed to prepare TPM2 JSON token object: %m"); + + r = cryptsetup_add_token_json(cd, v); + if (r < 0) + return log_error_errno(r, "Failed to add TPM2 JSON token to LUKS2 header: %m"); + + log_info("New TPM2 token enrolled as key slot %i.", keyslot); + return keyslot; +} diff --git a/src/cryptenroll/cryptenroll-tpm2.h b/src/cryptenroll/cryptenroll-tpm2.h new file mode 100644 index 0000000..2fbcdd4 --- /dev/null +++ b/src/cryptenroll/cryptenroll-tpm2.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <sys/types.h> + +#include "cryptsetup-util.h" +#include "log.h" +#include "tpm2-util.h" + +#if HAVE_TPM2 +int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t seal_key_handle, const char *device_key, Tpm2PCRValue *hash_pcrs, size_t n_hash_pcrs, const char *pubkey_path, uint32_t pubkey_pcr_mask, const char *signature_path, bool use_pin, const char *pcrlock_path); +#else +static inline int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t seal_key_handle, const char *device_key, Tpm2PCRValue *hash_pcrs, size_t n_hash_pcrs, const char *pubkey_path, uint32_t pubkey_pcr_mask, const char *signature_path, bool use_pin, const char *pcrlock_path) { + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "TPM2 key enrollment not supported."); +} +#endif diff --git a/src/cryptenroll/cryptenroll-wipe.c b/src/cryptenroll/cryptenroll-wipe.c new file mode 100644 index 0000000..314ebd3 --- /dev/null +++ b/src/cryptenroll/cryptenroll-wipe.c @@ -0,0 +1,445 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cryptenroll-wipe.h" +#include "cryptenroll.h" +#include "json.h" +#include "memory-util.h" +#include "parse-util.h" +#include "set.h" +#include "sort-util.h" + +static int find_all_slots(struct crypt_device *cd, Set *wipe_slots, Set *keep_slots) { + int slot_max; + + assert(cd); + assert(wipe_slots); + assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0); + + /* Finds all currently assigned slots, and adds them to 'wipe_slots', except if listed already in 'keep_slots' */ + + for (int slot = 0; slot < slot_max; slot++) { + crypt_keyslot_info status; + + /* No need to check this slot if we already know we want to wipe it or definitely keep it. */ + if (set_contains(keep_slots, INT_TO_PTR(slot)) || + set_contains(wipe_slots, INT_TO_PTR(slot))) + continue; + + status = crypt_keyslot_status(cd, slot); + if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST)) + continue; + + if (set_put(wipe_slots, INT_TO_PTR(slot)) < 0) + return log_oom(); + } + + return 0; +} + +static int find_empty_passphrase_slots(struct crypt_device *cd, Set *wipe_slots, Set *keep_slots) { + size_t vks; + int r, slot_max; + + assert(cd); + assert(wipe_slots); + assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0); + + /* Finds all slots with an empty passphrase assigned (i.e. "") and adds them to 'wipe_slots', except + * if listed already in 'keep_slots' */ + + 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; + + for (int slot = 0; slot < slot_max; slot++) { + _cleanup_(erase_and_freep) char *vk = NULL; + crypt_keyslot_info status; + + /* No need to check this slot if we already know we want to wipe it or definitely keep it. */ + if (set_contains(keep_slots, INT_TO_PTR(slot)) || + set_contains(wipe_slots, INT_TO_PTR(slot))) + continue; + + status = crypt_keyslot_status(cd, slot); + if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST)) + continue; + + vk = malloc(vks); + if (!vk) + return log_oom(); + + r = crypt_volume_key_get(cd, slot, vk, &vks, "", 0); + if (r < 0) { + log_debug_errno(r, "Failed to acquire volume key from slot %i with empty password, ignoring: %m", slot); + continue; + } + + if (set_put(wipe_slots, INT_TO_PTR(r)) < 0) + return log_oom(); + } + + return 0; +} + +static int find_slots_by_mask( + struct crypt_device *cd, + Set *wipe_slots, + Set *keep_slots, + unsigned by_mask) { + + _cleanup_set_free_ Set *listed_slots = NULL; + int r; + + assert(cd); + assert(wipe_slots); + + if (by_mask == 0) + return 0; + + /* Find all slots that are associated with a token of a type in the specified token type mask */ + + for (int token = 0; token < sym_crypt_token_max(CRYPT_LUKS2); token++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + JsonVariant *w, *z; + EnrollType t; + + r = cryptsetup_get_token_as_json(cd, token, NULL, &v); + if (IN_SET(r, -ENOENT, -EINVAL)) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to read JSON token data off disk, ignoring: %m"); + continue; + } + + w = json_variant_by_key(v, "type"); + if (!w || !json_variant_is_string(w)) { + log_warning("Token JSON data lacks type field, ignoring."); + continue; + } + + t = luks2_token_type_from_string(json_variant_string(w)); + + w = json_variant_by_key(v, "keyslots"); + if (!w || !json_variant_is_array(w)) { + log_warning("Token JSON data lacks keyslots field, ignoring."); + continue; + } + + JSON_VARIANT_ARRAY_FOREACH(z, w) { + int slot; + + if (!json_variant_is_string(z)) { + log_warning("Token JSON data's keyslot field is not an array of strings, ignoring."); + continue; + } + + r = safe_atoi(json_variant_string(z), &slot); + if (r < 0) { + log_warning_errno(r, "Token JSON data's keyslot filed is not an integer formatted as string, ignoring."); + continue; + } + + if (t >= 0 && (by_mask & (1U << t)) != 0) { + /* Selected by token type */ + if (set_put(wipe_slots, INT_TO_PTR(slot)) < 0) + return log_oom(); + } else if ((by_mask & (1U << ENROLL_PASSWORD)) != 0) { + /* If we shall remove all plain password slots, let's maintain a list of + * slots that are listed in any tokens, since those are *NOT* plain + * passwords */ + if (set_ensure_allocated(&listed_slots, NULL) < 0) + return log_oom(); + + if (set_put(listed_slots, INT_TO_PTR(slot)) < 0) + return log_oom(); + } + } + } + + /* "password" slots are those which have no token assigned. If we shall remove those, iterate through + * all slots and mark those for wiping that weren't listed in any token */ + if ((by_mask & (1U << ENROLL_PASSWORD)) != 0) { + int slot_max; + + assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0); + + for (int slot = 0; slot < slot_max; slot++) { + crypt_keyslot_info status; + + /* No need to check this slot if we already know we want to wipe it or definitely keep it. */ + if (set_contains(keep_slots, INT_TO_PTR(slot)) || + set_contains(wipe_slots, INT_TO_PTR(slot))) + continue; + + if (set_contains(listed_slots, INT_TO_PTR(slot))) /* This has a token, hence is not a password. */ + continue; + + status = crypt_keyslot_status(cd, slot); + if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST)) /* Not actually assigned? */ + continue; + + /* Finally, we found a password, add it to the list of slots to wipe */ + if (set_put(wipe_slots, INT_TO_PTR(slot)) < 0) + return log_oom(); + } + } + + return 0; +} + +static int find_slot_tokens(struct crypt_device *cd, Set *wipe_slots, Set *keep_slots, Set *wipe_tokens) { + int r; + + assert(cd); + assert(wipe_slots); + assert(keep_slots); + assert(wipe_tokens); + + /* Find all tokens matching the slots we want to wipe, so that we can wipe them too. Also, for update + * the slots sets according to the token data: add any other slots listed in the tokens we act on. */ + + for (int token = 0; token < sym_crypt_token_max(CRYPT_LUKS2); token++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + bool shall_wipe = false; + JsonVariant *w, *z; + + r = cryptsetup_get_token_as_json(cd, token, NULL, &v); + if (IN_SET(r, -ENOENT, -EINVAL)) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to read JSON token data off disk, ignoring: %m"); + continue; + } + + w = json_variant_by_key(v, "keyslots"); + if (!w || !json_variant_is_array(w)) { + log_warning("Token JSON data lacks keyslots field, ignoring."); + continue; + } + + /* Go through the slots associated with this token: if we shall keep any slot of them, the token shall stay too. */ + JSON_VARIANT_ARRAY_FOREACH(z, w) { + int slot; + + if (!json_variant_is_string(z)) { + log_warning("Token JSON data's keyslot field is not an array of strings, ignoring."); + continue; + } + + r = safe_atoi(json_variant_string(z), &slot); + if (r < 0) { + log_warning_errno(r, "Token JSON data's keyslot filed is not an integer formatted as string, ignoring."); + continue; + } + + if (set_contains(keep_slots, INT_TO_PTR(slot))) { + shall_wipe = false; + break; /* If we shall keep this slot, then this is definite: we will keep its token too */ + } + + /* If there's a slot associated with this token that we shall wipe, then remove the + * token too. But we are careful here: let's continue iterating, maybe there's a slot + * that we need to keep, in which case we can reverse the decision again. */ + if (set_contains(wipe_slots, INT_TO_PTR(slot))) + shall_wipe = true; + } + + /* Go through the slots again, and this time add them to the list of slots to keep/remove */ + JSON_VARIANT_ARRAY_FOREACH(z, w) { + int slot; + + if (!json_variant_is_string(z)) + continue; + if (safe_atoi(json_variant_string(z), &slot) < 0) + continue; + + if (set_put(shall_wipe ? wipe_slots : keep_slots, INT_TO_PTR(slot)) < 0) + return log_oom(); + } + + /* And of course, also remember the tokens to remove. */ + if (shall_wipe) + if (set_put(wipe_tokens, INT_TO_PTR(token)) < 0) + return log_oom(); + } + + return 0; +} + +static bool slots_remain(struct crypt_device *cd, Set *wipe_slots, Set *keep_slots) { + int slot_max; + + assert(cd); + assert_se((slot_max = crypt_keyslot_max(CRYPT_LUKS2)) > 0); + + /* Checks if any slots remaining in the LUKS2 header if we remove all slots listed in 'wipe_slots' + * (keeping those listed in 'keep_slots') */ + + for (int slot = 0; slot < slot_max; slot++) { + crypt_keyslot_info status; + + status = crypt_keyslot_status(cd, slot); + if (!IN_SET(status, CRYPT_SLOT_ACTIVE, CRYPT_SLOT_ACTIVE_LAST)) + continue; + + /* The "keep" set wins if a slot is listed in both sets. This is important so that we can + * safely add a new slot and remove all others of the same type, which in a naive + * implementation might mean we remove what we just added — which we of course don't want. */ + if (set_contains(keep_slots, INT_TO_PTR(slot)) || + !set_contains(wipe_slots, INT_TO_PTR(slot))) + return true; + } + + 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) { + + _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; + int r, slot_max, ret; + void *e; + + assert_se(cd); + + /* Shortcut if nothing to wipe. */ + if (n_explicit_slots == 0 && by_mask == 0 && by_scope == WIPE_EXPLICIT) + return 0; + + /* So this is a bit more complicated than I'd wish, but we want support three different axis for wiping slots: + * + * 1. Wiping by slot indexes + * 2. Wiping slots of specified token types + * 3. Wiping "all" entries, or entries with an empty password (i.e. "") + * + * (or any combination of the above) + * + * Plus: We always want to remove tokens matching the slots. + * Plus: We always want to exclude the slots/tokens we just added. + */ + + wipe_slots = set_new(NULL); + keep_slots = set_new(NULL); + wipe_tokens = set_new(NULL); + if (!wipe_slots || !keep_slots || !wipe_tokens) + 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) + return log_oom(); + + assert_se((slot_max = 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]); + + if (set_put(wipe_slots, INT_TO_PTR(explicit_slots[i])) < 0) + return log_oom(); + } + + /* Now, handle the "all" and "empty passphrase" cases. */ + switch (by_scope) { + + case WIPE_EXPLICIT: + break; /* Nothing to do here */ + + case WIPE_ALL: + r = find_all_slots(cd, wipe_slots, keep_slots); + if (r < 0) + return r; + + break; + + case WIPE_EMPTY_PASSPHRASE: + r = find_empty_passphrase_slots(cd, wipe_slots, keep_slots); + if (r < 0) + return r; + + break; + default: + assert_not_reached(); + } + + /* Then add all slots that match a token type */ + r = find_slots_by_mask(cd, wipe_slots, keep_slots, by_mask); + if (r < 0) + return r; + + /* And determine tokens that we shall remove */ + r = find_slot_tokens(cd, wipe_slots, keep_slots, wipe_tokens); + if (r < 0) + return r; + + /* Safety check: let's make sure that after we are done there's at least one slot remaining */ + if (!slots_remain(cd, wipe_slots, keep_slots)) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), + "Wipe operation would leave no valid slots around, can't allow that, sorry."); + + /* Generated ordered lists of the slots and the tokens to remove */ + ordered_slots = new(int, set_size(wipe_slots)); + if (!ordered_slots) + return log_oom(); + SET_FOREACH(e, wipe_slots) { + int slot = PTR_TO_INT(e); + + if (set_contains(keep_slots, INT_TO_PTR(slot))) + continue; + + ordered_slots[n_ordered_slots++] = slot; + } + typesafe_qsort(ordered_slots, n_ordered_slots, cmp_int); + + ordered_tokens = new(int, set_size(wipe_tokens)); + if (!ordered_tokens) + return log_oom(); + SET_FOREACH(e, wipe_tokens) + ordered_tokens[n_ordered_tokens++] = PTR_TO_INT(e); + 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, + "No slots to remove selected."); + return 0; + } + + if (DEBUG_LOGGING) { + for (size_t i = 0; i < n_ordered_slots; i++) + log_debug("Going to wipe slot %i.", ordered_slots[i]); + for (size_t i = 0; i < n_ordered_tokens; i++) + log_debug("Going to wipe token %i.", ordered_tokens[i]); + } + + /* Now, let's actually start wiping things. (We go from back to front, to make space at the end + * first.) */ + ret = 0; + for (size_t i = n_ordered_slots; i > 0; i--) { + r = crypt_keyslot_destroy(cd, ordered_slots[i - 1]); + if (r < 0) { + log_warning_errno(r, "Failed to wipe slot %i, continuing: %m", ordered_slots[i - 1]); + if (ret == 0) + ret = r; + } else + log_info("Wiped slot %i.", ordered_slots[i - 1]); + } + + for (size_t i = n_ordered_tokens; i > 0; i--) { + r = crypt_token_json_set(cd, ordered_tokens[i - 1], NULL); + if (r < 0) { + log_warning_errno(r, "Failed to wipe token %i, continuing: %m", ordered_tokens[i - 1]); + if (ret == 0) + ret = r; + } + } + + return ret; +} diff --git a/src/cryptenroll/cryptenroll-wipe.h b/src/cryptenroll/cryptenroll-wipe.h new file mode 100644 index 0000000..5bcd783 --- /dev/null +++ b/src/cryptenroll/cryptenroll-wipe.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "cryptenroll.h" +#include "cryptsetup-util.h" + +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); diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c new file mode 100644 index 0000000..1cb6652 --- /dev/null +++ b/src/cryptenroll/cryptenroll.c @@ -0,0 +1,762 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> + +#include "ask-password-api.h" +#include "build.h" +#include "cryptenroll-fido2.h" +#include "cryptenroll-list.h" +#include "cryptenroll-password.h" +#include "cryptenroll-pkcs11.h" +#include "cryptenroll-recovery.h" +#include "cryptenroll-tpm2.h" +#include "cryptenroll-wipe.h" +#include "cryptenroll.h" +#include "cryptsetup-util.h" +#include "env-util.h" +#include "escape.h" +#include "fileio.h" +#include "libfido2-util.h" +#include "main-func.h" +#include "memory-util.h" +#include "parse-argument.h" +#include "parse-util.h" +#include "path-util.h" +#include "pkcs11-util.h" +#include "pretty-print.h" +#include "string-table.h" +#include "strv.h" +#include "terminal-util.h" +#include "tpm2-pcr.h" + +static EnrollType arg_enroll_type = _ENROLL_TYPE_INVALID; +static char *arg_unlock_keyfile = NULL; +static UnlockType arg_unlock_type = UNLOCK_PASSWORD; +static char *arg_unlock_fido2_device = NULL; +static char *arg_pkcs11_token_uri = NULL; +static char *arg_fido2_device = NULL; +static char *arg_tpm2_device = NULL; +static uint32_t arg_tpm2_seal_key_handle = 0; +static char *arg_tpm2_device_key = NULL; +static Tpm2PCRValue *arg_tpm2_hash_pcr_values = NULL; +static size_t arg_tpm2_n_hash_pcr_values = 0; +static bool arg_tpm2_pin = false; +static char *arg_tpm2_public_key = NULL; +static uint32_t arg_tpm2_public_key_pcr_mask = 0; +static char *arg_tpm2_signature = NULL; +static char *arg_tpm2_pcrlock = NULL; +static char *arg_node = NULL; +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 Fido2EnrollFlags arg_fido2_lock_with = FIDO2ENROLL_PIN | FIDO2ENROLL_UP; +#if HAVE_LIBFIDO2 +static int arg_fido2_cred_alg = COSE_ES256; +#else +static int arg_fido2_cred_alg = 0; +#endif + +assert_cc(sizeof(arg_wipe_slots_mask) * 8 >= _ENROLL_TYPE_MAX); + +STATIC_DESTRUCTOR_REGISTER(arg_unlock_keyfile, freep); +STATIC_DESTRUCTOR_REGISTER(arg_unlock_fido2_device, freep); +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_tpm2_device_key, freep); +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_hash_pcr_values, freep); +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep); +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep); +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_pcrlock, freep); +STATIC_DESTRUCTOR_REGISTER(arg_node, freep); +STATIC_DESTRUCTOR_REGISTER(arg_wipe_slots, freep); + +static bool wipe_requested(void) { + return arg_n_wipe_slots > 0 || + arg_wipe_slots_scope != WIPE_EXPLICIT || + arg_wipe_slots_mask != 0; +} + +static const char* const enroll_type_table[_ENROLL_TYPE_MAX] = { + [ENROLL_PASSWORD] = "password", + [ENROLL_RECOVERY] = "recovery", + [ENROLL_PKCS11] = "pkcs11", + [ENROLL_FIDO2] = "fido2", + [ENROLL_TPM2] = "tpm2", +}; + +DEFINE_STRING_TABLE_LOOKUP(enroll_type, EnrollType); + +static const char *const luks2_token_type_table[_ENROLL_TYPE_MAX] = { + /* ENROLL_PASSWORD has no entry here, as slots of this type do not have a token in the LUKS2 header */ + [ENROLL_RECOVERY] = "systemd-recovery", + [ENROLL_PKCS11] = "systemd-pkcs11", + [ENROLL_FIDO2] = "systemd-fido2", + [ENROLL_TPM2] = "systemd-tpm2", +}; + +DEFINE_STRING_TABLE_LOOKUP(luks2_token_type, EnrollType); + +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("%1$s [OPTIONS...] BLOCK-DEVICE\n\n" + "%5$sEnroll a security token or authentication credential to a LUKS volume.%6$s\n\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --wipe-slot=SLOT1,SLOT2,…\n" + " Wipe specified slots\n" + "\n%3$sUnlocking:%4$s\n" + " --unlock-key-file=PATH\n" + " Use a file to unlock the volume\n" + " --unlock-fido2-device=PATH\n" + " Use a FIDO2 device to unlock the volume\n" + "\n%3$sSimple Enrollment:%4$s\n" + " --password Enroll a user-supplied password\n" + " --recovery-key Enroll a recovery key\n" + "\n%3$sPKCS11 Enrollment:%4$s\n" + " --pkcs11-token-uri=URI\n" + " Specify PKCS#11 security token URI\n" + "\n%3$sFIDO2 Enrollment:%4$s\n" + " --fido2-device=PATH\n" + " Enroll a FIDO2-HMAC security token\n" + " --fido2-credential-algorithm=STRING\n" + " Specify COSE algorithm for FIDO2 credential\n" + " --fido2-with-client-pin=BOOL\n" + " Whether to require entering a PIN to unlock the volume\n" + " --fido2-with-user-presence=BOOL\n" + " Whether to require user presence to unlock the volume\n" + " --fido2-with-user-verification=BOOL\n" + " Whether to require user verification to unlock the volume\n" + "\n%3$sTPM2 Enrollment:%4$s\n" + " --tpm2-device=PATH\n" + " Enroll a TPM2 device\n" + " --tpm2-device-key=PATH\n" + " Enroll a TPM2 device using its public key\n" + " --tpm2-seal-key-handle=HANDLE\n" + " Specify handle of key to use for sealing\n" + " --tpm2-pcrs=PCR1+PCR2+PCR3+…\n" + " Specify TPM2 PCRs to seal against\n" + " --tpm2-public-key=PATH\n" + " Enroll signed TPM2 PCR policy against PEM public key\n" + " --tpm2-public-key-pcrs=PCR1+PCR2+PCR3+…\n" + " Enroll signed TPM2 PCR policy for specified TPM2 PCRs\n" + " --tpm2-signature=PATH\n" + " Validate public key enrollment works with JSON signature\n" + " file\n" + " --tpm2-pcrlock=PATH\n" + " Specify pcrlock policy to lock against\n" + " --tpm2-with-pin=BOOL\n" + " Whether to require entering a PIN to unlock the volume\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), + ansi_normal(), + ansi_highlight(), + ansi_normal()); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + ARG_PASSWORD, + ARG_RECOVERY_KEY, + ARG_UNLOCK_KEYFILE, + ARG_UNLOCK_FIDO2_DEVICE, + ARG_PKCS11_TOKEN_URI, + ARG_FIDO2_DEVICE, + ARG_TPM2_DEVICE, + ARG_TPM2_DEVICE_KEY, + ARG_TPM2_SEAL_KEY_HANDLE, + ARG_TPM2_PCRS, + ARG_TPM2_PUBLIC_KEY, + ARG_TPM2_PUBLIC_KEY_PCRS, + ARG_TPM2_SIGNATURE, + ARG_TPM2_PCRLOCK, + ARG_TPM2_WITH_PIN, + ARG_WIPE_SLOT, + ARG_FIDO2_WITH_PIN, + ARG_FIDO2_WITH_UP, + ARG_FIDO2_WITH_UV, + ARG_FIDO2_CRED_ALG, + }; + + 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 }, + { "unlock-key-file", required_argument, NULL, ARG_UNLOCK_KEYFILE }, + { "unlock-fido2-device", required_argument, NULL, ARG_UNLOCK_FIDO2_DEVICE }, + { "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI }, + { "fido2-credential-algorithm", required_argument, NULL, ARG_FIDO2_CRED_ALG }, + { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE }, + { "fido2-with-client-pin", required_argument, NULL, ARG_FIDO2_WITH_PIN }, + { "fido2-with-user-presence", required_argument, NULL, ARG_FIDO2_WITH_UP }, + { "fido2-with-user-verification", required_argument, NULL, ARG_FIDO2_WITH_UV }, + { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, + { "tpm2-device-key", required_argument, NULL, ARG_TPM2_DEVICE_KEY }, + { "tpm2-seal-key-handle", required_argument, NULL, ARG_TPM2_SEAL_KEY_HANDLE }, + { "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS }, + { "tpm2-public-key", required_argument, NULL, ARG_TPM2_PUBLIC_KEY }, + { "tpm2-public-key-pcrs", required_argument, NULL, ARG_TPM2_PUBLIC_KEY_PCRS }, + { "tpm2-signature", required_argument, NULL, ARG_TPM2_SIGNATURE }, + { "tpm2-pcrlock", required_argument, NULL, ARG_TPM2_PCRLOCK }, + { "tpm2-with-pin", required_argument, NULL, ARG_TPM2_WITH_PIN }, + { "wipe-slot", required_argument, NULL, ARG_WIPE_SLOT }, + {} + }; + + bool auto_hash_pcr_values = true, auto_public_key_pcr_mask = true, auto_pcrlock = true; + 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_FIDO2_WITH_PIN: + r = parse_boolean_argument("--fido2-with-client-pin=", optarg, NULL); + if (r < 0) + return r; + + SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_PIN, r); + break; + + case ARG_FIDO2_WITH_UP: + r = parse_boolean_argument("--fido2-with-user-presence=", optarg, NULL); + if (r < 0) + return r; + + SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UP, r); + break; + + case ARG_FIDO2_WITH_UV: + r = parse_boolean_argument("--fido2-with-user-verification=", optarg, NULL); + if (r < 0) + return r; + + SET_FLAG(arg_fido2_lock_with, FIDO2ENROLL_UV, r); + break; + + 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_UNLOCK_KEYFILE: + if (arg_unlock_type != UNLOCK_PASSWORD) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Multiple unlock methods specified at once, refusing."); + + r = parse_path_argument(optarg, /* suppress_root= */ true, &arg_unlock_keyfile); + if (r < 0) + return r; + + arg_unlock_type = UNLOCK_KEYFILE; + break; + + case ARG_UNLOCK_FIDO2_DEVICE: { + _cleanup_free_ char *device = NULL; + + if (arg_unlock_type != UNLOCK_PASSWORD) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Multiple unlock methods specified at once, refusing."); + + assert(!arg_unlock_fido2_device); + + if (!streq(optarg, "auto")) { + device = strdup(optarg); + if (!device) + return log_oom(); + } + + arg_unlock_type = UNLOCK_FIDO2; + arg_unlock_fido2_device = TAKE_PTR(device); + 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_CRED_ALG: + r = parse_fido2_algorithm(optarg, &arg_fido2_cred_alg); + if (r < 0) + return log_error_errno(r, "Failed to parse COSE algorithm: %s", optarg); + 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")) { + device = strdup(optarg); + if (!device) + return log_oom(); + } + + arg_enroll_type = ENROLL_FIDO2; + arg_fido2_device = TAKE_PTR(device); + break; + } + + case ARG_TPM2_DEVICE: { + _cleanup_free_ char *device = NULL; + + if (streq(optarg, "list")) + return tpm2_list_devices(); + + if (arg_enroll_type >= 0 || arg_tpm2_device) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Multiple operations specified at once, refusing."); + + if (!streq(optarg, "auto")) { + device = strdup(optarg); + if (!device) + return log_oom(); + } + + arg_enroll_type = ENROLL_TPM2; + arg_tpm2_device = TAKE_PTR(device); + break; + } + + case ARG_TPM2_DEVICE_KEY: + if (arg_enroll_type >= 0 || arg_tpm2_device_key) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Multiple operations specified at once, refusing."); + + + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_device_key); + if (r < 0) + return r; + + arg_enroll_type = ENROLL_TPM2; + break; + + case ARG_TPM2_SEAL_KEY_HANDLE: + r = safe_atou32_full(optarg, 16, &arg_tpm2_seal_key_handle); + if (r < 0) + return log_error_errno(r, "Could not parse TPM2 seal key handle index '%s': %m", optarg); + + break; + + case ARG_TPM2_PCRS: + auto_hash_pcr_values = false; + r = tpm2_parse_pcr_argument_append(optarg, &arg_tpm2_hash_pcr_values, &arg_tpm2_n_hash_pcr_values); + if (r < 0) + return r; + + break; + + case ARG_TPM2_PUBLIC_KEY: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_public_key); + if (r < 0) + return r; + + break; + + case ARG_TPM2_PUBLIC_KEY_PCRS: + auto_public_key_pcr_mask = false; + r = tpm2_parse_pcr_argument_to_mask(optarg, &arg_tpm2_public_key_pcr_mask); + if (r < 0) + return r; + + break; + + case ARG_TPM2_SIGNATURE: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_signature); + if (r < 0) + return r; + + break; + + case ARG_TPM2_PCRLOCK: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_pcrlock); + if (r < 0) + return r; + + auto_pcrlock = false; + break; + + case ARG_TPM2_WITH_PIN: + r = parse_boolean_argument("--tpm2-with-pin=", optarg, &arg_tpm2_pin); + if (r < 0) + return r; + + break; + + case ARG_WIPE_SLOT: { + const char *p = optarg; + + if (isempty(optarg)) { + arg_wipe_slots_mask = 0; + arg_wipe_slots_scope = WIPE_EXPLICIT; + break; + } + + for (;;) { + _cleanup_free_ char *slot = NULL; + unsigned n; + + r = extract_first_word(&p, &slot, ",", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == 0) + break; + if (r < 0) + return log_error_errno(r, "Failed to parse slot list: %s", optarg); + + 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 { + int *a; + + r = safe_atou(slot, &n); + if (r < 0) + return log_error_errno(r, "Failed to parse slot index: %s", slot); + if (n > INT_MAX) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Slot index out of range: %u", n); + + a = reallocarray(arg_wipe_slots, arg_n_wipe_slots + 1, sizeof(int)); + if (!a) + return log_oom(); + + arg_wipe_slots = a; + arg_wipe_slots[arg_n_wipe_slots++] = (int) n; + } + } + break; + } + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + } + + 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(argv[optind], false, &arg_node); + if (r < 0) + return r; + + if (arg_enroll_type == ENROLL_FIDO2) { + + if (arg_unlock_type == UNLOCK_FIDO2 && !(arg_fido2_device && arg_unlock_fido2_device)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "When both enrolling and unlocking with FIDO2 tokens, automatic discovery is unsupported. " + "Please specify device paths for enrolling and unlocking respectively."); + + if (!arg_fido2_device) { + r = fido2_find_device_auto(&arg_fido2_device); + if (r < 0) + return r; + } + } + + if (auto_pcrlock) { + assert(!arg_tpm2_pcrlock); + + r = tpm2_pcrlock_search_file(NULL, NULL, &arg_tpm2_pcrlock); + if (r < 0) { + if (r != -ENOENT) + log_warning_errno(r, "Search for pcrlock.json failed, assuming it does not exist: %m"); + } else + log_info("Automatically using pcrlock policy '%s'.", arg_tpm2_pcrlock); + } + + if (auto_public_key_pcr_mask) { + assert(arg_tpm2_public_key_pcr_mask == 0); + arg_tpm2_public_key_pcr_mask = INDEX_TO_MASK(uint32_t, TPM2_PCR_KERNEL_BOOT); + } + + if (auto_hash_pcr_values && !arg_tpm2_pcrlock) { /* Only lock to PCR 7 by default if no pcrlock policy is around (which is a better replacement) */ + assert(arg_tpm2_n_hash_pcr_values == 0); + + if (!GREEDY_REALLOC_APPEND( + arg_tpm2_hash_pcr_values, + arg_tpm2_n_hash_pcr_values, + &TPM2_PCR_VALUE_MAKE(TPM2_PCR_INDEX_DEFAULT, /* hash= */ 0, /* value= */ {}), + 1)) + return log_oom(); + } + + return 1; +} + +static int check_for_homed(struct crypt_device *cd) { + int r; + + assert_se(cd); + + /* Politely refuse operating on homed volumes. The enrolled tokens for the user record and the LUKS2 + * volume should not get out of sync. */ + + for (int token = 0; token < crypt_token_max(CRYPT_LUKS2); token ++) { + r = cryptsetup_get_token_as_json(cd, token, "systemd-homed", NULL); + if (IN_SET(r, -ENOENT, -EINVAL, -EMEDIUMTYPE)) + continue; + if (r < 0) + return log_error_errno(r, "Failed to read JSON token data off disk: %m"); + + return log_error_errno(SYNTHETIC_ERRNO(EHOSTDOWN), + "LUKS2 volume is managed by systemd-homed, please use homectl to enroll tokens."); + } + + 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 = 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( + 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; + 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"); + + r = check_for_homed(cd); + if (r < 0) + return r; + + 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(); + + switch (arg_unlock_type) { + + case UNLOCK_KEYFILE: + r = load_volume_key_keyfile(cd, vk, &vks); + break; + + case UNLOCK_FIDO2: + r = load_volume_key_fido2(cd, arg_node, arg_unlock_fido2_device, vk, &vks); + break; + + case UNLOCK_PASSWORD: + r = load_volume_key_password(cd, arg_node, vk, &vks); + break; + + default: + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown LUKS unlock method"); + } + + if (r < 0) + return r; + + *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 slot, r; + + log_show_color(true); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + cryptsetup_enable_logging(NULL); + + if (arg_enroll_type < 0) + r = prepare_luks(&cd, NULL, 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, &vks); + if (r < 0) + return r; + + switch (arg_enroll_type) { + + case ENROLL_PASSWORD: + slot = enroll_password(cd, vk, vks); + break; + + case ENROLL_RECOVERY: + slot = enroll_recovery(cd, vk, vks); + break; + + case ENROLL_PKCS11: + slot = enroll_pkcs11(cd, vk, vks, arg_pkcs11_token_uri); + break; + + case ENROLL_FIDO2: + slot = enroll_fido2(cd, vk, vks, arg_fido2_device, arg_fido2_lock_with, arg_fido2_cred_alg); + break; + + case ENROLL_TPM2: + slot = enroll_tpm2(cd, vk, vks, 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_public_key_pcr_mask, arg_tpm2_signature, arg_tpm2_pin, arg_tpm2_pcrlock); + 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."); + } + 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); + if (r < 0) + return r; + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/cryptenroll/cryptenroll.h b/src/cryptenroll/cryptenroll.h new file mode 100644 index 0000000..335d9cc --- /dev/null +++ b/src/cryptenroll/cryptenroll.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <errno.h> + +typedef enum EnrollType { + ENROLL_PASSWORD, + ENROLL_RECOVERY, + ENROLL_PKCS11, + ENROLL_FIDO2, + ENROLL_TPM2, + _ENROLL_TYPE_MAX, + _ENROLL_TYPE_INVALID = -EINVAL, +} EnrollType; + +typedef enum UnlockType { + UNLOCK_PASSWORD, + UNLOCK_KEYFILE, + UNLOCK_FIDO2, + _UNLOCK_TYPE_MAX, + _UNLOCK_TYPE_INVALID = -EINVAL, +} UnlockType; + +typedef enum WipeScope { + WIPE_EXPLICIT, /* only wipe the listed slots */ + WIPE_ALL, /* wipe all slots */ + WIPE_EMPTY_PASSPHRASE, /* wipe slots with empty passphrases plus listed slots */ + _WIPE_SCOPE_MAX, + _WIPE_SCOPE_INVALID = -EINVAL, +} WipeScope; + +const char* enroll_type_to_string(EnrollType t); +EnrollType enroll_type_from_string(const char *s); + +const char* luks2_token_type_to_string(EnrollType t); +EnrollType luks2_token_type_from_string(const char *s); diff --git a/src/cryptenroll/meson.build b/src/cryptenroll/meson.build new file mode 100644 index 0000000..5374d65 --- /dev/null +++ b/src/cryptenroll/meson.build @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +systemd_cryptenroll_sources = files( + 'cryptenroll-list.c', + 'cryptenroll-password.c', + 'cryptenroll-recovery.c', + 'cryptenroll-wipe.c', + 'cryptenroll.c', +) + +if conf.get('HAVE_P11KIT') == 1 and conf.get('HAVE_OPENSSL') == 1 + systemd_cryptenroll_sources += files('cryptenroll-pkcs11.c') +endif + +if conf.get('HAVE_LIBFIDO2') == 1 + systemd_cryptenroll_sources += files('cryptenroll-fido2.c') +endif + +if conf.get('HAVE_TPM2') == 1 + systemd_cryptenroll_sources += files('cryptenroll-tpm2.c') +endif + +executables += [ + executable_template + { + 'name' : 'systemd-cryptenroll', + 'public' : true, + 'conditions' : ['HAVE_LIBCRYPTSETUP'], + 'sources' : systemd_cryptenroll_sources, + 'dependencies' : [ + libcryptsetup, + libdl, + libopenssl, + libp11kit_cflags, + ], + }, +] |