From 73d874bacdaf800b1f7ca0794f38e2fdd453fb6f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 24 Nov 2020 13:55:02 +0100 Subject: [PATCH 01/30] homed: move homectl's recovery key generation/modhex code to src/shared/ This allows us to later reuse the code to generate recovery keys for traditional LUKS volumes, too and share the code. --- src/basic/meson.build | 2 + src/{home/modhex.c => basic/recovery-key.c} | 54 ++++++++++++++++----- src/{home/modhex.h => basic/recovery-key.h} | 6 ++- src/home/homectl-recovery-key.c | 38 +-------------- src/home/homework.c | 2 +- src/home/meson.build | 16 ------ src/home/user-record-util.c | 2 +- src/test/meson.build | 4 ++ src/{home => test}/test-modhex.c | 2 +- 9 files changed, 58 insertions(+), 68 deletions(-) rename src/{home/modhex.c => basic/recovery-key.c} (51%) rename src/{home/modhex.h => basic/recovery-key.h} (68%) rename src/{home => test}/test-modhex.c (99%) diff --git a/src/basic/meson.build b/src/basic/meson.build index a4f97c1fb5..8f51c76dda 100644 --- a/src/basic/meson.build +++ b/src/basic/meson.build @@ -187,6 +187,8 @@ basic_sources = files(''' ratelimit.h raw-clone.h raw-reboot.h + recovery-key.c + recovery-key.h replace-var.c replace-var.h rlimit-util.c diff --git a/src/home/modhex.c b/src/basic/recovery-key.c similarity index 51% rename from src/home/modhex.c rename to src/basic/recovery-key.c index ae5f895722..a3c4500dff 100644 --- a/src/home/modhex.c +++ b/src/basic/recovery-key.c @@ -1,10 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include - -#include "modhex.h" -#include "macro.h" #include "memory-util.h" +#include "random-util.h" +#include "recovery-key.h" const char modhex_alphabet[16] = { 'c', 'b', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'n', 'r', 't', 'u', 'v' @@ -29,24 +27,24 @@ int normalize_recovery_key(const char *password, char **ret) { l = strlen(password); if (!IN_SET(l, - MODHEX_RAW_LENGTH*2, /* syntax without dashes */ - MODHEX_FORMATTED_LENGTH-1)) /* syntax with dashes */ + RECOVERY_KEY_MODHEX_RAW_LENGTH*2, /* syntax without dashes */ + RECOVERY_KEY_MODHEX_FORMATTED_LENGTH-1)) /* syntax with dashes */ return -EINVAL; - mangled = new(char, MODHEX_FORMATTED_LENGTH); + mangled = new(char, RECOVERY_KEY_MODHEX_FORMATTED_LENGTH); if (!mangled) return -ENOMEM; - for (size_t i = 0, j = 0; i < MODHEX_RAW_LENGTH; i++) { + for (size_t i = 0, j = 0; i < RECOVERY_KEY_MODHEX_RAW_LENGTH; i++) { size_t k; int a, b; - if (l == MODHEX_RAW_LENGTH*2) + if (l == RECOVERY_KEY_MODHEX_RAW_LENGTH*2) /* Syntax without dashes */ k = i * 2; else { /* Syntax with dashes */ - assert(l == MODHEX_FORMATTED_LENGTH-1); + assert(l == RECOVERY_KEY_MODHEX_FORMATTED_LENGTH-1); k = i * 2 + i / 4; if (i > 0 && i % 4 == 0 && password[k-1] != '-') @@ -67,8 +65,42 @@ int normalize_recovery_key(const char *password, char **ret) { mangled[j++] = '-'; } - mangled[MODHEX_FORMATTED_LENGTH-1] = 0; + mangled[RECOVERY_KEY_MODHEX_FORMATTED_LENGTH-1] = 0; *ret = TAKE_PTR(mangled); return 0; } + +int make_recovery_key(char **ret) { + _cleanup_(erase_and_freep) char *formatted = NULL; + _cleanup_(erase_and_freep) uint8_t *key = NULL; + int r; + + assert(ret); + + key = new(uint8_t, RECOVERY_KEY_MODHEX_RAW_LENGTH); + if (!key) + return -ENOMEM; + + r = genuine_random_bytes(key, RECOVERY_KEY_MODHEX_RAW_LENGTH, RANDOM_BLOCK); + if (r < 0) + return r; + + /* Let's now format it as 64 modhex chars, and after each 8 chars insert a dash */ + formatted = new(char, RECOVERY_KEY_MODHEX_FORMATTED_LENGTH); + if (!formatted) + return -ENOMEM; + + for (size_t i = 0, j = 0; i < RECOVERY_KEY_MODHEX_RAW_LENGTH; i++) { + formatted[j++] = modhex_alphabet[key[i] >> 4]; + formatted[j++] = modhex_alphabet[key[i] & 0xF]; + + if (i % 4 == 3) + formatted[j++] = '-'; + } + + formatted[RECOVERY_KEY_MODHEX_FORMATTED_LENGTH-1] = 0; + + *ret = TAKE_PTR(formatted); + return 0; +} diff --git a/src/home/modhex.h b/src/basic/recovery-key.h similarity index 68% rename from src/home/modhex.h rename to src/basic/recovery-key.h index 7776ed0ee1..68e8051a93 100644 --- a/src/home/modhex.h +++ b/src/basic/recovery-key.h @@ -2,10 +2,12 @@ #pragma once /* 256 bit keys = 32 bytes */ -#define MODHEX_RAW_LENGTH 32 +#define RECOVERY_KEY_MODHEX_RAW_LENGTH 32 /* Formatted as sequences of 64 modhex characters, with dashes inserted after multiples of 8 chars (incl. trailing NUL) */ -#define MODHEX_FORMATTED_LENGTH (MODHEX_RAW_LENGTH*2/8*9) +#define RECOVERY_KEY_MODHEX_FORMATTED_LENGTH (RECOVERY_KEY_MODHEX_RAW_LENGTH*2/8*9) + +int make_recovery_key(char **ret); extern const char modhex_alphabet[16]; diff --git a/src/home/homectl-recovery-key.c b/src/home/homectl-recovery-key.c index 4a6649d25c..f1a180baca 100644 --- a/src/home/homectl-recovery-key.c +++ b/src/home/homectl-recovery-key.c @@ -5,46 +5,12 @@ #include "libcrypt-util.h" #include "locale-util.h" #include "memory-util.h" -#include "modhex.h" #include "qrcode-util.h" #include "random-util.h" +#include "recovery-key.h" #include "strv.h" #include "terminal-util.h" -static int make_recovery_key(char **ret) { - _cleanup_(erase_and_freep) char *formatted = NULL; - _cleanup_(erase_and_freep) uint8_t *key = NULL; - int r; - - assert(ret); - - key = new(uint8_t, MODHEX_RAW_LENGTH); - if (!key) - return log_oom(); - - r = genuine_random_bytes(key, MODHEX_RAW_LENGTH, RANDOM_BLOCK); - if (r < 0) - return log_error_errno(r, "Failed to gather entropy for recovery key: %m"); - - /* Let's now format it as 64 modhex chars, and after each 8 chars insert a dash */ - formatted = new(char, MODHEX_FORMATTED_LENGTH); - if (!formatted) - return log_oom(); - - for (size_t i = 0, j = 0; i < MODHEX_RAW_LENGTH; i++) { - formatted[j++] = modhex_alphabet[key[i] >> 4]; - formatted[j++] = modhex_alphabet[key[i] & 0xF]; - - if (i % 4 == 3) - formatted[j++] = '-'; - } - - formatted[MODHEX_FORMATTED_LENGTH-1] = 0; - - *ret = TAKE_PTR(formatted); - return 0; -} - static int add_privileged(JsonVariant **v, const char *hashed) { _cleanup_(json_variant_unrefp) JsonVariant *e = NULL, *w = NULL, *l = NULL; int r; @@ -144,7 +110,7 @@ int identity_add_recovery_key(JsonVariant **v) { /* First, let's generate a secret key */ r = make_recovery_key(&password); if (r < 0) - return r; + return log_error_errno(r, "Failed to generate recovery key: %m"); /* Let's UNIX hash it */ r = hash_password(password, &hashed); diff --git a/src/home/homework.c b/src/home/homework.c index b61f650662..35f3206bb0 100644 --- a/src/home/homework.c +++ b/src/home/homework.c @@ -21,9 +21,9 @@ #include "main-func.h" #include "memory-util.h" #include "missing_magic.h" -#include "modhex.h" #include "mount-util.h" #include "path-util.h" +#include "recovery-key.h" #include "rm-rf.h" #include "stat-util.h" #include "strv.h" diff --git a/src/home/meson.build b/src/home/meson.build index 444dc47619..8a879cc5d5 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -19,8 +19,6 @@ systemd_homework_sources = files(''' homework-quota.h homework.c homework.h - modhex.c - modhex.h user-record-util.c user-record-util.h '''.split()) @@ -52,8 +50,6 @@ systemd_homed_sources = files(''' homed-varlink.c homed-varlink.h homed.c - modhex.c - modhex.h user-record-pwquality.c user-record-pwquality.h user-record-sign.c @@ -80,8 +76,6 @@ homectl_sources = files(''' homectl-recovery-key.c homectl-recovery-key.h homectl.c - modhex.c - modhex.h user-record-pwquality.c user-record-pwquality.h user-record-util.c @@ -92,8 +86,6 @@ pam_systemd_home_sym = 'src/home/pam_systemd_home.sym' pam_systemd_home_c = files(''' home-util.c home-util.h - modhex.c - modhex.h pam_systemd_home.c user-record-util.c user-record-util.h @@ -112,11 +104,3 @@ if conf.get('ENABLE_HOMED') == 1 install_dir : pkgsysconfdir) endif endif - -tests += [ - [['src/home/test-modhex.c', - 'src/home/modhex.c', - 'src/home/modhex.h'], - [], - []], -] diff --git a/src/home/user-record-util.c b/src/home/user-record-util.c index 6bcbb56aac..b8f59c55ec 100644 --- a/src/home/user-record-util.c +++ b/src/home/user-record-util.c @@ -7,7 +7,7 @@ #include "id128-util.h" #include "libcrypt-util.h" #include "memory-util.h" -#include "modhex.h" +#include "recovery-key.h" #include "mountpoint-util.h" #include "path-util.h" #include "stat-util.h" diff --git a/src/test/meson.build b/src/test/meson.build index 3574589463..c2345bd6ee 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -212,6 +212,10 @@ tests += [ [], []], + [['src/test/test-modhex.c'], + [], + []], + [['src/test/test-libmount.c'], [], [threads, diff --git a/src/home/test-modhex.c b/src/test/test-modhex.c similarity index 99% rename from src/home/test-modhex.c rename to src/test/test-modhex.c index 1bd9061a7b..836460cec4 100644 --- a/src/home/test-modhex.c +++ b/src/test/test-modhex.c @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ -#include "modhex.h" +#include "recovery-key.h" #include "alloc-util.h" #include "string-util.h" From f2d5df8a302dcd940689310ef8623d9b48bbc68a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 24 Nov 2020 15:07:53 +0100 Subject: [PATCH 02/30] homed: move helper calls for RSA encryption to shared code --- src/home/homectl-pkcs11.c | 42 ++------------------------------------- src/shared/meson.build | 1 + src/shared/openssl-util.c | 41 ++++++++++++++++++++++++++++++++++++++ src/shared/openssl-util.h | 4 ++++ 4 files changed, 48 insertions(+), 40 deletions(-) create mode 100644 src/shared/openssl-util.c diff --git a/src/home/homectl-pkcs11.c b/src/home/homectl-pkcs11.c index 4b7f8336aa..f4cfb94d2c 100644 --- a/src/home/homectl-pkcs11.c +++ b/src/home/homectl-pkcs11.c @@ -93,43 +93,6 @@ static int acquire_pkcs11_certificate( #endif } -static int encrypt_bytes( - EVP_PKEY *pkey, - const void *decrypted_key, - size_t decrypted_key_size, - void **ret_encrypt_key, - size_t *ret_encrypt_key_size) { - - _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = NULL; - _cleanup_free_ void *b = NULL; - size_t l; - - ctx = EVP_PKEY_CTX_new(pkey, NULL); - if (!ctx) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to allocate public key context"); - - if (EVP_PKEY_encrypt_init(ctx) <= 0) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize public key context"); - - if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to configure PKCS#1 padding"); - - if (EVP_PKEY_encrypt(ctx, NULL, &l, decrypted_key, decrypted_key_size) <= 0) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to determine encrypted key size"); - - b = malloc(l); - if (!b) - return log_oom(); - - if (EVP_PKEY_encrypt(ctx, b, &l, decrypted_key, decrypted_key_size) <= 0) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to determine encrypted key size"); - - *ret_encrypt_key = TAKE_PTR(b); - *ret_encrypt_key_size = l; - - return 0; -} - static int add_pkcs11_encrypted_key( JsonVariant **v, const char *uri, @@ -267,9 +230,8 @@ int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) { size_t decrypted_key_size, encrypted_key_size; _cleanup_(X509_freep) X509 *cert = NULL; EVP_PKEY *pkey; + int bits, r; RSA *rsa; - int bits; - int r; assert(v); @@ -308,7 +270,7 @@ int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) { if (r < 0) return log_error_errno(r, "Failed to generate random key: %m"); - r = encrypt_bytes(pkey, decrypted_key, decrypted_key_size, &encrypted_key, &encrypted_key_size); + 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"); diff --git a/src/shared/meson.build b/src/shared/meson.build index cc844eb09e..ebe98df24a 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -183,6 +183,7 @@ shared_sources = files(''' nsflags.h numa-util.c numa-util.h + openssl-util.c openssl-util.h os-util.c os-util.h diff --git a/src/shared/openssl-util.c b/src/shared/openssl-util.c new file mode 100644 index 0000000000..1e2aaa2130 --- /dev/null +++ b/src/shared/openssl-util.c @@ -0,0 +1,41 @@ +#include "openssl-util.h" +#include "alloc-util.h" + +#if HAVE_OPENSSL +int rsa_encrypt_bytes( + EVP_PKEY *pkey, + const void *decrypted_key, + size_t decrypted_key_size, + void **ret_encrypt_key, + size_t *ret_encrypt_key_size) { + + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = NULL; + _cleanup_free_ void *b = NULL; + size_t l; + + ctx = EVP_PKEY_CTX_new(pkey, NULL); + if (!ctx) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Failed to allocate public key context"); + + if (EVP_PKEY_encrypt_init(ctx) <= 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize public key context"); + + if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Failed to configure PKCS#1 padding"); + + if (EVP_PKEY_encrypt(ctx, NULL, &l, decrypted_key, decrypted_key_size) <= 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Failed to determine encrypted key size"); + + b = malloc(l); + if (!b) + return -ENOMEM; + + if (EVP_PKEY_encrypt(ctx, b, &l, decrypted_key, decrypted_key_size) <= 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Failed to determine encrypted key size"); + + *ret_encrypt_key = TAKE_PTR(b); + *ret_encrypt_key_size = l; + + return 0; +} +#endif diff --git a/src/shared/openssl-util.h b/src/shared/openssl-util.h index 1b49834dd8..a669b0926f 100644 --- a/src/shared/openssl-util.h +++ b/src/shared/openssl-util.h @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "macro.h" + #if HAVE_OPENSSL # include @@ -9,4 +11,6 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(X509_NAME*, X509_NAME_free); DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_PKEY_CTX*, EVP_PKEY_CTX_free); DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_CIPHER_CTX*, EVP_CIPHER_CTX_free); +int rsa_encrypt_bytes(EVP_PKEY *pkey, const void *decrypted_key, size_t decrypted_key_size, void **ret_encrypt_key, size_t *ret_encrypt_key_size); + #endif From 2289a78473282902db1108168df6414ae7d91b2f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 24 Nov 2020 15:08:20 +0100 Subject: [PATCH 03/30] homed: move pkcs11 LUKS glue into shared code That way we can lter reuse it from cryptsetup/cryptenroll too. --- src/home/homectl-pkcs11.c | 84 +------------------------------------ src/shared/pkcs11-util.c | 88 +++++++++++++++++++++++++++++++++++++++ src/shared/pkcs11-util.h | 5 +++ 3 files changed, 94 insertions(+), 83 deletions(-) diff --git a/src/home/homectl-pkcs11.c b/src/home/homectl-pkcs11.c index f4cfb94d2c..7cabd723a7 100644 --- a/src/home/homectl-pkcs11.c +++ b/src/home/homectl-pkcs11.c @@ -11,88 +11,6 @@ #include "random-util.h" #include "strv.h" -struct pkcs11_callback_data { - char *pin_used; - X509 *cert; -}; - -#if HAVE_P11KIT -static void pkcs11_callback_data_release(struct pkcs11_callback_data *data) { - erase_and_free(data->pin_used); - X509_free(data->cert); -} - -static int pkcs11_callback( - CK_FUNCTION_LIST *m, - CK_SESSION_HANDLE session, - CK_SLOT_ID slot_id, - const CK_SLOT_INFO *slot_info, - const CK_TOKEN_INFO *token_info, - P11KitUri *uri, - void *userdata) { - - _cleanup_(erase_and_freep) char *pin_used = NULL; - struct pkcs11_callback_data *data = userdata; - CK_OBJECT_HANDLE object; - int r; - - assert(m); - assert(slot_info); - assert(token_info); - assert(uri); - assert(data); - - /* Called for every token matching our URI */ - - r = pkcs11_token_login(m, session, slot_id, token_info, "home directory operation", "user-home", "pkcs11-pin", UINT64_MAX, &pin_used); - if (r < 0) - return r; - - r = pkcs11_token_find_x509_certificate(m, session, uri, &object); - if (r < 0) - return r; - - r = pkcs11_token_read_x509_certificate(m, session, object, &data->cert); - if (r < 0) - return r; - - /* Let's read some random data off the token and write it to the kernel pool before we generate our - * random key from it. This way we can claim the quality of the RNG is at least as good as the - * kernel's and the token's pool */ - (void) pkcs11_token_acquire_rng(m, session); - - data->pin_used = TAKE_PTR(pin_used); - return 1; -} -#endif - -static int acquire_pkcs11_certificate( - const char *uri, - X509 **ret_cert, - char **ret_pin_used) { - -#if HAVE_P11KIT - _cleanup_(pkcs11_callback_data_release) struct pkcs11_callback_data data = {}; - int r; - - r = pkcs11_find_token(uri, pkcs11_callback, &data); - if (r == -EAGAIN) /* pkcs11_find_token() doesn't log about this error, but all others */ - return log_error_errno(SYNTHETIC_ERRNO(ENXIO), - "Specified PKCS#11 token with URI '%s' not found.", - uri); - if (r < 0) - return r; - - *ret_cert = TAKE_PTR(data.cert); - *ret_pin_used = TAKE_PTR(data.pin_used); - - return 0; -#else - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "PKCS#11 tokens not supported on this build."); -#endif -} - static int add_pkcs11_encrypted_key( JsonVariant **v, const char *uri, @@ -235,7 +153,7 @@ int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) { assert(v); - r = acquire_pkcs11_certificate(uri, &cert, &pin); + r = pkcs11_acquire_certificate(uri, "home directory operation", "user-home", &cert, &pin); if (r < 0) return r; diff --git a/src/shared/pkcs11-util.c b/src/shared/pkcs11-util.c index 078a86ec32..fc59f98701 100644 --- a/src/shared/pkcs11-util.c +++ b/src/shared/pkcs11-util.c @@ -924,4 +924,92 @@ int pkcs11_find_token( return -EAGAIN; } +#if HAVE_OPENSSL +struct pkcs11_acquire_certificate_callback_data { + char *pin_used; + X509 *cert; + const char *askpw_friendly_name, *askpw_icon_name; +}; + +static void pkcs11_acquire_certificate_callback_data_release(struct pkcs11_acquire_certificate_callback_data *data) { + erase_and_free(data->pin_used); + X509_free(data->cert); +} + +static int pkcs11_acquire_certificate_callback( + CK_FUNCTION_LIST *m, + CK_SESSION_HANDLE session, + CK_SLOT_ID slot_id, + const CK_SLOT_INFO *slot_info, + const CK_TOKEN_INFO *token_info, + P11KitUri *uri, + void *userdata) { + + _cleanup_(erase_and_freep) char *pin_used = NULL; + struct pkcs11_acquire_certificate_callback_data *data = userdata; + CK_OBJECT_HANDLE object; + int r; + + assert(m); + assert(slot_info); + assert(token_info); + assert(uri); + assert(data); + + /* Called for every token matching our URI */ + + r = pkcs11_token_login(m, session, slot_id, token_info, data->askpw_friendly_name, data->askpw_icon_name, "pkcs11-pin", UINT64_MAX, &pin_used); + if (r < 0) + return r; + + r = pkcs11_token_find_x509_certificate(m, session, uri, &object); + if (r < 0) + return r; + + r = pkcs11_token_read_x509_certificate(m, session, object, &data->cert); + if (r < 0) + return r; + + /* Let's read some random data off the token and write it to the kernel pool before we generate our + * random key from it. This way we can claim the quality of the RNG is at least as good as the + * kernel's and the token's pool */ + (void) pkcs11_token_acquire_rng(m, session); + + data->pin_used = TAKE_PTR(pin_used); + return 1; +} + +int pkcs11_acquire_certificate( + const char *uri, + const char *askpw_friendly_name, + const char *askpw_icon_name, + X509 **ret_cert, + char **ret_pin_used) { + + _cleanup_(pkcs11_acquire_certificate_callback_data_release) struct pkcs11_acquire_certificate_callback_data data = { + .askpw_friendly_name = askpw_friendly_name, + .askpw_icon_name = askpw_icon_name, + }; + int r; + + assert(uri); + assert(ret_cert); + + r = pkcs11_find_token(uri, pkcs11_acquire_certificate_callback, &data); + if (r == -EAGAIN) /* pkcs11_find_token() doesn't log about this error, but all others */ + return log_error_errno(SYNTHETIC_ERRNO(ENXIO), + "Specified PKCS#11 token with URI '%s' not found.", + uri); + if (r < 0) + return r; + + *ret_cert = TAKE_PTR(data.cert); + + if (ret_pin_used) + *ret_pin_used = TAKE_PTR(data.pin_used); + + return 0; +} + +#endif #endif diff --git a/src/shared/pkcs11-util.h b/src/shared/pkcs11-util.h index f14607de84..990368a41b 100644 --- a/src/shared/pkcs11-util.h +++ b/src/shared/pkcs11-util.h @@ -44,4 +44,9 @@ int pkcs11_token_acquire_rng(CK_FUNCTION_LIST *m, CK_SESSION_HANDLE session); typedef int (*pkcs11_find_token_callback_t)(CK_FUNCTION_LIST *m, CK_SESSION_HANDLE session, CK_SLOT_ID slotid, const CK_SLOT_INFO *slot_info, const CK_TOKEN_INFO *token_info, P11KitUri *uri, void *userdata); int pkcs11_find_token(const char *pkcs11_uri, pkcs11_find_token_callback_t callback, void *userdata); + +#if HAVE_OPENSSL +int pkcs11_acquire_certificate(const char *uri, const char *askpw_friendly_name, const char *askpw_icon_name, X509 **ret_cert, char **ret_pin_used); +#endif + #endif From d041e4fc4a69df0b8992c07c9c42b0f369fdb9d8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 24 Nov 2020 15:29:03 +0100 Subject: [PATCH 04/30] homed: split out code that determines suitable LUKS passphrase size from RSA key We can use this in cryptenroll later on, hence let's make this generic. --- src/home/homectl-pkcs11.c | 22 ++++------------------ src/shared/openssl-util.c | 35 +++++++++++++++++++++++++++++++++++ src/shared/openssl-util.h | 2 ++ 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/home/homectl-pkcs11.c b/src/home/homectl-pkcs11.c index 7cabd723a7..95cf932936 100644 --- a/src/home/homectl-pkcs11.c +++ b/src/home/homectl-pkcs11.c @@ -148,8 +148,7 @@ int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) { size_t decrypted_key_size, encrypted_key_size; _cleanup_(X509_freep) X509 *cert = NULL; EVP_PKEY *pkey; - int bits, r; - RSA *rsa; + int r; assert(v); @@ -161,22 +160,9 @@ int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) { if (!pkey) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to extract public key from X.509 certificate."); - if (EVP_PKEY_base_id(pkey) != EVP_PKEY_RSA) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "X.509 certificate does not refer to RSA key."); - - rsa = EVP_PKEY_get0_RSA(pkey); - if (!rsa) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire RSA public key from X.509 certificate."); - - bits = RSA_bits(rsa); - log_debug("Bits in RSA key: %i", bits); - - /* We use PKCS#1 padding for the RSA cleartext, hence let's leave some extra space for it, hence only - * generate a random key half the size of the RSA length */ - decrypted_key_size = bits / 8 / 2; - - if (decrypted_key_size < 1) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Uh, RSA key size too short?"); + r = rsa_pkey_to_suitable_key_size(pkey, &decrypted_key_size); + if (r < 0) + return log_error_errno(r, "Failed to extract RSA key size from X509 certificate."); log_debug("Generating %zu bytes random key.", decrypted_key_size); diff --git a/src/shared/openssl-util.c b/src/shared/openssl-util.c index 1e2aaa2130..895539f436 100644 --- a/src/shared/openssl-util.c +++ b/src/shared/openssl-util.c @@ -38,4 +38,39 @@ int rsa_encrypt_bytes( return 0; } + +int rsa_pkey_to_suitable_key_size( + EVP_PKEY *pkey, + size_t *ret_suitable_key_size) { + + size_t suitable_key_size; + RSA *rsa; + int bits; + + assert_se(pkey); + assert_se(ret_suitable_key_size); + + /* Analyzes the specified public key and that it is RSA. If so, will return a suitable size for a + * disk encryption key to encrypt with RSA for use in PKCS#11 security token schemes. */ + + if (EVP_PKEY_base_id(pkey) != EVP_PKEY_RSA) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "X.509 certificate does not refer to RSA key."); + + rsa = EVP_PKEY_get0_RSA(pkey); + if (!rsa) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire RSA public key from X.509 certificate."); + + bits = RSA_bits(rsa); + log_debug("Bits in RSA key: %i", bits); + + /* We use PKCS#1 padding for the RSA cleartext, hence let's leave some extra space for it, hence only + * generate a random key half the size of the RSA length */ + suitable_key_size = bits / 8 / 2; + + if (suitable_key_size < 1) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Uh, RSA key size too short?"); + + *ret_suitable_key_size = suitable_key_size; + return 0; +} #endif diff --git a/src/shared/openssl-util.h b/src/shared/openssl-util.h index a669b0926f..b753a690ba 100644 --- a/src/shared/openssl-util.h +++ b/src/shared/openssl-util.h @@ -13,4 +13,6 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_CIPHER_CTX*, EVP_CIPHER_CTX_free); int rsa_encrypt_bytes(EVP_PKEY *pkey, const void *decrypted_key, size_t decrypted_key_size, void **ret_encrypt_key, size_t *ret_encrypt_key_size); +int rsa_pkey_to_suitable_key_size(EVP_PKEY *pkey, size_t *ret_suitable_key_size); + #endif From f240cbb645338101cfe46410b056928b0febd8ed Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 25 Nov 2020 10:28:11 +0100 Subject: [PATCH 05/30] homed: move code to list and resolve "auto" pkcs#11 URL into common code That way we can reuse it from systemd-cryptenroll. --- src/home/homectl-pkcs11.c | 140 -------------------------------------- src/home/homectl.c | 4 +- src/shared/pkcs11-util.c | 138 +++++++++++++++++++++++++++++++++++++ src/shared/pkcs11-util.h | 3 + 4 files changed, 143 insertions(+), 142 deletions(-) diff --git a/src/home/homectl-pkcs11.c b/src/home/homectl-pkcs11.c index 95cf932936..c6aaa2e6d6 100644 --- a/src/home/homectl-pkcs11.c +++ b/src/home/homectl-pkcs11.c @@ -201,143 +201,3 @@ int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) { return 0; } - -#if HAVE_P11KIT -static int list_callback( - CK_FUNCTION_LIST *m, - CK_SESSION_HANDLE session, - CK_SLOT_ID slot_id, - const CK_SLOT_INFO *slot_info, - const CK_TOKEN_INFO *token_info, - P11KitUri *uri, - void *userdata) { - - _cleanup_free_ char *token_uri_string = NULL, *token_label = NULL, *token_manufacturer_id = NULL, *token_model = NULL; - _cleanup_(p11_kit_uri_freep) P11KitUri *token_uri = NULL; - Table *t = userdata; - int uri_result, r; - - assert(slot_info); - assert(token_info); - - /* We only care about hardware devices here with a token inserted. Let's filter everything else - * out. (Note that the user can explicitly specify non-hardware tokens if they like, but during - * enumeration we'll filter those, since software tokens are typically the system certificate store - * and such, and it's typically not what people want to bind their home directories to.) */ - if (!FLAGS_SET(token_info->flags, CKF_HW_SLOT|CKF_TOKEN_PRESENT)) - return -EAGAIN; - - token_label = pkcs11_token_label(token_info); - if (!token_label) - return log_oom(); - - token_manufacturer_id = pkcs11_token_manufacturer_id(token_info); - if (!token_manufacturer_id) - return log_oom(); - - token_model = pkcs11_token_model(token_info); - if (!token_model) - return log_oom(); - - token_uri = uri_from_token_info(token_info); - if (!token_uri) - return log_oom(); - - uri_result = p11_kit_uri_format(token_uri, P11_KIT_URI_FOR_ANY, &token_uri_string); - if (uri_result != P11_KIT_URI_OK) - return log_warning_errno(SYNTHETIC_ERRNO(EAGAIN), "Failed to format slot URI: %s", p11_kit_uri_message(uri_result)); - - r = table_add_many( - t, - TABLE_STRING, token_uri_string, - TABLE_STRING, token_label, - TABLE_STRING, token_manufacturer_id, - TABLE_STRING, token_model); - if (r < 0) - return table_log_add_error(r); - - return -EAGAIN; /* keep scanning */ -} -#endif - -int list_pkcs11_tokens(void) { -#if HAVE_P11KIT - _cleanup_(table_unrefp) Table *t = NULL; - int r; - - t = table_new("uri", "label", "manufacturer", "model"); - if (!t) - return log_oom(); - - r = pkcs11_find_token(NULL, list_callback, t); - if (r < 0 && r != -EAGAIN) - return r; - - if (table_get_rows(t) <= 1) { - log_info("No suitable PKCS#11 tokens found."); - return 0; - } - - r = table_print(t, stdout); - if (r < 0) - return log_error_errno(r, "Failed to show device table: %m"); - - return 0; -#else - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "PKCS#11 tokens not supported on this build."); -#endif -} - -#if HAVE_P11KIT -static int auto_callback( - CK_FUNCTION_LIST *m, - CK_SESSION_HANDLE session, - CK_SLOT_ID slot_id, - const CK_SLOT_INFO *slot_info, - const CK_TOKEN_INFO *token_info, - P11KitUri *uri, - void *userdata) { - - _cleanup_(p11_kit_uri_freep) P11KitUri *token_uri = NULL; - char **t = userdata; - int uri_result; - - assert(slot_info); - assert(token_info); - - if (!FLAGS_SET(token_info->flags, CKF_HW_SLOT|CKF_TOKEN_PRESENT)) - return -EAGAIN; - - if (*t) - return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), - "More than one suitable PKCS#11 token found."); - - token_uri = uri_from_token_info(token_info); - if (!token_uri) - return log_oom(); - - uri_result = p11_kit_uri_format(token_uri, P11_KIT_URI_FOR_ANY, t); - if (uri_result != P11_KIT_URI_OK) - return log_warning_errno(SYNTHETIC_ERRNO(EAGAIN), "Failed to format slot URI: %s", p11_kit_uri_message(uri_result)); - - return 0; -} -#endif - -int find_pkcs11_token_auto(char **ret) { -#if HAVE_P11KIT - int r; - - r = pkcs11_find_token(NULL, auto_callback, ret); - if (r == -EAGAIN) - return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "No suitable PKCS#11 tokens found."); - if (r < 0) - return r; - - return 0; -#else - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "PKCS#11 tokens not supported on this build."); -#endif -} diff --git a/src/home/homectl.c b/src/home/homectl.c index 7cfda7ed2f..e10cfaf2a4 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -3146,7 +3146,7 @@ static int parse_argv(int argc, char *argv[]) { const char *p; if (streq(optarg, "list")) - return list_pkcs11_tokens(); + return pkcs11_list_tokens(); /* If --pkcs11-token-uri= is specified we always drop everything old */ FOREACH_STRING(p, "pkcs11TokenUri", "pkcs11EncryptedKey") { @@ -3163,7 +3163,7 @@ static int parse_argv(int argc, char *argv[]) { if (streq(optarg, "auto")) { _cleanup_free_ char *found = NULL; - r = find_pkcs11_token_auto(&found); + r = pkcs11_find_token_auto(&found); if (r < 0) return r; r = strv_consume(&arg_pkcs11_token_uri, TAKE_PTR(found)); diff --git a/src/shared/pkcs11-util.c b/src/shared/pkcs11-util.c index fc59f98701..27c209cdf8 100644 --- a/src/shared/pkcs11-util.c +++ b/src/shared/pkcs11-util.c @@ -5,6 +5,7 @@ #include "ask-password-api.h" #include "escape.h" #include "fd-util.h" +#include "format-table.h" #include "io-util.h" #include "memory-util.h" #if HAVE_OPENSSL @@ -1010,6 +1011,143 @@ int pkcs11_acquire_certificate( return 0; } +#endif +static int list_callback( + CK_FUNCTION_LIST *m, + CK_SESSION_HANDLE session, + CK_SLOT_ID slot_id, + const CK_SLOT_INFO *slot_info, + const CK_TOKEN_INFO *token_info, + P11KitUri *uri, + void *userdata) { + + _cleanup_free_ char *token_uri_string = NULL, *token_label = NULL, *token_manufacturer_id = NULL, *token_model = NULL; + _cleanup_(p11_kit_uri_freep) P11KitUri *token_uri = NULL; + Table *t = userdata; + int uri_result, r; + + assert(slot_info); + assert(token_info); + + /* We only care about hardware devices here with a token inserted. Let's filter everything else + * out. (Note that the user can explicitly specify non-hardware tokens if they like, but during + * enumeration we'll filter those, since software tokens are typically the system certificate store + * and such, and it's typically not what people want to bind their home directories to.) */ + if (!FLAGS_SET(token_info->flags, CKF_HW_SLOT|CKF_TOKEN_PRESENT)) + return -EAGAIN; + + token_label = pkcs11_token_label(token_info); + if (!token_label) + return log_oom(); + + token_manufacturer_id = pkcs11_token_manufacturer_id(token_info); + if (!token_manufacturer_id) + return log_oom(); + + token_model = pkcs11_token_model(token_info); + if (!token_model) + return log_oom(); + + token_uri = uri_from_token_info(token_info); + if (!token_uri) + return log_oom(); + + uri_result = p11_kit_uri_format(token_uri, P11_KIT_URI_FOR_ANY, &token_uri_string); + if (uri_result != P11_KIT_URI_OK) + return log_warning_errno(SYNTHETIC_ERRNO(EAGAIN), "Failed to format slot URI: %s", p11_kit_uri_message(uri_result)); + + r = table_add_many( + t, + TABLE_STRING, token_uri_string, + TABLE_STRING, token_label, + TABLE_STRING, token_manufacturer_id, + TABLE_STRING, token_model); + if (r < 0) + return table_log_add_error(r); + + return -EAGAIN; /* keep scanning */ +} #endif + +int pkcs11_list_tokens(void) { +#if HAVE_P11KIT + _cleanup_(table_unrefp) Table *t = NULL; + int r; + + t = table_new("uri", "label", "manufacturer", "model"); + if (!t) + return log_oom(); + + r = pkcs11_find_token(NULL, list_callback, t); + if (r < 0 && r != -EAGAIN) + return r; + + if (table_get_rows(t) <= 1) { + log_info("No suitable PKCS#11 tokens found."); + return 0; + } + + r = table_print(t, stdout); + if (r < 0) + return log_error_errno(r, "Failed to show device table: %m"); + + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "PKCS#11 tokens not supported on this build."); #endif +} + +#if HAVE_P11KIT +static int auto_callback( + CK_FUNCTION_LIST *m, + CK_SESSION_HANDLE session, + CK_SLOT_ID slot_id, + const CK_SLOT_INFO *slot_info, + const CK_TOKEN_INFO *token_info, + P11KitUri *uri, + void *userdata) { + + _cleanup_(p11_kit_uri_freep) P11KitUri *token_uri = NULL; + char **t = userdata; + int uri_result; + + assert(slot_info); + assert(token_info); + + if (!FLAGS_SET(token_info->flags, CKF_HW_SLOT|CKF_TOKEN_PRESENT)) + return -EAGAIN; + + if (*t) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), + "More than one suitable PKCS#11 token found."); + + token_uri = uri_from_token_info(token_info); + if (!token_uri) + return log_oom(); + + uri_result = p11_kit_uri_format(token_uri, P11_KIT_URI_FOR_ANY, t); + if (uri_result != P11_KIT_URI_OK) + return log_warning_errno(SYNTHETIC_ERRNO(EAGAIN), "Failed to format slot URI: %s", p11_kit_uri_message(uri_result)); + + return 0; +} +#endif + +int pkcs11_find_token_auto(char **ret) { +#if HAVE_P11KIT + int r; + + r = pkcs11_find_token(NULL, auto_callback, ret); + if (r == -EAGAIN) + return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "No suitable PKCS#11 tokens found."); + if (r < 0) + return r; + + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "PKCS#11 tokens not supported on this build."); +#endif +} diff --git a/src/shared/pkcs11-util.h b/src/shared/pkcs11-util.h index 990368a41b..3fa2505deb 100644 --- a/src/shared/pkcs11-util.h +++ b/src/shared/pkcs11-util.h @@ -50,3 +50,6 @@ int pkcs11_acquire_certificate(const char *uri, const char *askpw_friendly_name, #endif #endif + +int pkcs11_list_tokens(void); +int pkcs11_find_token_auto(char **ret); From 4098bc134ebdc5120bda6c8dd946865f7419e07d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 8 Dec 2020 13:18:28 +0100 Subject: [PATCH 06/30] cryptsetup-util: add helper call for extracting/parsing token JSON --- src/shared/cryptsetup-util.c | 103 +++++++++++++++++++++++++++++++++++ src/shared/cryptsetup-util.h | 7 +++ 2 files changed, 110 insertions(+) diff --git a/src/shared/cryptsetup-util.c b/src/shared/cryptsetup-util.c index b02d95ac55..850c79ed12 100644 --- a/src/shared/cryptsetup-util.c +++ b/src/shared/cryptsetup-util.c @@ -5,6 +5,7 @@ #include "cryptsetup-util.h" #include "dlfcn-util.h" #include "log.h" +#include "parse-util.h" static void *cryptsetup_dl = NULL; @@ -26,6 +27,8 @@ int (*sym_crypt_resize)(struct crypt_device *cd, const char *name, uint64_t new_ int (*sym_crypt_set_data_device)(struct crypt_device *cd, const char *device); void (*sym_crypt_set_debug_level)(int level); void (*sym_crypt_set_log_callback)(struct crypt_device *cd, void (*log)(int level, const char *msg, void *usrptr), void *usrptr); +int (*sym_crypt_token_json_get)(struct crypt_device *cd, int token, const char **json) = NULL; +int (*sym_crypt_token_json_set)(struct crypt_device *cd, int token, const char *json) = NULL; int (*sym_crypt_volume_key_get)(struct crypt_device *cd, int keyslot, char *volume_key, size_t *volume_key_size, const char *passphrase, size_t passphrase_size); int dlopen_cryptsetup(void) { @@ -61,6 +64,8 @@ int dlopen_cryptsetup(void) { DLSYM_ARG(crypt_set_data_device), DLSYM_ARG(crypt_set_debug_level), DLSYM_ARG(crypt_set_log_callback), + DLSYM_ARG(crypt_token_json_get), + DLSYM_ARG(crypt_token_json_set), DLSYM_ARG(crypt_volume_key_get), NULL); if (r < 0) @@ -108,4 +113,102 @@ void cryptsetup_enable_logging(struct crypt_device *cd) { sym_crypt_set_debug_level(DEBUG_LOGGING ? CRYPT_DEBUG_ALL : CRYPT_DEBUG_NONE); } +int cryptsetup_get_token_as_json( + struct crypt_device *cd, + int idx, + const char *verify_type, + JsonVariant **ret) { + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + const char *text; + int r; + + assert(cd); + + /* Extracts and parses the LUKS2 JSON token data from a LUKS2 device. Optionally verifies the type of + * the token. Returns: + * + * -EINVAL → token index out of range or "type" field missing + * -ENOENT → token doesn't exist + * -EMEDIUMTYPE → "verify_type" specified and doesn't match token's type + */ + + r = dlopen_cryptsetup(); + if (r < 0) + return r; + + r = sym_crypt_token_json_get(cd, idx, &text); + if (r < 0) + return r; + + r = json_parse(text, 0, &v, NULL, NULL); + if (r < 0) + return r; + + if (verify_type) { + JsonVariant *w; + + w = json_variant_by_key(v, "type"); + if (!w) + return -EINVAL; + + if (!streq_ptr(json_variant_string(w), verify_type)) + return -EMEDIUMTYPE; + } + + if (ret) + *ret = TAKE_PTR(v); + + return 0; +} + +int cryptsetup_get_keyslot_from_token(JsonVariant *v) { + int keyslot, r; + JsonVariant *w; + + /* Parses the "keyslots" field of a LUKS2 token object. The field can be an array, but here we assume + * that it contains a single element only, since that's the only way we ever generate it + * ourselves. */ + + w = json_variant_by_key(v, "keyslots"); + if (!w) + return -ENOENT; + if (!json_variant_is_array(w) || json_variant_elements(w) != 1) + return -EMEDIUMTYPE; + + w = json_variant_by_index(w, 0); + if (!w) + return -ENOENT; + if (!json_variant_is_string(w)) + return -EMEDIUMTYPE; + + r = safe_atoi(json_variant_string(w), &keyslot); + if (r < 0) + return r; + if (keyslot < 0) + return -EINVAL; + + return keyslot; +} + +int cryptsetup_add_token_json(struct crypt_device *cd, JsonVariant *v) { + _cleanup_free_ char *text = NULL; + int r; + + r = dlopen_cryptsetup(); + if (r < 0) + return r; + + r = json_variant_format(v, 0, &text); + if (r < 0) + return log_debug_errno(r, "Failed to format token data for LUKS: %m"); + + log_debug("Adding token text <%s>", text); + + r = sym_crypt_token_json_set(cd, CRYPT_ANY_TOKEN, text); + if (r < 0) + return log_debug_errno(r, "Failed to write token data to LUKS: %m"); + + return 0; +} #endif diff --git a/src/shared/cryptsetup-util.h b/src/shared/cryptsetup-util.h index b96ffc74e3..c6c56d6801 100644 --- a/src/shared/cryptsetup-util.h +++ b/src/shared/cryptsetup-util.h @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "json.h" #include "macro.h" #if HAVE_LIBCRYPTSETUP @@ -24,6 +25,8 @@ extern int (*sym_crypt_resize)(struct crypt_device *cd, const char *name, uint64 extern int (*sym_crypt_set_data_device)(struct crypt_device *cd, const char *device); extern void (*sym_crypt_set_debug_level)(int level); extern void (*sym_crypt_set_log_callback)(struct crypt_device *cd, void (*log)(int level, const char *msg, void *usrptr), void *usrptr); +extern int (*sym_crypt_token_json_get)(struct crypt_device *cd, int token, const char **json); +extern int (*sym_crypt_token_json_set)(struct crypt_device *cd, int token, const char *json); extern int (*sym_crypt_volume_key_get)(struct crypt_device *cd, int keyslot, char *volume_key, size_t *volume_key_size, const char *passphrase, size_t passphrase_size); int dlopen_cryptsetup(void); @@ -33,4 +36,8 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(struct crypt_device *, sym_crypt_free); void cryptsetup_enable_logging(struct crypt_device *cd); +int cryptsetup_get_token_as_json(struct crypt_device *cd, int idx, const char *verify_type, JsonVariant **ret); +int cryptsetup_get_keyslot_from_token(JsonVariant *v); +int cryptsetup_add_token_json(struct crypt_device *cd, JsonVariant *v); + #endif From 4760384d53cd1efacc462ec133d497084141f922 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 8 Dec 2020 14:46:31 +0100 Subject: [PATCH 07/30] cryptsetup-util: add helper for setting minimal PBKDF --- src/shared/cryptsetup-util.c | 26 ++++++++++++++++++++++++++ src/shared/cryptsetup-util.h | 3 +++ 2 files changed, 29 insertions(+) diff --git a/src/shared/cryptsetup-util.c b/src/shared/cryptsetup-util.c index 850c79ed12..a793b9ac5b 100644 --- a/src/shared/cryptsetup-util.c +++ b/src/shared/cryptsetup-util.c @@ -27,6 +27,7 @@ int (*sym_crypt_resize)(struct crypt_device *cd, const char *name, uint64_t new_ int (*sym_crypt_set_data_device)(struct crypt_device *cd, const char *device); void (*sym_crypt_set_debug_level)(int level); void (*sym_crypt_set_log_callback)(struct crypt_device *cd, void (*log)(int level, const char *msg, void *usrptr), void *usrptr); +int (*sym_crypt_set_pbkdf_type)(struct crypt_device *cd, const struct crypt_pbkdf_type *pbkdf) = NULL; int (*sym_crypt_token_json_get)(struct crypt_device *cd, int token, const char **json) = NULL; int (*sym_crypt_token_json_set)(struct crypt_device *cd, int token, const char *json) = NULL; int (*sym_crypt_volume_key_get)(struct crypt_device *cd, int keyslot, char *volume_key, size_t *volume_key_size, const char *passphrase, size_t passphrase_size); @@ -64,6 +65,7 @@ int dlopen_cryptsetup(void) { DLSYM_ARG(crypt_set_data_device), DLSYM_ARG(crypt_set_debug_level), DLSYM_ARG(crypt_set_log_callback), + DLSYM_ARG(crypt_set_pbkdf_type), DLSYM_ARG(crypt_token_json_get), DLSYM_ARG(crypt_token_json_set), DLSYM_ARG(crypt_volume_key_get), @@ -113,6 +115,30 @@ void cryptsetup_enable_logging(struct crypt_device *cd) { sym_crypt_set_debug_level(DEBUG_LOGGING ? CRYPT_DEBUG_ALL : CRYPT_DEBUG_NONE); } +int cryptsetup_set_minimal_pbkdf(struct crypt_device *cd) { + + static const struct crypt_pbkdf_type minimal_pbkdf = { + .hash = "sha512", + .type = CRYPT_KDF_PBKDF2, + .iterations = 1, + .time_ms = 1, + }; + + int r; + + /* Sets a minimal PKBDF in case we already have a high entropy key. */ + + r = dlopen_cryptsetup(); + if (r < 0) + return r; + + r = sym_crypt_set_pbkdf_type(cd, &minimal_pbkdf); + if (r < 0) + return r; + + return 0; +} + int cryptsetup_get_token_as_json( struct crypt_device *cd, int idx, diff --git a/src/shared/cryptsetup-util.h b/src/shared/cryptsetup-util.h index c6c56d6801..26f5dd3c89 100644 --- a/src/shared/cryptsetup-util.h +++ b/src/shared/cryptsetup-util.h @@ -25,6 +25,7 @@ extern int (*sym_crypt_resize)(struct crypt_device *cd, const char *name, uint64 extern int (*sym_crypt_set_data_device)(struct crypt_device *cd, const char *device); extern void (*sym_crypt_set_debug_level)(int level); extern void (*sym_crypt_set_log_callback)(struct crypt_device *cd, void (*log)(int level, const char *msg, void *usrptr), void *usrptr); +extern int (*sym_crypt_set_pbkdf_type)(struct crypt_device *cd, const struct crypt_pbkdf_type *pbkdf); extern int (*sym_crypt_token_json_get)(struct crypt_device *cd, int token, const char **json); extern int (*sym_crypt_token_json_set)(struct crypt_device *cd, int token, const char *json); extern int (*sym_crypt_volume_key_get)(struct crypt_device *cd, int keyslot, char *volume_key, size_t *volume_key_size, const char *passphrase, size_t passphrase_size); @@ -36,6 +37,8 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(struct crypt_device *, sym_crypt_free); void cryptsetup_enable_logging(struct crypt_device *cd); +int cryptsetup_set_minimal_pbkdf(struct crypt_device *cd); + int cryptsetup_get_token_as_json(struct crypt_device *cd, int idx, const char *verify_type, JsonVariant **ret); int cryptsetup_get_keyslot_from_token(JsonVariant *v); int cryptsetup_add_token_json(struct crypt_device *cd, JsonVariant *v); From 8414cd48e98b34731b9e5a99ede7fec6d1107dee Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 25 Nov 2020 11:51:39 +0100 Subject: [PATCH 08/30] cryptsetup: split code that allocates udev security device monitor into its own function --- src/cryptsetup/cryptsetup.c | 42 ++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 9ff99d91a9..5d016143f5 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -560,6 +560,32 @@ static char *make_bindname(const char *volume) { return s; } +static int make_security_device_monitor(sd_event *event, sd_device_monitor **ret) { + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL; + int r; + + assert(ret); + + r = sd_device_monitor_new(&monitor); + if (r < 0) + return log_error_errno(r, "Failed to allocate device monitor: %m"); + + r = sd_device_monitor_filter_add_match_tag(monitor, "security-device"); + if (r < 0) + return log_error_errno(r, "Failed to configure device monitor: %m"); + + r = sd_device_monitor_attach_event(monitor, event); + if (r < 0) + return log_error_errno(r, "Failed to attach device monitor: %m"); + + r = sd_device_monitor_start(monitor, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + + *ret = TAKE_PTR(monitor); + return 0; +} + static int attach_luks_or_plain_or_bitlk( struct crypt_device *cd, const char *name, @@ -668,21 +694,9 @@ static int attach_luks_or_plain_or_bitlk( if (r < 0) return log_error_errno(r, "Failed to allocate event loop: %m"); - r = sd_device_monitor_new(&monitor); + r = make_security_device_monitor(event, &monitor); if (r < 0) - return log_error_errno(r, "Failed to allocate device monitor: %m"); - - r = sd_device_monitor_filter_add_match_tag(monitor, "security-device"); - if (r < 0) - return log_error_errno(r, "Failed to configure device monitor: %m"); - - r = sd_device_monitor_attach_event(monitor, event); - if (r < 0) - return log_error_errno(r, "Failed to attach device monitor: %m"); - - r = sd_device_monitor_start(monitor, NULL, NULL); - if (r < 0) - return log_error_errno(r, "Failed to start device monitor: %m"); + return r; log_notice("Security token %s not present for unlocking volume %s, please plug it in.", arg_pkcs11_uri, friendly); From d3ad474f0c60a55057cca35ad5f62177bd395fe2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 25 Nov 2020 11:58:55 +0100 Subject: [PATCH 09/30] cryptsetup: be more careful with erasing key material from memory --- src/cryptsetup/cryptsetup.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 5d016143f5..4ac76f1826 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -468,7 +468,8 @@ static int get_password( return log_oom(); strncpy(c, *p, arg_key_size); - free_and_replace(*p, c); + erase_and_free(*p); + *p = TAKE_PTR(c); } *ret = TAKE_PTR(passwords); @@ -486,7 +487,7 @@ static int attach_tcrypt( uint32_t flags) { int r = 0; - _cleanup_free_ char *passphrase = NULL; + _cleanup_(erase_and_freep) char *passphrase = NULL; struct crypt_params_tcrypt params = { .flags = CRYPT_TCRYPT_LEGACY_MODES, .keyfiles = (const char **)arg_tcrypt_keyfiles, @@ -656,8 +657,8 @@ static int attach_luks_or_plain_or_bitlk( if (arg_pkcs11_uri) { _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL; + _cleanup_(erase_and_freep) void *decrypted_key = NULL; _cleanup_(sd_event_unrefp) sd_event *event = NULL; - _cleanup_free_ void *decrypted_key = NULL; _cleanup_free_ char *friendly = NULL; size_t decrypted_key_size = 0; @@ -724,7 +725,7 @@ static int attach_luks_or_plain_or_bitlk( if (pass_volume_key) r = crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); else { - _cleanup_free_ char *base64_encoded = NULL; + _cleanup_(erase_and_freep) char *base64_encoded = NULL; /* Before using this key as passphrase we base64 encode it. Why? For compatibility * with homed's PKCS#11 hookup: there we want to use the key we acquired through From b997d1115bf21b58dd876e02ce095bc483e3c875 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 25 Nov 2020 11:10:29 +0100 Subject: [PATCH 10/30] cryptsetup: read PKCS#11 key and token info from LUKS2 metadata Optionally, embedd PKCS#11 token URI and encrypted key in LUKS2 JSON metadata header. That way it becomes very easy to unlock properly set up PKCS#11-enabled LUKS2 volumes, a simple /etc/crypttab line like the following suffices: mytest /dev/disk/by-partuuid/41c1df55-e628-4dbb-8492-bc69d81e172e - pkcs11-uri=auto Such a line declares that unlocking via PKCS#11 shall be attempted, and the token URI and the encrypted key shall be read from the LUKS2 header. An external key file for the encrypted PKCS#11 key is hence no longer necessary, nor is specifying the precise URI to use. --- src/cryptsetup/cryptsetup-pkcs11.c | 80 ++++++++++++++++++++++++++++++ src/cryptsetup/cryptsetup-pkcs11.h | 19 +++++++ src/cryptsetup/cryptsetup.c | 52 ++++++++++++++----- src/shared/cryptsetup-util.h | 4 ++ 4 files changed, 142 insertions(+), 13 deletions(-) diff --git a/src/cryptsetup/cryptsetup-pkcs11.c b/src/cryptsetup/cryptsetup-pkcs11.c index b645ff28e0..93cf7c64b3 100644 --- a/src/cryptsetup/cryptsetup-pkcs11.c +++ b/src/cryptsetup/cryptsetup-pkcs11.c @@ -14,8 +14,11 @@ #include "fd-util.h" #include "fileio.h" #include "format-util.h" +#include "hexdecoct.h" +#include "json.h" #include "macro.h" #include "memory-util.h" +#include "parse-util.h" #include "pkcs11-util.h" #include "random-util.h" #include "stat-util.h" @@ -156,3 +159,80 @@ int decrypt_pkcs11_key( return 0; } + +int find_pkcs11_auto_data( + struct crypt_device *cd, + char **ret_uri, + void **ret_encrypted_key, + size_t *ret_encrypted_key_size, + int *ret_keyslot) { + + _cleanup_free_ char *uri = NULL; + _cleanup_free_ void *key = NULL; + int r, keyslot = -1; + size_t key_size = 0; + + assert(cd); + assert(ret_uri); + assert(ret_encrypted_key); + assert(ret_encrypted_key_size); + assert(ret_keyslot); + + /* Loads PKCS#11 metadata from LUKS2 JSON token headers. */ + + for (int token = 0; token < LUKS2_TOKENS_MAX; token++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + JsonVariant *w; + + r = cryptsetup_get_token_as_json(cd, token, "systemd-pkcs11", &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"); + + if (uri) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), + "Multiple PKCS#11 tokens enrolled, cannot automatically determine token."); + + w = json_variant_by_key(v, "pkcs11-uri"); + if (!w || !json_variant_is_string(w)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "PKCS#11 token data lacks 'pkcs11-uri' field."); + + uri = strdup(json_variant_string(w)); + if (!uri) + return log_oom(); + + if (!pkcs11_uri_valid(uri)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "PKCS#11 token data contains invalid PKCS#11 URI."); + + w = json_variant_by_key(v, "pkcs11-key"); + if (!w || !json_variant_is_string(w)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "PKCS#11 token data lacks 'pkcs11-key' field."); + + assert(!key); + assert(key_size == 0); + r = unbase64mem(json_variant_string(w), (size_t) -1, &key, &key_size); + if (r < 0) + return log_error_errno(r, "Failed to decode base64 encoded key."); + + assert(keyslot < 0); + keyslot = cryptsetup_get_keyslot_from_token(v); + if (keyslot < 0) + return log_error_errno(keyslot, "Failed to extract keyslot index from PKCS#11 JSON data: %m"); + } + + if (!uri) + return log_error_errno(SYNTHETIC_ERRNO(ENXIO), + "No valid PKCS#11 token data found."); + + log_info("Automatically discovered security PKCS#11 token '%s' unlocks volume.", uri); + + *ret_uri = TAKE_PTR(uri); + *ret_encrypted_key = TAKE_PTR(key); + *ret_encrypted_key_size = key_size; + *ret_keyslot = keyslot; + return 0; +} diff --git a/src/cryptsetup/cryptsetup-pkcs11.h b/src/cryptsetup/cryptsetup-pkcs11.h index 522ed28bd3..4cd82e0215 100644 --- a/src/cryptsetup/cryptsetup-pkcs11.h +++ b/src/cryptsetup/cryptsetup-pkcs11.h @@ -3,6 +3,7 @@ #include +#include "cryptsetup-util.h" #include "log.h" #include "time-util.h" @@ -21,6 +22,13 @@ int decrypt_pkcs11_key( void **ret_decrypted_key, size_t *ret_decrypted_key_size); +int find_pkcs11_auto_data( + struct crypt_device *cd, + char **ret_uri, + void **ret_encrypted_key, + size_t *ret_encrypted_key_size, + int *ret_keyslot); + #else static inline int decrypt_pkcs11_key( @@ -40,4 +48,15 @@ static inline int decrypt_pkcs11_key( "PKCS#11 Token support not available."); } +static inline int find_pkcs11_auto_data( + struct crypt_device *cd, + char **ret_uri, + void **ret_encrypted_key, + size_t *ret_encrypted_key_size, + int *ret_keyslot) { + + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "PKCS#11 Token support not available."); +} + #endif diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 4ac76f1826..10ba44a559 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -64,6 +64,7 @@ static uint64_t arg_offset = 0; static uint64_t arg_skip = 0; static usec_t arg_timeout = USEC_INFINITY; static char *arg_pkcs11_uri = NULL; +static bool arg_pkcs11_uri_auto = false; STATIC_DESTRUCTOR_REGISTER(arg_cipher, freep); STATIC_DESTRUCTOR_REGISTER(arg_hash, freep); @@ -262,12 +263,19 @@ static int parse_one_option(const char *option) { } else if ((val = startswith(option, "pkcs11-uri="))) { - if (!pkcs11_uri_valid(val)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "pkcs11-uri= parameter expects a PKCS#11 URI, refusing"); + if (streq(val, "auto")) { + arg_pkcs11_uri = mfree(arg_pkcs11_uri); + arg_pkcs11_uri_auto = true; + } else { + if (!pkcs11_uri_valid(val)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "pkcs11-uri= parameter expects a PKCS#11 URI, refusing"); - r = free_and_strdup(&arg_pkcs11_uri, val); - if (r < 0) - return log_oom(); + r = free_and_strdup(&arg_pkcs11_uri, val); + if (r < 0) + return log_oom(); + + arg_pkcs11_uri_auto = false; + } } else if ((val = startswith(option, "try-empty-password="))) { @@ -498,7 +506,7 @@ static int attach_tcrypt( assert(name); assert(key_file || key_data || !strv_isempty(passwords)); - if (arg_pkcs11_uri) + if (arg_pkcs11_uri || arg_pkcs11_uri_auto) /* Ask for a regular password */ return log_error_errno(SYNTHETIC_ERRNO(EAGAIN), "Sorry, but tcrypt devices are currently not supported in conjunction with pkcs11 support."); @@ -655,12 +663,29 @@ static int attach_luks_or_plain_or_bitlk( crypt_get_volume_key_size(cd)*8, crypt_get_device_name(cd)); - if (arg_pkcs11_uri) { + if (arg_pkcs11_uri || arg_pkcs11_uri_auto) { _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL; + _cleanup_free_ char *friendly = NULL, *discovered_uri = NULL; + size_t decrypted_key_size = 0, discovered_key_size = 0; _cleanup_(erase_and_freep) void *decrypted_key = NULL; _cleanup_(sd_event_unrefp) sd_event *event = NULL; - _cleanup_free_ char *friendly = NULL; - size_t decrypted_key_size = 0; + _cleanup_free_ void *discovered_key = NULL; + int keyslot = arg_key_slot; + const char *uri; + + if (arg_pkcs11_uri_auto) { + r = find_pkcs11_auto_data(cd, &discovered_uri, &discovered_key, &discovered_key_size, &keyslot); + if (IN_SET(r, -ENOTUNIQ, -ENXIO)) + return log_debug_errno(SYNTHETIC_ERRNO(EAGAIN), + "Automatic PKCS#11 metadata discovery was not possible because missing or not unique, falling back to traditional unlocking."); + if (r < 0) + return r; + + key_data = discovered_key; + key_data_size = discovered_key_size; + uri = discovered_uri; + } else + uri = arg_pkcs11_uri; if (!key_file && !key_data) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "PKCS#11 mode selected but no key file specified, refusing."); @@ -675,7 +700,7 @@ static int attach_luks_or_plain_or_bitlk( r = decrypt_pkcs11_key( name, friendly, - arg_pkcs11_uri, + uri, key_file, arg_keyfile_size, arg_keyfile_offset, key_data, key_data_size, until, @@ -700,7 +725,7 @@ static int attach_luks_or_plain_or_bitlk( return r; log_notice("Security token %s not present for unlocking volume %s, please plug it in.", - arg_pkcs11_uri, friendly); + uri, friendly); /* Let's immediately rescan in case the token appeared in the time we needed * to create and configure the monitor */ @@ -739,7 +764,7 @@ static int attach_luks_or_plain_or_bitlk( if (r < 0) return log_oom(); - r = crypt_activate_by_passphrase(cd, name, arg_key_slot, base64_encoded, strlen(base64_encoded), flags); + r = crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, strlen(base64_encoded), flags); } if (r == -EPERM) { log_error_errno(r, "Failed to activate with PKCS#11 decrypted key. (Key incorrect?)"); @@ -1030,7 +1055,7 @@ static int run(int argc, char *argv[]) { * 5. We enquire the user for a password */ - if (!key_file && !key_data && !arg_pkcs11_uri) { + if (!key_file && !key_data && !arg_pkcs11_uri && !arg_pkcs11_uri_auto) { if (arg_try_empty_password) { /* Hmm, let's try an empty password now, but only once */ @@ -1068,6 +1093,7 @@ static int run(int argc, char *argv[]) { key_data = erase_and_free(key_data); key_data_size = 0; arg_pkcs11_uri = mfree(arg_pkcs11_uri); + arg_pkcs11_uri_auto = false; } if (arg_tries != 0 && tries >= arg_tries) diff --git a/src/shared/cryptsetup-util.h b/src/shared/cryptsetup-util.h index 26f5dd3c89..fa2d2f65f3 100644 --- a/src/shared/cryptsetup-util.h +++ b/src/shared/cryptsetup-util.h @@ -43,4 +43,8 @@ int cryptsetup_get_token_as_json(struct crypt_device *cd, int idx, const char *v int cryptsetup_get_keyslot_from_token(JsonVariant *v); int cryptsetup_add_token_json(struct crypt_device *cd, JsonVariant *v); +/* Stolen from cryptsetup's sources. We use to iterate through all tokens defined for a volume. Ideally, we'd + * be able to query this via some API, but there appears to be none currently in libcryptsetup. */ +#define LUKS2_TOKENS_MAX 32 + #endif From b8c80b56d13ba2704ae28bcf1ada4d3f512cb1be Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 26 Nov 2020 13:29:56 +0100 Subject: [PATCH 11/30] cryptsetup: split up attach_luks_or_plain_or_bitlk() into smaller functions Just some refactoring. --- src/cryptsetup/cryptsetup.c | 430 +++++++++++++++++++++--------------- 1 file changed, 250 insertions(+), 180 deletions(-) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 10ba44a559..bc88d7102a 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -595,6 +595,240 @@ static int make_security_device_monitor(sd_event *event, sd_device_monitor **ret return 0; } +static int attach_luks_or_plain_or_bitlk_by_pkcs11( + struct crypt_device *cd, + const char *name, + const char *key_file, + const void *key_data, + size_t key_data_size, + usec_t until, + uint32_t flags, + bool pass_volume_key) { + + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL; + _cleanup_free_ char *friendly = NULL, *discovered_uri = NULL; + size_t decrypted_key_size = 0, discovered_key_size = 0; + _cleanup_(erase_and_freep) void *decrypted_key = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + _cleanup_free_ void *discovered_key = NULL; + int keyslot = arg_key_slot, r; + const char *uri; + + assert(cd); + assert(name); + assert(arg_pkcs11_uri || arg_pkcs11_uri_auto); + + if (arg_pkcs11_uri_auto) { + r = find_pkcs11_auto_data(cd, &discovered_uri, &discovered_key, &discovered_key_size, &keyslot); + if (IN_SET(r, -ENOTUNIQ, -ENXIO)) + return log_debug_errno(SYNTHETIC_ERRNO(EAGAIN), + "Automatic PKCS#11 metadata discovery was not possible because missing or not unique, falling back to traditional unlocking."); + if (r < 0) + return r; + + uri = discovered_uri; + key_data = discovered_key; + key_data_size = discovered_key_size; + } else { + uri = arg_pkcs11_uri; + + if (!key_file && !key_data) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "PKCS#11 mode selected but no key file specified, refusing."); + } + + friendly = friendly_disk_name(crypt_get_device_name(cd), name); + if (!friendly) + return log_oom(); + + for (;;) { + bool processed = false; + + r = decrypt_pkcs11_key( + name, + friendly, + uri, + key_file, arg_keyfile_size, arg_keyfile_offset, + key_data, key_data_size, + until, + &decrypted_key, &decrypted_key_size); + if (r >= 0) + break; + if (r != -EAGAIN) /* EAGAIN means: token not found */ + return r; + + if (!monitor) { + /* We didn't find the token. In this case, watch for it via udev. Let's + * create an event loop and monitor first. */ + + assert(!event); + + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to allocate event loop: %m"); + + r = make_security_device_monitor(event, &monitor); + if (r < 0) + return r; + + log_notice("Security token %s not present for unlocking volume %s, please plug it in.", + uri, friendly); + + /* Let's immediately rescan in case the token appeared in the time we needed + * to create and configure the monitor */ + continue; + } + + for (;;) { + /* Wait for one event, and then eat all subsequent events until there are no + * further ones */ + r = sd_event_run(event, processed ? 0 : UINT64_MAX); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + if (r == 0) + break; + + processed = true; + } + + log_debug("Got one or more potentially relevant udev events, rescanning PKCS#11..."); + } + + if (pass_volume_key) + r = crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); + else { + _cleanup_(erase_and_freep) char *base64_encoded = NULL; + + /* Before using this key as passphrase we base64 encode it. Why? For compatibility + * with homed's PKCS#11 hookup: there we want to use the key we acquired through + * PKCS#11 for other authentication/decryption mechanisms too, and some of them do + * not not take arbitrary binary blobs, but require NUL-terminated strings — most + * importantly UNIX password hashes. Hence, for compatibility we want to use a string + * without embedded NUL here too, and that's easiest to generate from a binary blob + * via base64 encoding. */ + + r = base64mem(decrypted_key, decrypted_key_size, &base64_encoded); + if (r < 0) + return log_oom(); + + r = crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, strlen(base64_encoded), flags); + } + if (r == -EPERM) { + log_error_errno(r, "Failed to activate with PKCS#11 decrypted key. (Key incorrect?)"); + return -EAGAIN; /* log actual error, but return EAGAIN */ + } + if (r < 0) + return log_error_errno(r, "Failed to activate with PKCS#11 acquired key: %m"); + + return 0; +} + +static int attach_luks_or_plain_or_bitlk_by_key_data( + struct crypt_device *cd, + const char *name, + const void *key_data, + size_t key_data_size, + uint32_t flags, + bool pass_volume_key) { + + int r; + + assert(cd); + assert(name); + assert(key_data); + + if (pass_volume_key) + r = crypt_activate_by_volume_key(cd, name, key_data, key_data_size, flags); + else + r = crypt_activate_by_passphrase(cd, name, arg_key_slot, key_data, key_data_size, flags); + if (r == -EPERM) { + log_error_errno(r, "Failed to activate. (Key incorrect?)"); + return -EAGAIN; /* Log actual error, but return EAGAIN */ + } + if (r < 0) + return log_error_errno(r, "Failed to activate: %m"); + + return 0; +} + +static int attach_luks_or_plain_or_bitlk_by_key_file( + struct crypt_device *cd, + const char *name, + const char *key_file, + uint32_t flags, + bool pass_volume_key) { + + _cleanup_(erase_and_freep) char *kfdata = NULL; + _cleanup_free_ char *bindname = NULL; + size_t kfsize; + int r; + + assert(cd); + assert(name); + assert(key_file); + + /* If we read the key via AF_UNIX, make this client recognizable */ + bindname = make_bindname(name); + if (!bindname) + return log_oom(); + + r = read_full_file_full( + AT_FDCWD, key_file, + arg_keyfile_offset == 0 ? UINT64_MAX : arg_keyfile_offset, + arg_keyfile_size == 0 ? SIZE_MAX : arg_keyfile_size, + READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET, + bindname, + &kfdata, &kfsize); + if (r == -ENOENT) { + log_error_errno(r, "Failed to activate, key file '%s' missing.", key_file); + return -EAGAIN; /* Log actual error, but return EAGAIN */ + } + + if (pass_volume_key) + r = crypt_activate_by_volume_key(cd, name, kfdata, kfsize, flags); + else + r = crypt_activate_by_passphrase(cd, name, arg_key_slot, kfdata, kfsize, flags); + if (r == -EPERM) { + log_error_errno(r, "Failed to activate with key file '%s'. (Key data incorrect?)", key_file); + return -EAGAIN; /* Log actual error, but return EAGAIN */ + } + if (r < 0) + return log_error_errno(r, "Failed to activate with key file '%s': %m", key_file); + + return 0; +} + +static int attach_luks_or_plain_or_bitlk_by_passphrase( + struct crypt_device *cd, + const char *name, + char **passwords, + uint32_t flags, + bool pass_volume_key) { + + char **p; + int r; + + assert(cd); + assert(name); + + r = -EINVAL; + STRV_FOREACH(p, passwords) { + if (pass_volume_key) + r = crypt_activate_by_volume_key(cd, name, *p, arg_key_size, flags); + else + r = crypt_activate_by_passphrase(cd, name, arg_key_slot, *p, strlen(*p), flags); + if (r >= 0) + break; + } + if (r == -EPERM) { + log_error_errno(r, "Failed to activate with specified passphrase. (Passphrase incorrect?)"); + return -EAGAIN; /* log actual error, but return EAGAIN */ + } + if (r < 0) + return log_error_errno(r, "Failed to activate with specified passphrase: %m"); + + return 0; +} + static int attach_luks_or_plain_or_bitlk( struct crypt_device *cd, const char *name, @@ -605,8 +839,8 @@ static int attach_luks_or_plain_or_bitlk( uint32_t flags, usec_t until) { - int r = 0; bool pass_volume_key = false; + int r; assert(cd); assert(name); @@ -620,13 +854,14 @@ static int attach_luks_or_plain_or_bitlk( const char *cipher, *cipher_mode; _cleanup_free_ char *truncated_cipher = NULL; - if (arg_hash) { + if (streq_ptr(arg_hash, "plain")) /* plain isn't a real hash type. it just means "use no hash" */ - if (!streq(arg_hash, "plain")) - params.hash = arg_hash; - } else if (!key_file) - /* for CRYPT_PLAIN, the behaviour of cryptsetup - * package is to not hash when a key file is provided */ + params.hash = NULL; + else if (arg_hash) + params.hash = arg_hash; + else if (!key_file) + /* for CRYPT_PLAIN, the behaviour of cryptsetup package is to not hash when a key + * file is provided */ params.hash = "ripemd160"; if (arg_cipher) { @@ -654,7 +889,7 @@ static int attach_luks_or_plain_or_bitlk( return log_error_errno(r, "Loading of cryptographic parameters failed: %m"); /* hash == NULL implies the user passed "plain" */ - pass_volume_key = (params.hash == NULL); + pass_volume_key = !params.hash; } log_info("Set cipher %s, mode %s, key size %i bits for device %s.", @@ -663,179 +898,14 @@ static int attach_luks_or_plain_or_bitlk( crypt_get_volume_key_size(cd)*8, crypt_get_device_name(cd)); - if (arg_pkcs11_uri || arg_pkcs11_uri_auto) { - _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL; - _cleanup_free_ char *friendly = NULL, *discovered_uri = NULL; - size_t decrypted_key_size = 0, discovered_key_size = 0; - _cleanup_(erase_and_freep) void *decrypted_key = NULL; - _cleanup_(sd_event_unrefp) sd_event *event = NULL; - _cleanup_free_ void *discovered_key = NULL; - int keyslot = arg_key_slot; - const char *uri; + if (arg_pkcs11_uri || arg_pkcs11_uri_auto) + return attach_luks_or_plain_or_bitlk_by_pkcs11(cd, name, key_file, key_data, key_data_size, until, flags, pass_volume_key); + if (key_data) + return attach_luks_or_plain_or_bitlk_by_key_data(cd, name, key_data, key_data_size, flags, pass_volume_key); + if (key_file) + return attach_luks_or_plain_or_bitlk_by_key_file(cd, name, key_file, flags, pass_volume_key); - if (arg_pkcs11_uri_auto) { - r = find_pkcs11_auto_data(cd, &discovered_uri, &discovered_key, &discovered_key_size, &keyslot); - if (IN_SET(r, -ENOTUNIQ, -ENXIO)) - return log_debug_errno(SYNTHETIC_ERRNO(EAGAIN), - "Automatic PKCS#11 metadata discovery was not possible because missing or not unique, falling back to traditional unlocking."); - if (r < 0) - return r; - - key_data = discovered_key; - key_data_size = discovered_key_size; - uri = discovered_uri; - } else - uri = arg_pkcs11_uri; - - if (!key_file && !key_data) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "PKCS#11 mode selected but no key file specified, refusing."); - - friendly = friendly_disk_name(crypt_get_device_name(cd), name); - if (!friendly) - return log_oom(); - - for (;;) { - bool processed = false; - - r = decrypt_pkcs11_key( - name, - friendly, - uri, - key_file, arg_keyfile_size, arg_keyfile_offset, - key_data, key_data_size, - until, - &decrypted_key, &decrypted_key_size); - if (r >= 0) - break; - if (r != -EAGAIN) /* EAGAIN means: token not found */ - return r; - - if (!monitor) { - /* We didn't find the token. In this case, watch for it via udev. Let's - * create an event loop and monitor first. */ - - assert(!event); - - r = sd_event_default(&event); - if (r < 0) - return log_error_errno(r, "Failed to allocate event loop: %m"); - - r = make_security_device_monitor(event, &monitor); - if (r < 0) - return r; - - log_notice("Security token %s not present for unlocking volume %s, please plug it in.", - uri, friendly); - - /* Let's immediately rescan in case the token appeared in the time we needed - * to create and configure the monitor */ - continue; - } - - for (;;) { - /* Wait for one event, and then eat all subsequent events until there are no - * further ones */ - r = sd_event_run(event, processed ? 0 : UINT64_MAX); - if (r < 0) - return log_error_errno(r, "Failed to run event loop: %m"); - if (r == 0) - break; - - processed = true; - } - - log_debug("Got one or more potentially relevant udev events, rescanning PKCS#11..."); - } - - if (pass_volume_key) - r = crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); - else { - _cleanup_(erase_and_freep) char *base64_encoded = NULL; - - /* Before using this key as passphrase we base64 encode it. Why? For compatibility - * with homed's PKCS#11 hookup: there we want to use the key we acquired through - * PKCS#11 for other authentication/decryption mechanisms too, and some of them do - * not not take arbitrary binary blobs, but require NUL-terminated strings — most - * importantly UNIX password hashes. Hence, for compatibility we want to use a string - * without embedded NUL here too, and that's easiest to generate from a binary blob - * via base64 encoding. */ - - r = base64mem(decrypted_key, decrypted_key_size, &base64_encoded); - if (r < 0) - return log_oom(); - - r = crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, strlen(base64_encoded), flags); - } - if (r == -EPERM) { - log_error_errno(r, "Failed to activate with PKCS#11 decrypted key. (Key incorrect?)"); - return -EAGAIN; /* log actual error, but return EAGAIN */ - } - if (r < 0) - return log_error_errno(r, "Failed to activate with PKCS#11 acquired key: %m"); - - } else if (key_data) { - if (pass_volume_key) - r = crypt_activate_by_volume_key(cd, name, key_data, key_data_size, flags); - else - r = crypt_activate_by_passphrase(cd, name, arg_key_slot, key_data, key_data_size, flags); - if (r == -EPERM) { - log_error_errno(r, "Failed to activate. (Key incorrect?)"); - return -EAGAIN; /* Log actual error, but return EAGAIN */ - } - if (r < 0) - return log_error_errno(r, "Failed to activate: %m"); - - } else if (key_file) { - _cleanup_(erase_and_freep) char *kfdata = NULL; - _cleanup_free_ char *bindname = NULL; - size_t kfsize; - - /* If we read the key via AF_UNIX, make this client recognizable */ - bindname = make_bindname(name); - if (!bindname) - return log_oom(); - - r = read_full_file_full( - AT_FDCWD, key_file, - arg_keyfile_offset == 0 ? UINT64_MAX : arg_keyfile_offset, - arg_keyfile_size == 0 ? SIZE_MAX : arg_keyfile_size, - READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE|READ_FULL_FILE_CONNECT_SOCKET, - bindname, - &kfdata, &kfsize); - if (r == -ENOENT) { - log_error_errno(r, "Failed to activate, key file '%s' missing.", key_file); - return -EAGAIN; /* Log actual error, but return EAGAIN */ - } - - r = crypt_activate_by_passphrase(cd, name, arg_key_slot, kfdata, kfsize, flags); - if (r == -EPERM) { - log_error_errno(r, "Failed to activate with key file '%s'. (Key data incorrect?)", key_file); - return -EAGAIN; /* Log actual error, but return EAGAIN */ - } - if (r < 0) - return log_error_errno(r, "Failed to activate with key file '%s': %m", key_file); - - } else { - char **p; - - r = -EINVAL; - STRV_FOREACH(p, passwords) { - if (pass_volume_key) - r = crypt_activate_by_volume_key(cd, name, *p, arg_key_size, flags); - else - r = crypt_activate_by_passphrase(cd, name, arg_key_slot, *p, strlen(*p), flags); - if (r >= 0) - break; - } - if (r == -EPERM) { - log_error_errno(r, "Failed to activate with specified passphrase. (Passphrase incorrect?)"); - return -EAGAIN; /* log actual error, but return EAGAIN */ - } - if (r < 0) - return log_error_errno(r, "Failed to activate with specified passphrase: %m"); - } - - return r; + return attach_luks_or_plain_or_bitlk_by_passphrase(cd, name, passwords, flags, pass_volume_key); } static int help(void) { From 69cb28965b9a835270a7def918e765ac156ae07a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 25 Nov 2020 15:07:06 +0100 Subject: [PATCH 12/30] homed: turn libfido2 into a dlopen() type dependency --- meson.build | 4 +- src/home/homectl-fido2.c | 147 ++++++++++++++++++++----------------- src/home/homework-fido2.c | 77 ++++++++++--------- src/shared/libfido2-util.c | 117 +++++++++++++++++++++++++++++ src/shared/libfido2-util.h | 72 ++++++++++++++++++ src/shared/meson.build | 2 + 6 files changed, 313 insertions(+), 106 deletions(-) create mode 100644 src/shared/libfido2-util.c create mode 100644 src/shared/libfido2-util.h diff --git a/meson.build b/meson.build index 82c6de900c..9a7f9be3cb 100644 --- a/meson.build +++ b/meson.build @@ -2271,8 +2271,7 @@ if conf.get('ENABLE_HOMED') == 1 libcrypt, libopenssl, libfdisk, - libp11kit, - libfido2], + libp11kit], install_rpath : rootlibexecdir, install : true, install_dir : rootlibexecdir) @@ -2298,7 +2297,6 @@ if conf.get('ENABLE_HOMED') == 1 libcrypt, libopenssl, libp11kit, - libfido2, libdl], install_rpath : rootlibexecdir, install : true, diff --git a/src/home/homectl-fido2.c b/src/home/homectl-fido2.c index 5557b70e67..983891e0e0 100644 --- a/src/home/homectl-fido2.c +++ b/src/home/homectl-fido2.c @@ -11,6 +11,7 @@ #include "homectl-fido2.h" #include "homectl-pkcs11.h" #include "libcrypt-util.h" +#include "libfido2-util.h" #include "locale-util.h" #include "memory-util.h" #include "random-util.h" @@ -116,11 +117,11 @@ int identity_add_fido2_parameters( const char *device) { #if HAVE_LIBFIDO2 - _cleanup_(fido_cbor_info_free) fido_cbor_info_t *di = NULL; - _cleanup_(fido_assert_free) fido_assert_t *a = NULL; + _cleanup_(fido_cbor_info_free_wrapper) fido_cbor_info_t *di = NULL; + _cleanup_(fido_assert_free_wrapper) fido_assert_t *a = NULL; + _cleanup_(fido_cred_free_wrapper) fido_cred_t *c = NULL; + _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL; _cleanup_(erase_and_freep) char *used_pin = NULL; - _cleanup_(fido_cred_free) fido_cred_t *c = NULL; - _cleanup_(fido_dev_free) fido_dev_t *d = NULL; _cleanup_(erase_and_freep) void *salt = NULL; JsonVariant *un, *realm, *rn; bool found_extension = false; @@ -146,6 +147,10 @@ int identity_add_fido2_parameters( assert(v); assert(device); + r = dlopen_libfido2(); + if (r < 0) + return log_error_errno(r, "FIDO2 token support is not installed."); + salt = malloc(FIDO2_SALT_SIZE); if (!salt) return log_oom(); @@ -154,30 +159,30 @@ int identity_add_fido2_parameters( if (r < 0) return log_error_errno(r, "Failed to generate salt: %m"); - d = fido_dev_new(); + d = sym_fido_dev_new(); if (!d) return log_oom(); - r = fido_dev_open(d, device); + r = sym_fido_dev_open(d, device); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to open FIDO2 device %s: %s", device, fido_strerr(r)); + "Failed to open FIDO2 device %s: %s", device, sym_fido_strerr(r)); - if (!fido_dev_is_fido2(d)) + if (!sym_fido_dev_is_fido2(d)) return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "Specified device %s is not a FIDO2 device.", device); - di = fido_cbor_info_new(); + di = sym_fido_cbor_info_new(); if (!di) return log_oom(); - r = fido_dev_get_cbor_info(d, di); + r = sym_fido_dev_get_cbor_info(d, di); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to get CBOR device info for %s: %s", device, fido_strerr(r)); + "Failed to get CBOR device info for %s: %s", device, sym_fido_strerr(r)); - e = fido_cbor_info_extensions_ptr(di); - n = fido_cbor_info_extensions_len(di); + e = sym_fido_cbor_info_extensions_ptr(di); + n = sym_fido_cbor_info_extensions_len(di); for (size_t i = 0; i < n; i++) if (streq(e[i], "hmac-secret")) { @@ -189,24 +194,24 @@ int identity_add_fido2_parameters( return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", device); - c = fido_cred_new(); + c = sym_fido_cred_new(); if (!c) return log_oom(); - r = fido_cred_set_extensions(c, FIDO_EXT_HMAC_SECRET); + r = sym_fido_cred_set_extensions(c, FIDO_EXT_HMAC_SECRET); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to enable HMAC-SECRET extension on FIDO2 credential: %s", fido_strerr(r)); + "Failed to enable HMAC-SECRET extension on FIDO2 credential: %s", sym_fido_strerr(r)); - r = fido_cred_set_rp(c, "io.systemd.home", "Home Directory"); + r = sym_fido_cred_set_rp(c, "io.systemd.home", "Home Directory"); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 credential relying party ID/name: %s", fido_strerr(r)); + "Failed to set FIDO2 credential relying party ID/name: %s", sym_fido_strerr(r)); - r = fido_cred_set_type(c, COSE_ES256); + r = sym_fido_cred_set_type(c, COSE_ES256); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 credential type to ES256: %s", fido_strerr(r)); + "Failed to set FIDO2 credential type to ES256: %s", sym_fido_strerr(r)); un = json_variant_by_key(*v, "userName"); if (!un) @@ -231,29 +236,29 @@ int identity_add_fido2_parameters( return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "realName field of user record is not a string"); - r = fido_cred_set_user(c, + r = sym_fido_cred_set_user(c, (const unsigned char*) fido_un, strlen(fido_un), /* We pass the user ID and name as the same */ fido_un, rn ? json_variant_string(rn) : NULL, NULL /* icon URL */); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 credential user data: %s", fido_strerr(r)); + "Failed to set FIDO2 credential user data: %s", sym_fido_strerr(r)); - r = fido_cred_set_clientdata_hash(c, (const unsigned char[32]) {}, 32); + r = sym_fido_cred_set_clientdata_hash(c, (const unsigned char[32]) {}, 32); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 client data hash: %s", fido_strerr(r)); + "Failed to set FIDO2 client data hash: %s", sym_fido_strerr(r)); - r = fido_cred_set_rk(c, FIDO_OPT_FALSE); + r = sym_fido_cred_set_rk(c, FIDO_OPT_FALSE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to turn off FIDO2 resident key option of credential: %s", fido_strerr(r)); + "Failed to turn off FIDO2 resident key option of credential: %s", sym_fido_strerr(r)); - r = fido_cred_set_uv(c, FIDO_OPT_FALSE); + r = sym_fido_cred_set_uv(c, FIDO_OPT_FALSE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to turn off FIDO2 user verification option of credential: %s", fido_strerr(r)); + "Failed to turn off FIDO2 user verification option of credential: %s", sym_fido_strerr(r)); log_info("Initializing FIDO2 credential on security token."); @@ -261,7 +266,7 @@ int identity_add_fido2_parameters( emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", emoji_enabled() ? " " : ""); - r = fido_dev_make_cred(d, c, NULL); + r = sym_fido_dev_make_cred(d, c, NULL); if (r == FIDO_ERR_PIN_REQUIRED) { _cleanup_free_ char *text = NULL; @@ -283,7 +288,7 @@ int identity_add_fido2_parameters( continue; } - r = fido_dev_make_cred(d, c, *i); + r = sym_fido_dev_make_cred(d, c, *i); if (r == FIDO_OK) { used_pin = strdup(*i); if (!used_pin) @@ -308,75 +313,75 @@ int identity_add_fido2_parameters( "Token action timeout. (User didn't interact with token quickly enough.)"); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to generate FIDO2 credential: %s", fido_strerr(r)); + "Failed to generate FIDO2 credential: %s", sym_fido_strerr(r)); - cid = fido_cred_id_ptr(c); + cid = sym_fido_cred_id_ptr(c); if (!cid) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get FIDO2 credential ID."); - cid_size = fido_cred_id_len(c); + cid_size = sym_fido_cred_id_len(c); - a = fido_assert_new(); + a = sym_fido_assert_new(); if (!a) return log_oom(); - r = fido_assert_set_extensions(a, FIDO_EXT_HMAC_SECRET); + r = sym_fido_assert_set_extensions(a, FIDO_EXT_HMAC_SECRET); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to enable HMAC-SECRET extension on FIDO2 assertion: %s", fido_strerr(r)); + "Failed to enable HMAC-SECRET extension on FIDO2 assertion: %s", sym_fido_strerr(r)); - r = fido_assert_set_hmac_salt(a, salt, FIDO2_SALT_SIZE); + r = sym_fido_assert_set_hmac_salt(a, salt, FIDO2_SALT_SIZE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set salt on FIDO2 assertion: %s", fido_strerr(r)); + "Failed to set salt on FIDO2 assertion: %s", sym_fido_strerr(r)); - r = fido_assert_set_rp(a, "io.systemd.home"); + r = sym_fido_assert_set_rp(a, "io.systemd.home"); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 assertion ID: %s", fido_strerr(r)); + "Failed to set FIDO2 assertion ID: %s", sym_fido_strerr(r)); - r = fido_assert_set_clientdata_hash(a, (const unsigned char[32]) {}, 32); + r = sym_fido_assert_set_clientdata_hash(a, (const unsigned char[32]) {}, 32); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 assertion client data hash: %s", fido_strerr(r)); + "Failed to set FIDO2 assertion client data hash: %s", sym_fido_strerr(r)); - r = fido_assert_allow_cred(a, cid, cid_size); + r = sym_fido_assert_allow_cred(a, cid, cid_size); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to add FIDO2 assertion credential ID: %s", fido_strerr(r)); + "Failed to add FIDO2 assertion credential ID: %s", sym_fido_strerr(r)); - r = fido_assert_set_up(a, FIDO_OPT_FALSE); + r = sym_fido_assert_set_up(a, FIDO_OPT_FALSE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to turn off FIDO2 assertion user presence: %s", fido_strerr(r)); + "Failed to turn off FIDO2 assertion user presence: %s", sym_fido_strerr(r)); log_info("Generating secret key on FIDO2 security token."); - r = fido_dev_get_assert(d, a, used_pin); + r = sym_fido_dev_get_assert(d, a, used_pin); if (r == FIDO_ERR_UP_REQUIRED) { - r = fido_assert_set_up(a, FIDO_OPT_TRUE); + r = sym_fido_assert_set_up(a, FIDO_OPT_TRUE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to turn on FIDO2 assertion user presence: %s", fido_strerr(r)); + "Failed to turn on FIDO2 assertion user presence: %s", sym_fido_strerr(r)); log_notice("%s%sIn order to allow secret key generation, please verify presence on security token.", emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", emoji_enabled() ? " " : ""); - r = fido_dev_get_assert(d, a, used_pin); + r = sym_fido_dev_get_assert(d, a, used_pin); } if (r == FIDO_ERR_ACTION_TIMEOUT) return log_error_errno(SYNTHETIC_ERRNO(ENOSTR), "Token action timeout. (User didn't interact with token quickly enough.)"); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to ask token for assertion: %s", fido_strerr(r)); + "Failed to ask token for assertion: %s", sym_fido_strerr(r)); - secret = fido_assert_hmac_secret_ptr(a, 0); + secret = sym_fido_assert_hmac_secret_ptr(a, 0); if (!secret) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve HMAC secret."); - secret_size = fido_assert_hmac_secret_len(a, 0); + secret_size = sym_fido_assert_hmac_secret_len(a, 0); r = add_fido2_credential_id(v, cid, cid_size); if (r < 0) @@ -413,11 +418,15 @@ int list_fido2_devices(void) { fido_dev_info_t *di = NULL; int r; - di = fido_dev_info_new(allocated); + r = dlopen_libfido2(); + if (r < 0) + return log_error_errno(r, "FIDO2 token support is not installed."); + + di = sym_fido_dev_info_new(allocated); if (!di) return log_oom(); - r = fido_dev_info_manifest(di, allocated, &found); + r = sym_fido_dev_info_manifest(di, allocated, &found); if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) { /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ log_info("No FIDO2 devices found."); @@ -425,7 +434,7 @@ int list_fido2_devices(void) { goto finish; } if (r != FIDO_OK) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", fido_strerr(r)); + r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); goto finish; } @@ -438,7 +447,7 @@ int list_fido2_devices(void) { for (size_t i = 0; i < found; i++) { const fido_dev_info_t *entry; - entry = fido_dev_info_ptr(di, i); + entry = sym_fido_dev_info_ptr(di, i); if (!entry) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get device information for FIDO device %zu.", i); @@ -447,9 +456,9 @@ int list_fido2_devices(void) { r = table_add_many( t, - TABLE_PATH, fido_dev_info_path(entry), - TABLE_STRING, fido_dev_info_manufacturer_string(entry), - TABLE_STRING, fido_dev_info_product_string(entry)); + TABLE_PATH, sym_fido_dev_info_path(entry), + TABLE_STRING, sym_fido_dev_info_manufacturer_string(entry), + TABLE_STRING, sym_fido_dev_info_product_string(entry)); if (r < 0) { table_log_add_error(r); goto finish; @@ -465,7 +474,7 @@ int list_fido2_devices(void) { r = 0; finish: - fido_dev_info_free(&di, allocated); + sym_fido_dev_info_free(&di, allocated); return r; #else return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), @@ -482,18 +491,22 @@ int find_fido2_auto(char **ret) { const char *path; int r; - di = fido_dev_info_new(di_size); + r = dlopen_libfido2(); + if (r < 0) + return log_error_errno(r, "FIDO2 token support is not installed."); + + di = sym_fido_dev_info_new(di_size); if (!di) return log_oom(); - r = fido_dev_info_manifest(di, di_size, &found); + r = sym_fido_dev_info_manifest(di, di_size, &found); if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) { /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ r = log_error_errno(SYNTHETIC_ERRNO(ENODEV), "No FIDO2 devices found."); goto finish; } if (r != FIDO_OK) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", fido_strerr(r)); + r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); goto finish; } if (found > 1) { @@ -501,14 +514,14 @@ int find_fido2_auto(char **ret) { goto finish; } - entry = fido_dev_info_ptr(di, 0); + entry = sym_fido_dev_info_ptr(di, 0); if (!entry) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get device information for FIDO device 0."); goto finish; } - path = fido_dev_info_path(entry); + path = sym_fido_dev_info_path(entry); if (!path) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to query FIDO device path."); @@ -525,7 +538,7 @@ int find_fido2_auto(char **ret) { r = 0; finish: - fido_dev_info_free(&di, di_size); + sym_fido_dev_info_free(&di, di_size); return r; #else return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), diff --git a/src/home/homework-fido2.c b/src/home/homework-fido2.c index 2f717a5938..7c21dd34f3 100644 --- a/src/home/homework-fido2.c +++ b/src/home/homework-fido2.c @@ -4,6 +4,7 @@ #include "hexdecoct.h" #include "homework-fido2.h" +#include "libfido2-util.h" #include "strv.h" static int fido2_use_specific_token( @@ -13,39 +14,39 @@ static int fido2_use_specific_token( const Fido2HmacSalt *salt, char **ret) { - _cleanup_(fido_cbor_info_free) fido_cbor_info_t *di = NULL; - _cleanup_(fido_assert_free) fido_assert_t *a = NULL; - _cleanup_(fido_dev_free) fido_dev_t *d = NULL; + _cleanup_(fido_cbor_info_free_wrapper) fido_cbor_info_t *di = NULL; + _cleanup_(fido_assert_free_wrapper) fido_assert_t *a = NULL; + _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL; bool found_extension = false; size_t n, hmac_size; const void *hmac; char **e; int r; - d = fido_dev_new(); + d = sym_fido_dev_new(); if (!d) return log_oom(); - r = fido_dev_open(d, path); + r = sym_fido_dev_open(d, path); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to open FIDO2 device %s: %s", path, fido_strerr(r)); + "Failed to open FIDO2 device %s: %s", path, sym_fido_strerr(r)); - if (!fido_dev_is_fido2(d)) + if (!sym_fido_dev_is_fido2(d)) return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "Specified device %s is not a FIDO2 device.", path); - di = fido_cbor_info_new(); + di = sym_fido_cbor_info_new(); if (!di) return log_oom(); - r = fido_dev_get_cbor_info(d, di); + r = sym_fido_dev_get_cbor_info(d, di); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to get CBOR device info for %s: %s", path, fido_strerr(r)); + "Failed to get CBOR device info for %s: %s", path, sym_fido_strerr(r)); - e = fido_cbor_info_extensions_ptr(di); - n = fido_cbor_info_extensions_len(di); + e = sym_fido_cbor_info_extensions_ptr(di); + n = sym_fido_cbor_info_extensions_len(di); for (size_t i = 0; i < n; i++) if (streq(e[i], "hmac-secret")) { @@ -57,49 +58,49 @@ static int fido2_use_specific_token( return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", path); - a = fido_assert_new(); + a = sym_fido_assert_new(); if (!a) return log_oom(); - r = fido_assert_set_extensions(a, FIDO_EXT_HMAC_SECRET); + r = sym_fido_assert_set_extensions(a, FIDO_EXT_HMAC_SECRET); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to enable HMAC-SECRET extension on FIDO2 assertion: %s", fido_strerr(r)); + "Failed to enable HMAC-SECRET extension on FIDO2 assertion: %s", sym_fido_strerr(r)); - r = fido_assert_set_hmac_salt(a, salt->salt, salt->salt_size); + r = sym_fido_assert_set_hmac_salt(a, salt->salt, salt->salt_size); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set salt on FIDO2 assertion: %s", fido_strerr(r)); + "Failed to set salt on FIDO2 assertion: %s", sym_fido_strerr(r)); - r = fido_assert_set_rp(a, "io.systemd.home"); + r = sym_fido_assert_set_rp(a, "io.systemd.home"); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 assertion ID: %s", fido_strerr(r)); + "Failed to set FIDO2 assertion ID: %s", sym_fido_strerr(r)); - r = fido_assert_set_clientdata_hash(a, (const unsigned char[32]) {}, 32); + r = sym_fido_assert_set_clientdata_hash(a, (const unsigned char[32]) {}, 32); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 assertion client data hash: %s", fido_strerr(r)); + "Failed to set FIDO2 assertion client data hash: %s", sym_fido_strerr(r)); - r = fido_assert_allow_cred(a, salt->credential.id, salt->credential.size); + r = sym_fido_assert_allow_cred(a, salt->credential.id, salt->credential.size); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to add FIDO2 assertion credential ID: %s", fido_strerr(r)); + "Failed to add FIDO2 assertion credential ID: %s", sym_fido_strerr(r)); - r = fido_assert_set_up(a, h->fido2_user_presence_permitted <= 0 ? FIDO_OPT_FALSE : FIDO_OPT_TRUE); + r = sym_fido_assert_set_up(a, h->fido2_user_presence_permitted <= 0 ? FIDO_OPT_FALSE : FIDO_OPT_TRUE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 assertion user presence: %s", fido_strerr(r)); + "Failed to set FIDO2 assertion user presence: %s", sym_fido_strerr(r)); log_info("Asking FIDO2 token for authentication."); - r = fido_dev_get_assert(d, a, NULL); /* try without pin first */ + r = sym_fido_dev_get_assert(d, a, NULL); /* try without pin first */ if (r == FIDO_ERR_PIN_REQUIRED) { char **i; /* OK, we needed a pin, try with all pins in turn */ STRV_FOREACH(i, secret->token_pin) { - r = fido_dev_get_assert(d, a, *i); + r = sym_fido_dev_get_assert(d, a, *i); if (r != FIDO_ERR_PIN_INVALID) break; } @@ -128,14 +129,14 @@ static int fido2_use_specific_token( "Token action timeout. (User didn't interact with token quickly enough.)"); default: return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to ask token for assertion: %s", fido_strerr(r)); + "Failed to ask token for assertion: %s", sym_fido_strerr(r)); } - hmac = fido_assert_hmac_secret_ptr(a, 0); + hmac = sym_fido_assert_hmac_secret_ptr(a, 0); if (!hmac) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve HMAC secret."); - hmac_size = fido_assert_hmac_secret_len(a, 0); + hmac_size = sym_fido_assert_hmac_secret_len(a, 0); r = base64mem(hmac, hmac_size, ret); if (r < 0) @@ -149,18 +150,22 @@ int fido2_use_token(UserRecord *h, UserRecord *secret, const Fido2HmacSalt *salt fido_dev_info_t *di = NULL; int r; - di = fido_dev_info_new(allocated); + r = dlopen_libfido2(); + if (r < 0) + return log_error_errno(r, "FIDO2 support is not installed."); + + di = sym_fido_dev_info_new(allocated); if (!di) return log_oom(); - r = fido_dev_info_manifest(di, allocated, &found); + r = sym_fido_dev_info_manifest(di, allocated, &found); if (r == FIDO_ERR_INTERNAL) { /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ r = log_debug_errno(SYNTHETIC_ERRNO(EAGAIN), "Got FIDO_ERR_INTERNAL, assuming no devices."); goto finish; } if (r != FIDO_OK) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", fido_strerr(r)); + r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); goto finish; } @@ -168,14 +173,14 @@ int fido2_use_token(UserRecord *h, UserRecord *secret, const Fido2HmacSalt *salt const fido_dev_info_t *entry; const char *path; - entry = fido_dev_info_ptr(di, i); + entry = sym_fido_dev_info_ptr(di, i); if (!entry) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get device information for FIDO device %zu.", i); goto finish; } - path = fido_dev_info_path(entry); + path = sym_fido_dev_info_path(entry); if (!path) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to query FIDO device path."); @@ -192,6 +197,6 @@ int fido2_use_token(UserRecord *h, UserRecord *secret, const Fido2HmacSalt *salt r = -EAGAIN; finish: - fido_dev_info_free(&di, allocated); + sym_fido_dev_info_free(&di, allocated); return r; } diff --git a/src/shared/libfido2-util.c b/src/shared/libfido2-util.c new file mode 100644 index 0000000000..281eb7537a --- /dev/null +++ b/src/shared/libfido2-util.c @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libfido2-util.h" + +#if HAVE_LIBFIDO2 +#include "alloc-util.h" +#include "dlfcn-util.h" +#include "log.h" + +static void *libfido2_dl = NULL; + +int (*sym_fido_assert_allow_cred)(fido_assert_t *, const unsigned char *, size_t) = NULL; +void (*sym_fido_assert_free)(fido_assert_t **) = NULL; +size_t (*sym_fido_assert_hmac_secret_len)(const fido_assert_t *, size_t) = NULL; +const unsigned char* (*sym_fido_assert_hmac_secret_ptr)(const fido_assert_t *, size_t) = NULL; +fido_assert_t* (*sym_fido_assert_new)(void) = NULL; +int (*sym_fido_assert_set_clientdata_hash)(fido_assert_t *, const unsigned char *, size_t) = NULL; +int (*sym_fido_assert_set_extensions)(fido_assert_t *, int) = NULL; +int (*sym_fido_assert_set_hmac_salt)(fido_assert_t *, const unsigned char *, size_t) = NULL; +int (*sym_fido_assert_set_rp)(fido_assert_t *, const char *) = NULL; +int (*sym_fido_assert_set_up)(fido_assert_t *, fido_opt_t) = NULL; +size_t (*sym_fido_cbor_info_extensions_len)(const fido_cbor_info_t *) = NULL; +char **(*sym_fido_cbor_info_extensions_ptr)(const fido_cbor_info_t *) = NULL; +void (*sym_fido_cbor_info_free)(fido_cbor_info_t **) = NULL; +fido_cbor_info_t* (*sym_fido_cbor_info_new)(void) = NULL; +void (*sym_fido_cred_free)(fido_cred_t **) = NULL; +size_t (*sym_fido_cred_id_len)(const fido_cred_t *) = NULL; +const unsigned char* (*sym_fido_cred_id_ptr)(const fido_cred_t *) = NULL; +fido_cred_t* (*sym_fido_cred_new)(void) = NULL; +int (*sym_fido_cred_set_clientdata_hash)(fido_cred_t *, const unsigned char *, size_t) = NULL; +int (*sym_fido_cred_set_extensions)(fido_cred_t *, int) = NULL; +int (*sym_fido_cred_set_rk)(fido_cred_t *, fido_opt_t) = NULL; +int (*sym_fido_cred_set_rp)(fido_cred_t *, const char *, const char *) = NULL; +int (*sym_fido_cred_set_type)(fido_cred_t *, int) = NULL; +int (*sym_fido_cred_set_user)(fido_cred_t *, const unsigned char *, size_t, const char *, const char *, const char *) = NULL; +int (*sym_fido_cred_set_uv)(fido_cred_t *, fido_opt_t) = NULL; +void (*sym_fido_dev_free)(fido_dev_t **) = NULL; +int (*sym_fido_dev_get_assert)(fido_dev_t *, fido_assert_t *, const char *) = NULL; +int (*sym_fido_dev_get_cbor_info)(fido_dev_t *, fido_cbor_info_t *) = NULL; +void (*sym_fido_dev_info_free)(fido_dev_info_t **, size_t) = NULL; +int (*sym_fido_dev_info_manifest)(fido_dev_info_t *, size_t, size_t *) = NULL; +const char* (*sym_fido_dev_info_manufacturer_string)(const fido_dev_info_t *) = NULL; +const char* (*sym_fido_dev_info_product_string)(const fido_dev_info_t *) = NULL; +fido_dev_info_t* (*sym_fido_dev_info_new)(size_t) = NULL; +const char* (*sym_fido_dev_info_path)(const fido_dev_info_t *) = NULL; +const fido_dev_info_t* (*sym_fido_dev_info_ptr)(const fido_dev_info_t *, size_t) = NULL; +bool (*sym_fido_dev_is_fido2)(const fido_dev_t *) = NULL; +int (*sym_fido_dev_make_cred)(fido_dev_t *, fido_cred_t *, const char *) = NULL; +fido_dev_t* (*sym_fido_dev_new)(void) = NULL; +int (*sym_fido_dev_open)(fido_dev_t *, const char *) = NULL; +const char* (*sym_fido_strerr)(int) = NULL; + +int dlopen_libfido2(void) { + _cleanup_(dlclosep) void *dl = NULL; + int r; + + if (libfido2_dl) + return 0; /* Already loaded */ + + dl = dlopen("libfido2.so.1", RTLD_LAZY); + if (!dl) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "libfido2 support is not installed: %s", dlerror()); + + r = dlsym_many_and_warn( + dl, + LOG_DEBUG, + DLSYM_ARG(fido_assert_allow_cred), + DLSYM_ARG(fido_assert_free), + DLSYM_ARG(fido_assert_hmac_secret_len), + DLSYM_ARG(fido_assert_hmac_secret_ptr), + DLSYM_ARG(fido_assert_new), + DLSYM_ARG(fido_assert_set_clientdata_hash), + DLSYM_ARG(fido_assert_set_extensions), + DLSYM_ARG(fido_assert_set_hmac_salt), + DLSYM_ARG(fido_assert_set_rp), + DLSYM_ARG(fido_assert_set_up), + DLSYM_ARG(fido_cbor_info_extensions_len), + DLSYM_ARG(fido_cbor_info_extensions_ptr), + DLSYM_ARG(fido_cbor_info_free), + DLSYM_ARG(fido_cbor_info_new), + DLSYM_ARG(fido_cred_free), + DLSYM_ARG(fido_cred_id_len), + DLSYM_ARG(fido_cred_id_ptr), + DLSYM_ARG(fido_cred_new), + DLSYM_ARG(fido_cred_set_clientdata_hash), + DLSYM_ARG(fido_cred_set_extensions), + DLSYM_ARG(fido_cred_set_rk), + DLSYM_ARG(fido_cred_set_rp), + DLSYM_ARG(fido_cred_set_type), + DLSYM_ARG(fido_cred_set_user), + DLSYM_ARG(fido_cred_set_uv), + DLSYM_ARG(fido_dev_free), + DLSYM_ARG(fido_dev_get_assert), + DLSYM_ARG(fido_dev_get_cbor_info), + DLSYM_ARG(fido_dev_info_free), + DLSYM_ARG(fido_dev_info_manifest), + DLSYM_ARG(fido_dev_info_manufacturer_string), + DLSYM_ARG(fido_dev_info_new), + DLSYM_ARG(fido_dev_info_path), + DLSYM_ARG(fido_dev_info_product_string), + DLSYM_ARG(fido_dev_info_ptr), + DLSYM_ARG(fido_dev_is_fido2), + DLSYM_ARG(fido_dev_make_cred), + DLSYM_ARG(fido_dev_new), + DLSYM_ARG(fido_dev_open), + DLSYM_ARG(fido_strerr), + NULL); + if (r < 0) + return r; + + /* Note that we never release the reference here, because there's no real reason to, after all this + * was traditionally a regular shared library dependency which lives forever too. */ + libfido2_dl = TAKE_PTR(dl); + return 1; +} +#endif diff --git a/src/shared/libfido2-util.h b/src/shared/libfido2-util.h new file mode 100644 index 0000000000..baa5ee9212 --- /dev/null +++ b/src/shared/libfido2-util.h @@ -0,0 +1,72 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "macro.h" + +#if HAVE_LIBFIDO2 +#include + +extern int (*sym_fido_assert_allow_cred)(fido_assert_t *, const unsigned char *, size_t); +extern void (*sym_fido_assert_free)(fido_assert_t **); +extern size_t (*sym_fido_assert_hmac_secret_len)(const fido_assert_t *, size_t); +extern const unsigned char* (*sym_fido_assert_hmac_secret_ptr)(const fido_assert_t *, size_t); +extern fido_assert_t* (*sym_fido_assert_new)(void); +extern int (*sym_fido_assert_set_clientdata_hash)(fido_assert_t *, const unsigned char *, size_t); +extern int (*sym_fido_assert_set_extensions)(fido_assert_t *, int); +extern int (*sym_fido_assert_set_hmac_salt)(fido_assert_t *, const unsigned char *, size_t); +extern int (*sym_fido_assert_set_rp)(fido_assert_t *, const char *); +extern int (*sym_fido_assert_set_up)(fido_assert_t *, fido_opt_t); +extern size_t (*sym_fido_cbor_info_extensions_len)(const fido_cbor_info_t *); +extern char **(*sym_fido_cbor_info_extensions_ptr)(const fido_cbor_info_t *); +extern void (*sym_fido_cbor_info_free)(fido_cbor_info_t **); +extern fido_cbor_info_t* (*sym_fido_cbor_info_new)(void); +extern void (*sym_fido_cred_free)(fido_cred_t **); +extern size_t (*sym_fido_cred_id_len)(const fido_cred_t *); +extern const unsigned char* (*sym_fido_cred_id_ptr)(const fido_cred_t *); +extern fido_cred_t* (*sym_fido_cred_new)(void); +extern int (*sym_fido_cred_set_clientdata_hash)(fido_cred_t *, const unsigned char *, size_t); +extern int (*sym_fido_cred_set_extensions)(fido_cred_t *, int); +extern int (*sym_fido_cred_set_rk)(fido_cred_t *, fido_opt_t); +extern int (*sym_fido_cred_set_rp)(fido_cred_t *, const char *, const char *); +extern int (*sym_fido_cred_set_type)(fido_cred_t *, int); +extern int (*sym_fido_cred_set_user)(fido_cred_t *, const unsigned char *, size_t, const char *, const char *, const char *); +extern int (*sym_fido_cred_set_uv)(fido_cred_t *, fido_opt_t); +extern void (*sym_fido_dev_free)(fido_dev_t **); +extern int (*sym_fido_dev_get_assert)(fido_dev_t *, fido_assert_t *, const char *); +extern int (*sym_fido_dev_get_cbor_info)(fido_dev_t *, fido_cbor_info_t *); +extern void (*sym_fido_dev_info_free)(fido_dev_info_t **, size_t); +extern int (*sym_fido_dev_info_manifest)(fido_dev_info_t *, size_t, size_t *); +extern const char* (*sym_fido_dev_info_manufacturer_string)(const fido_dev_info_t *); +extern const char* (*sym_fido_dev_info_product_string)(const fido_dev_info_t *); +extern fido_dev_info_t* (*sym_fido_dev_info_new)(size_t); +extern const char* (*sym_fido_dev_info_path)(const fido_dev_info_t *); +extern const fido_dev_info_t* (*sym_fido_dev_info_ptr)(const fido_dev_info_t *, size_t); +extern bool (*sym_fido_dev_is_fido2)(const fido_dev_t *); +extern int (*sym_fido_dev_make_cred)(fido_dev_t *, fido_cred_t *, const char *); +extern fido_dev_t* (*sym_fido_dev_new)(void); +extern int (*sym_fido_dev_open)(fido_dev_t *, const char *); +extern const char* (*sym_fido_strerr)(int); + +int dlopen_libfido2(void); + +static inline void fido_cbor_info_free_wrapper(fido_cbor_info_t **p) { + if (*p) + sym_fido_cbor_info_free(p); +} + +static inline void fido_assert_free_wrapper(fido_assert_t **p) { + if (*p) + sym_fido_assert_free(p); +} + +static inline void fido_dev_free_wrapper(fido_dev_t **p) { + if (*p) + sym_fido_dev_free(p); +} + +static inline void fido_cred_free_wrapper(fido_cred_t **p) { + if (*p) + sym_fido_cred_free(p); +} + +#endif diff --git a/src/shared/meson.build b/src/shared/meson.build index ebe98df24a..45b8d1b07c 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -146,6 +146,8 @@ shared_sources = files(''' json.h libcrypt-util.c libcrypt-util.h + libfido2-util.c + libfido2-util.h libmount-util.h linux/auto_dev-ioctl.h linux/bpf.h From fb2d839c0660482e6605a6766fb5e67e68f86a9d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 25 Nov 2020 15:15:25 +0100 Subject: [PATCH 13/30] homed: move fido2 device enumeration logic to shared code --- src/home/homectl-fido2.c | 135 ------------------------------------ src/home/homectl-fido2.h | 4 -- src/home/homectl.c | 5 +- src/shared/libfido2-util.c | 137 +++++++++++++++++++++++++++++++++++++ src/shared/libfido2-util.h | 3 + 5 files changed, 143 insertions(+), 141 deletions(-) diff --git a/src/home/homectl-fido2.c b/src/home/homectl-fido2.c index 983891e0e0..99a6bd5d9d 100644 --- a/src/home/homectl-fido2.c +++ b/src/home/homectl-fido2.c @@ -410,138 +410,3 @@ int identity_add_fido2_parameters( "FIDO2 tokens not supported on this build."); #endif } - -int list_fido2_devices(void) { -#if HAVE_LIBFIDO2 - _cleanup_(table_unrefp) Table *t = NULL; - size_t allocated = 64, found = 0; - fido_dev_info_t *di = NULL; - int r; - - r = dlopen_libfido2(); - if (r < 0) - return log_error_errno(r, "FIDO2 token support is not installed."); - - di = sym_fido_dev_info_new(allocated); - if (!di) - return log_oom(); - - r = sym_fido_dev_info_manifest(di, allocated, &found); - if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) { - /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ - log_info("No FIDO2 devices found."); - r = 0; - goto finish; - } - if (r != FIDO_OK) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); - goto finish; - } - - t = table_new("path", "manufacturer", "product"); - if (!t) { - r = log_oom(); - goto finish; - } - - for (size_t i = 0; i < found; i++) { - const fido_dev_info_t *entry; - - entry = sym_fido_dev_info_ptr(di, i); - if (!entry) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to get device information for FIDO device %zu.", i); - goto finish; - } - - r = table_add_many( - t, - TABLE_PATH, sym_fido_dev_info_path(entry), - TABLE_STRING, sym_fido_dev_info_manufacturer_string(entry), - TABLE_STRING, sym_fido_dev_info_product_string(entry)); - if (r < 0) { - table_log_add_error(r); - goto finish; - } - } - - r = table_print(t, stdout); - if (r < 0) { - log_error_errno(r, "Failed to show device table: %m"); - goto finish; - } - - r = 0; - -finish: - sym_fido_dev_info_free(&di, allocated); - return r; -#else - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "FIDO2 tokens not supported on this build."); -#endif -} - -int find_fido2_auto(char **ret) { -#if HAVE_LIBFIDO2 - _cleanup_free_ char *copy = NULL; - size_t di_size = 64, found = 0; - const fido_dev_info_t *entry; - fido_dev_info_t *di = NULL; - const char *path; - int r; - - r = dlopen_libfido2(); - if (r < 0) - return log_error_errno(r, "FIDO2 token support is not installed."); - - di = sym_fido_dev_info_new(di_size); - if (!di) - return log_oom(); - - r = sym_fido_dev_info_manifest(di, di_size, &found); - if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) { - /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ - r = log_error_errno(SYNTHETIC_ERRNO(ENODEV), "No FIDO2 devices found."); - goto finish; - } - if (r != FIDO_OK) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); - goto finish; - } - if (found > 1) { - r = log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "More than one FIDO2 device found."); - goto finish; - } - - entry = sym_fido_dev_info_ptr(di, 0); - if (!entry) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to get device information for FIDO device 0."); - goto finish; - } - - path = sym_fido_dev_info_path(entry); - if (!path) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to query FIDO device path."); - goto finish; - } - - copy = strdup(path); - if (!copy) { - r = log_oom(); - goto finish; - } - - *ret = TAKE_PTR(copy); - r = 0; - -finish: - sym_fido_dev_info_free(&di, di_size); - return r; -#else - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "FIDO2 tokens not supported on this build."); -#endif -} diff --git a/src/home/homectl-fido2.h b/src/home/homectl-fido2.h index d0349f5405..7b8d9f60ac 100644 --- a/src/home/homectl-fido2.h +++ b/src/home/homectl-fido2.h @@ -4,7 +4,3 @@ #include "json.h" int identity_add_fido2_parameters(JsonVariant **v, const char *device); - -int list_fido2_devices(void); - -int find_fido2_auto(char **ret); diff --git a/src/home/homectl.c b/src/home/homectl.c index e10cfaf2a4..c9e54b1e67 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -18,6 +18,7 @@ #include "homectl-fido2.h" #include "homectl-pkcs11.h" #include "homectl-recovery-key.h" +#include "libfido2-util.h" #include "locale-util.h" #include "main-func.h" #include "memory-util.h" @@ -3184,7 +3185,7 @@ static int parse_argv(int argc, char *argv[]) { const char *p; if (streq(optarg, "list")) - return list_fido2_devices(); + return fido2_list_devices(); FOREACH_STRING(p, "fido2HmacCredential", "fido2HmacSalt") { r = drop_from_identity(p); @@ -3200,7 +3201,7 @@ static int parse_argv(int argc, char *argv[]) { if (streq(optarg, "auto")) { _cleanup_free_ char *found = NULL; - r = find_fido2_auto(&found); + r = fido2_find_device_auto(&found); if (r < 0) return r; diff --git a/src/shared/libfido2-util.c b/src/shared/libfido2-util.c index 281eb7537a..8358aa2fe3 100644 --- a/src/shared/libfido2-util.c +++ b/src/shared/libfido2-util.c @@ -5,6 +5,8 @@ #if HAVE_LIBFIDO2 #include "alloc-util.h" #include "dlfcn-util.h" +#include "format-table.h" +#include "locale-util.h" #include "log.h" static void *libfido2_dl = NULL; @@ -115,3 +117,138 @@ int dlopen_libfido2(void) { return 1; } #endif + +int fido2_list_devices(void) { +#if HAVE_LIBFIDO2 + _cleanup_(table_unrefp) Table *t = NULL; + size_t allocated = 64, found = 0; + fido_dev_info_t *di = NULL; + int r; + + r = dlopen_libfido2(); + if (r < 0) + return log_error_errno(r, "FIDO2 token support is not installed."); + + di = sym_fido_dev_info_new(allocated); + if (!di) + return log_oom(); + + r = sym_fido_dev_info_manifest(di, allocated, &found); + if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) { + /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ + log_info("No FIDO2 devices found."); + r = 0; + goto finish; + } + if (r != FIDO_OK) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); + goto finish; + } + + t = table_new("path", "manufacturer", "product"); + if (!t) { + r = log_oom(); + goto finish; + } + + for (size_t i = 0; i < found; i++) { + const fido_dev_info_t *entry; + + entry = sym_fido_dev_info_ptr(di, i); + if (!entry) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to get device information for FIDO device %zu.", i); + goto finish; + } + + r = table_add_many( + t, + TABLE_PATH, sym_fido_dev_info_path(entry), + TABLE_STRING, sym_fido_dev_info_manufacturer_string(entry), + TABLE_STRING, sym_fido_dev_info_product_string(entry)); + if (r < 0) { + table_log_add_error(r); + goto finish; + } + } + + r = table_print(t, stdout); + if (r < 0) { + log_error_errno(r, "Failed to show device table: %m"); + goto finish; + } + + r = 0; + +finish: + sym_fido_dev_info_free(&di, allocated); + return r; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "FIDO2 tokens not supported on this build."); +#endif +} + +int fido2_find_device_auto(char **ret) { +#if HAVE_LIBFIDO2 + _cleanup_free_ char *copy = NULL; + size_t di_size = 64, found = 0; + const fido_dev_info_t *entry; + fido_dev_info_t *di = NULL; + const char *path; + int r; + + r = dlopen_libfido2(); + if (r < 0) + return log_error_errno(r, "FIDO2 token support is not installed."); + + di = sym_fido_dev_info_new(di_size); + if (!di) + return log_oom(); + + r = sym_fido_dev_info_manifest(di, di_size, &found); + if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) { + /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ + r = log_error_errno(SYNTHETIC_ERRNO(ENODEV), "No FIDO devices found."); + goto finish; + } + if (r != FIDO_OK) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO devices: %s", sym_fido_strerr(r)); + goto finish; + } + if (found > 1) { + r = log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "More than one FIDO device found."); + goto finish; + } + + entry = sym_fido_dev_info_ptr(di, 0); + if (!entry) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to get device information for FIDO device 0."); + goto finish; + } + + path = sym_fido_dev_info_path(entry); + if (!path) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to query FIDO device path."); + goto finish; + } + + copy = strdup(path); + if (!copy) { + r = log_oom(); + goto finish; + } + + *ret = TAKE_PTR(copy); + r = 0; + +finish: + sym_fido_dev_info_free(&di, di_size); + return r; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "FIDO2 tokens not supported on this build."); +#endif +} diff --git a/src/shared/libfido2-util.h b/src/shared/libfido2-util.h index baa5ee9212..37b2f921ff 100644 --- a/src/shared/libfido2-util.h +++ b/src/shared/libfido2-util.h @@ -70,3 +70,6 @@ static inline void fido_cred_free_wrapper(fido_cred_t **p) { } #endif + +int fido2_list_devices(void); +int fido2_find_device_auto(char **ret); From 17599e129b63b9dfdcbb1f6d223bbad70c4e7e92 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 25 Nov 2020 16:34:54 +0100 Subject: [PATCH 14/30] homed: move fido2 setup code to src/shared/ That way we can reuse it from systemd-cryptenroll --- src/home/homectl-fido2.c | 278 +++++------------------------------ src/shared/libfido2-util.c | 286 +++++++++++++++++++++++++++++++++++++ src/shared/libfido2-util.h | 14 ++ 3 files changed, 332 insertions(+), 246 deletions(-) diff --git a/src/home/homectl-fido2.c b/src/home/homectl-fido2.c index 99a6bd5d9d..d5edec1bc4 100644 --- a/src/home/homectl-fido2.c +++ b/src/home/homectl-fido2.c @@ -110,109 +110,22 @@ static int add_fido2_salt( } #endif -#define FIDO2_SALT_SIZE 32 - int identity_add_fido2_parameters( JsonVariant **v, const char *device) { #if HAVE_LIBFIDO2 - _cleanup_(fido_cbor_info_free_wrapper) fido_cbor_info_t *di = NULL; - _cleanup_(fido_assert_free_wrapper) fido_assert_t *a = NULL; - _cleanup_(fido_cred_free_wrapper) fido_cred_t *c = NULL; - _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL; - _cleanup_(erase_and_freep) char *used_pin = NULL; - _cleanup_(erase_and_freep) void *salt = NULL; JsonVariant *un, *realm, *rn; - bool found_extension = false; - const void *cid, *secret; + _cleanup_(erase_and_freep) void *secret = NULL, *salt = NULL; + _cleanup_(erase_and_freep) char *used_pin = NULL; + size_t cid_size, salt_size, secret_size; + _cleanup_free_ void *cid = NULL; const char *fido_un; - size_t n, cid_size, secret_size; - char **e; int r; - /* Construction is like this: we generate a salt of 32 bytes. We then ask the FIDO2 device to - * HMAC-SHA256 it for us with its internal key. The result is the key used by LUKS and account - * authentication. LUKS and UNIX password auth all do their own salting before hashing, so that FIDO2 - * device never sees the volume key. - * - * S = HMAC-SHA256(I, D) - * - * with: S → LUKS/account authentication key (never stored) - * I → internal key on FIDO2 device (stored in the FIDO2 device) - * D → salt we generate here (stored in the privileged part of the JSON record) - * - */ - assert(v); assert(device); - r = dlopen_libfido2(); - if (r < 0) - return log_error_errno(r, "FIDO2 token support is not installed."); - - salt = malloc(FIDO2_SALT_SIZE); - if (!salt) - return log_oom(); - - r = genuine_random_bytes(salt, FIDO2_SALT_SIZE, RANDOM_BLOCK); - if (r < 0) - return log_error_errno(r, "Failed to generate salt: %m"); - - d = sym_fido_dev_new(); - if (!d) - return log_oom(); - - r = sym_fido_dev_open(d, device); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to open FIDO2 device %s: %s", device, sym_fido_strerr(r)); - - if (!sym_fido_dev_is_fido2(d)) - return log_error_errno(SYNTHETIC_ERRNO(ENODEV), - "Specified device %s is not a FIDO2 device.", device); - - di = sym_fido_cbor_info_new(); - if (!di) - return log_oom(); - - r = sym_fido_dev_get_cbor_info(d, di); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to get CBOR device info for %s: %s", device, sym_fido_strerr(r)); - - e = sym_fido_cbor_info_extensions_ptr(di); - n = sym_fido_cbor_info_extensions_len(di); - - for (size_t i = 0; i < n; i++) - if (streq(e[i], "hmac-secret")) { - found_extension = true; - break; - } - - if (!found_extension) - return log_error_errno(SYNTHETIC_ERRNO(ENODEV), - "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", device); - - c = sym_fido_cred_new(); - if (!c) - return log_oom(); - - r = sym_fido_cred_set_extensions(c, FIDO_EXT_HMAC_SECRET); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to enable HMAC-SECRET extension on FIDO2 credential: %s", sym_fido_strerr(r)); - - r = sym_fido_cred_set_rp(c, "io.systemd.home", "Home Directory"); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 credential relying party ID/name: %s", sym_fido_strerr(r)); - - r = sym_fido_cred_set_type(c, COSE_ES256); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 credential type to ES256: %s", sym_fido_strerr(r)); - un = json_variant_by_key(*v, "userName"); if (!un) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), @@ -236,164 +149,37 @@ int identity_add_fido2_parameters( return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "realName field of user record is not a string"); - r = sym_fido_cred_set_user(c, - (const unsigned char*) fido_un, strlen(fido_un), /* We pass the user ID and name as the same */ - fido_un, - rn ? json_variant_string(rn) : NULL, - NULL /* icon URL */); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 credential user data: %s", sym_fido_strerr(r)); - - r = sym_fido_cred_set_clientdata_hash(c, (const unsigned char[32]) {}, 32); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 client data hash: %s", sym_fido_strerr(r)); - - r = sym_fido_cred_set_rk(c, FIDO_OPT_FALSE); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to turn off FIDO2 resident key option of credential: %s", sym_fido_strerr(r)); - - r = sym_fido_cred_set_uv(c, FIDO_OPT_FALSE); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to turn off FIDO2 user verification option of credential: %s", sym_fido_strerr(r)); - - log_info("Initializing FIDO2 credential on security token."); - - log_notice("%s%s(Hint: This might require verification of user presence on security token.)", - emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", - emoji_enabled() ? " " : ""); - - r = sym_fido_dev_make_cred(d, c, NULL); - if (r == FIDO_ERR_PIN_REQUIRED) { - _cleanup_free_ char *text = NULL; - - if (asprintf(&text, "Please enter security token PIN:") < 0) - return log_oom(); - - for (;;) { - _cleanup_(strv_free_erasep) char **pin = NULL; - char **i; - - r = ask_password_auto(text, "user-home", NULL, "fido2-pin", USEC_INFINITY, 0, &pin); - if (r < 0) - return log_error_errno(r, "Failed to acquire user PIN: %m"); - - r = FIDO_ERR_PIN_INVALID; - STRV_FOREACH(i, pin) { - if (isempty(*i)) { - log_info("PIN may not be empty."); - continue; - } - - r = sym_fido_dev_make_cred(d, c, *i); - if (r == FIDO_OK) { - used_pin = strdup(*i); - if (!used_pin) - return log_oom(); - break; - } - if (r != FIDO_ERR_PIN_INVALID) - break; - } - - if (r != FIDO_ERR_PIN_INVALID) - break; - - log_notice("PIN incorrect, please try again."); - } - } - if (r == FIDO_ERR_PIN_AUTH_BLOCKED) - return log_notice_errno(SYNTHETIC_ERRNO(EPERM), - "Token PIN is currently blocked, please remove and reinsert token."); - if (r == FIDO_ERR_ACTION_TIMEOUT) - return log_error_errno(SYNTHETIC_ERRNO(ENOSTR), - "Token action timeout. (User didn't interact with token quickly enough.)"); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to generate FIDO2 credential: %s", sym_fido_strerr(r)); - - cid = sym_fido_cred_id_ptr(c); - if (!cid) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get FIDO2 credential ID."); - - cid_size = sym_fido_cred_id_len(c); - - a = sym_fido_assert_new(); - if (!a) - return log_oom(); - - r = sym_fido_assert_set_extensions(a, FIDO_EXT_HMAC_SECRET); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to enable HMAC-SECRET extension on FIDO2 assertion: %s", sym_fido_strerr(r)); - - r = sym_fido_assert_set_hmac_salt(a, salt, FIDO2_SALT_SIZE); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set salt on FIDO2 assertion: %s", sym_fido_strerr(r)); - - r = sym_fido_assert_set_rp(a, "io.systemd.home"); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 assertion ID: %s", sym_fido_strerr(r)); - - r = sym_fido_assert_set_clientdata_hash(a, (const unsigned char[32]) {}, 32); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 assertion client data hash: %s", sym_fido_strerr(r)); - - r = sym_fido_assert_allow_cred(a, cid, cid_size); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to add FIDO2 assertion credential ID: %s", sym_fido_strerr(r)); - - r = sym_fido_assert_set_up(a, FIDO_OPT_FALSE); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to turn off FIDO2 assertion user presence: %s", sym_fido_strerr(r)); - - log_info("Generating secret key on FIDO2 security token."); - - r = sym_fido_dev_get_assert(d, a, used_pin); - if (r == FIDO_ERR_UP_REQUIRED) { - r = sym_fido_assert_set_up(a, FIDO_OPT_TRUE); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to turn on FIDO2 assertion user presence: %s", sym_fido_strerr(r)); - - log_notice("%s%sIn order to allow secret key generation, please verify presence on security token.", - emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", - emoji_enabled() ? " " : ""); - - r = sym_fido_dev_get_assert(d, a, used_pin); - } - if (r == FIDO_ERR_ACTION_TIMEOUT) - return log_error_errno(SYNTHETIC_ERRNO(ENOSTR), - "Token action timeout. (User didn't interact with token quickly enough.)"); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to ask token for assertion: %s", sym_fido_strerr(r)); - - secret = sym_fido_assert_hmac_secret_ptr(a, 0); - if (!secret) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve HMAC secret."); - - secret_size = sym_fido_assert_hmac_secret_len(a, 0); - - r = add_fido2_credential_id(v, cid, cid_size); + r = fido2_generate_hmac_hash( + device, + /* rp_id= */ "io.systemd.home", + /* rp_name= */ "Home Directory", + /* user_id= */ fido_un, strlen(fido_un), /* We pass the user ID and name as the same */ + /* user_name= */ fido_un, + /* user_display_name= */ rn ? json_variant_string(rn) : NULL, + /* user_icon_name= */ NULL, + /* askpw_icon_name= */ "user-home", + &cid, &cid_size, + &salt, &salt_size, + &secret, &secret_size, + &used_pin); if (r < 0) return r; - r = add_fido2_salt(v, - cid, - cid_size, - salt, - FIDO2_SALT_SIZE, - secret, - secret_size); + r = add_fido2_credential_id( + v, + cid, + cid_size); + if (r < 0) + return r; + + r = add_fido2_salt( + v, + cid, + cid_size, + salt, + salt_size, + secret, + secret_size); if (r < 0) return r; diff --git a/src/shared/libfido2-util.c b/src/shared/libfido2-util.c index 8358aa2fe3..5438ffc369 100644 --- a/src/shared/libfido2-util.c +++ b/src/shared/libfido2-util.c @@ -4,10 +4,14 @@ #if HAVE_LIBFIDO2 #include "alloc-util.h" +#include "ask-password-api.h" #include "dlfcn-util.h" #include "format-table.h" #include "locale-util.h" #include "log.h" +#include "memory-util.h" +#include "random-util.h" +#include "strv.h" static void *libfido2_dl = NULL; @@ -116,6 +120,288 @@ int dlopen_libfido2(void) { libfido2_dl = TAKE_PTR(dl); return 1; } + +#define FIDO2_SALT_SIZE 32 + +int fido2_generate_hmac_hash( + const char *device, + const char *rp_id, + const char *rp_name, + const void *user_id, size_t user_id_len, + const char *user_name, + const char *user_display_name, + const char *user_icon, + const char *askpw_icon_name, + void **ret_cid, size_t *ret_cid_size, + void **ret_salt, size_t *ret_salt_size, + void **ret_secret, size_t *ret_secret_size, + char **ret_usedpin) { + + _cleanup_(fido_cbor_info_free_wrapper) fido_cbor_info_t *di = NULL; + _cleanup_(erase_and_freep) void *salt = NULL, *secret_copy = NULL; + _cleanup_(fido_assert_free_wrapper) fido_assert_t *a = NULL; + _cleanup_(fido_cred_free_wrapper) fido_cred_t *c = NULL; + _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL; + _cleanup_(erase_and_freep) char *used_pin = NULL; + _cleanup_free_ char *cid_copy = NULL; + size_t n, cid_size, secret_size; + bool found_extension = false; + const void *cid, *secret; + char **e; + int r; + + assert(device); + assert(ret_cid); + assert(ret_cid_size); + assert(ret_salt); + assert(ret_salt_size); + assert(ret_secret); + assert(ret_secret_size); + + /* Construction is like this: we generate a salt of 32 bytes. We then ask the FIDO2 device to + * HMAC-SHA256 it for us with its internal key. The result is the key used by LUKS and account + * authentication. LUKS and UNIX password auth all do their own salting before hashing, so that FIDO2 + * device never sees the volume key. + * + * S = HMAC-SHA256(I, D) + * + * with: S → LUKS/account authentication key (never stored) + * I → internal key on FIDO2 device (stored in the FIDO2 device) + * D → salt we generate here (stored in the privileged part of the JSON record) + * + */ + + assert(device); + + r = dlopen_libfido2(); + if (r < 0) + return log_error_errno(r, "FIDO2 token support is not installed."); + + salt = malloc(FIDO2_SALT_SIZE); + if (!salt) + return log_oom(); + + r = genuine_random_bytes(salt, FIDO2_SALT_SIZE, RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Failed to generate salt: %m"); + + d = sym_fido_dev_new(); + if (!d) + return log_oom(); + + r = sym_fido_dev_open(d, device); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to open FIDO2 device %s: %s", device, sym_fido_strerr(r)); + + if (!sym_fido_dev_is_fido2(d)) + return log_error_errno(SYNTHETIC_ERRNO(ENODEV), + "Specified device %s is not a FIDO2 device.", device); + + di = sym_fido_cbor_info_new(); + if (!di) + return log_oom(); + + r = sym_fido_dev_get_cbor_info(d, di); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to get CBOR device info for %s: %s", device, sym_fido_strerr(r)); + + e = sym_fido_cbor_info_extensions_ptr(di); + n = sym_fido_cbor_info_extensions_len(di); + + for (size_t i = 0; i < n; i++) + if (streq(e[i], "hmac-secret")) { + found_extension = true; + break; + } + + if (!found_extension) + return log_error_errno(SYNTHETIC_ERRNO(ENODEV), + "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", device); + + c = sym_fido_cred_new(); + if (!c) + return log_oom(); + + r = sym_fido_cred_set_extensions(c, FIDO_EXT_HMAC_SECRET); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to enable HMAC-SECRET extension on FIDO2 credential: %s", sym_fido_strerr(r)); + + r = sym_fido_cred_set_rp(c, rp_id, rp_name); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to set FIDO2 credential relying party ID/name: %s", sym_fido_strerr(r)); + + r = sym_fido_cred_set_type(c, COSE_ES256); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to set FIDO2 credential type to ES256: %s", sym_fido_strerr(r)); + + r = sym_fido_cred_set_user( + c, + user_id, user_id_len, + user_name, + user_display_name, + user_icon); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to set FIDO2 credential user data: %s", sym_fido_strerr(r)); + + r = sym_fido_cred_set_clientdata_hash(c, (const unsigned char[32]) {}, 32); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to set FIDO2 client data hash: %s", sym_fido_strerr(r)); + + r = sym_fido_cred_set_rk(c, FIDO_OPT_FALSE); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to turn off FIDO2 resident key option of credential: %s", sym_fido_strerr(r)); + + r = sym_fido_cred_set_uv(c, FIDO_OPT_FALSE); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to turn off FIDO2 user verification option of credential: %s", sym_fido_strerr(r)); + + log_info("Initializing FIDO2 credential on security token."); + + log_notice("%s%s(Hint: This might require verification of user presence on security token.)", + emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", + emoji_enabled() ? " " : ""); + + r = sym_fido_dev_make_cred(d, c, NULL); + if (r == FIDO_ERR_PIN_REQUIRED) { + for (;;) { + _cleanup_(strv_free_erasep) char **pin = NULL; + char **i; + + r = ask_password_auto("Please enter security token PIN:", askpw_icon_name, NULL, "fido2-pin", USEC_INFINITY, 0, &pin); + if (r < 0) + return log_error_errno(r, "Failed to acquire user PIN: %m"); + + r = FIDO_ERR_PIN_INVALID; + STRV_FOREACH(i, pin) { + if (isempty(*i)) { + log_info("PIN may not be empty."); + continue; + } + + r = sym_fido_dev_make_cred(d, c, *i); + if (r == FIDO_OK) { + used_pin = strdup(*i); + if (!used_pin) + return log_oom(); + break; + } + if (r != FIDO_ERR_PIN_INVALID) + break; + } + + if (r != FIDO_ERR_PIN_INVALID) + break; + + log_notice("PIN incorrect, please try again."); + } + } + if (r == FIDO_ERR_PIN_AUTH_BLOCKED) + return log_notice_errno(SYNTHETIC_ERRNO(EPERM), + "Token PIN is currently blocked, please remove and reinsert token."); + if (r == FIDO_ERR_ACTION_TIMEOUT) + return log_error_errno(SYNTHETIC_ERRNO(ENOSTR), + "Token action timeout. (User didn't interact with token quickly enough.)"); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to generate FIDO2 credential: %s", sym_fido_strerr(r)); + + cid = sym_fido_cred_id_ptr(c); + if (!cid) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get FIDO2 credential ID."); + + cid_size = sym_fido_cred_id_len(c); + + a = sym_fido_assert_new(); + if (!a) + return log_oom(); + + r = sym_fido_assert_set_extensions(a, FIDO_EXT_HMAC_SECRET); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to enable HMAC-SECRET extension on FIDO2 assertion: %s", sym_fido_strerr(r)); + + r = sym_fido_assert_set_hmac_salt(a, salt, FIDO2_SALT_SIZE); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to set salt on FIDO2 assertion: %s", sym_fido_strerr(r)); + + r = sym_fido_assert_set_rp(a, rp_id); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to set FIDO2 assertion ID: %s", sym_fido_strerr(r)); + + r = sym_fido_assert_set_clientdata_hash(a, (const unsigned char[32]) {}, 32); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to set FIDO2 assertion client data hash: %s", sym_fido_strerr(r)); + + r = sym_fido_assert_allow_cred(a, cid, cid_size); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to add FIDO2 assertion credential ID: %s", sym_fido_strerr(r)); + + r = sym_fido_assert_set_up(a, FIDO_OPT_FALSE); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to turn off FIDO2 assertion user presence: %s", sym_fido_strerr(r)); + + log_info("Generating secret key on FIDO2 security token."); + + r = sym_fido_dev_get_assert(d, a, used_pin); + if (r == FIDO_ERR_UP_REQUIRED) { + r = sym_fido_assert_set_up(a, FIDO_OPT_TRUE); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to turn on FIDO2 assertion user presence: %s", sym_fido_strerr(r)); + + log_notice("%s%sIn order to allow secret key generation, please verify presence on security token.", + emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", + emoji_enabled() ? " " : ""); + + r = sym_fido_dev_get_assert(d, a, used_pin); + } + if (r == FIDO_ERR_ACTION_TIMEOUT) + return log_error_errno(SYNTHETIC_ERRNO(ENOSTR), + "Token action timeout. (User didn't interact with token quickly enough.)"); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to ask token for assertion: %s", sym_fido_strerr(r)); + + secret = sym_fido_assert_hmac_secret_ptr(a, 0); + if (!secret) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve HMAC secret."); + + secret_size = sym_fido_assert_hmac_secret_len(a, 0); + + secret_copy = memdup(secret, secret_size); + if (!secret_copy) + return log_oom(); + + cid_copy = memdup(cid, cid_size); + if (!cid_copy) + return log_oom(); + + *ret_cid = TAKE_PTR(cid_copy); + *ret_cid_size = cid_size; + *ret_salt = TAKE_PTR(salt); + *ret_salt_size = FIDO2_SALT_SIZE; + *ret_secret = TAKE_PTR(secret_copy); + *ret_secret_size = secret_size; + + if (ret_usedpin) + *ret_usedpin = TAKE_PTR(used_pin); + + return 0; +} #endif int fido2_list_devices(void) { diff --git a/src/shared/libfido2-util.h b/src/shared/libfido2-util.h index 37b2f921ff..869168ee7b 100644 --- a/src/shared/libfido2-util.h +++ b/src/shared/libfido2-util.h @@ -69,6 +69,20 @@ static inline void fido_cred_free_wrapper(fido_cred_t **p) { sym_fido_cred_free(p); } +int fido2_generate_hmac_hash( + const char *device, + const char *rp_id, + const char *rp_name, + const void *user_id, size_t user_id_len, + const char *user_name, + const char *user_display_name, + const char *user_icon, + const char *askpw_icon_name, + void **ret_cid, size_t *ret_cid_size, + void **ret_salt, size_t *ret_salt_size, + void **ret_secret, size_t *ret_secret_size, + char **ret_usedpin); + #endif int fido2_list_devices(void); From ebcb3f38d29418a10e408e983e5e81abcaeb178d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 26 Nov 2020 13:30:24 +0100 Subject: [PATCH 15/30] homed: split out HMAC-HASH fido2 decode code into src/shared/ That way we can use it later on in systemd-cryptsetup to unlock devices with FIDO2 tokens. --- src/home/homework-fido2.c | 198 +++---------------------------- src/shared/libfido2-util.c | 233 +++++++++++++++++++++++++++++++++++++ src/shared/libfido2-util.h | 12 ++ 3 files changed, 264 insertions(+), 179 deletions(-) diff --git a/src/home/homework-fido2.c b/src/home/homework-fido2.c index 7c21dd34f3..87d301c5b4 100644 --- a/src/home/homework-fido2.c +++ b/src/home/homework-fido2.c @@ -5,138 +5,34 @@ #include "hexdecoct.h" #include "homework-fido2.h" #include "libfido2-util.h" -#include "strv.h" +#include "memory-util.h" -static int fido2_use_specific_token( - const char *path, +int fido2_use_token( UserRecord *h, UserRecord *secret, const Fido2HmacSalt *salt, char **ret) { - _cleanup_(fido_cbor_info_free_wrapper) fido_cbor_info_t *di = NULL; - _cleanup_(fido_assert_free_wrapper) fido_assert_t *a = NULL; - _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL; - bool found_extension = false; - size_t n, hmac_size; - const void *hmac; - char **e; + _cleanup_(erase_and_freep) void *hmac = NULL; + size_t hmac_size; int r; - d = sym_fido_dev_new(); - if (!d) - return log_oom(); + assert(h); + assert(secret); + assert(salt); + assert(ret); - r = sym_fido_dev_open(d, path); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to open FIDO2 device %s: %s", path, sym_fido_strerr(r)); - - if (!sym_fido_dev_is_fido2(d)) - return log_error_errno(SYNTHETIC_ERRNO(ENODEV), - "Specified device %s is not a FIDO2 device.", path); - - di = sym_fido_cbor_info_new(); - if (!di) - return log_oom(); - - r = sym_fido_dev_get_cbor_info(d, di); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to get CBOR device info for %s: %s", path, sym_fido_strerr(r)); - - e = sym_fido_cbor_info_extensions_ptr(di); - n = sym_fido_cbor_info_extensions_len(di); - - for (size_t i = 0; i < n; i++) - if (streq(e[i], "hmac-secret")) { - found_extension = true; - break; - } - - if (!found_extension) - return log_error_errno(SYNTHETIC_ERRNO(ENODEV), - "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", path); - - a = sym_fido_assert_new(); - if (!a) - return log_oom(); - - r = sym_fido_assert_set_extensions(a, FIDO_EXT_HMAC_SECRET); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to enable HMAC-SECRET extension on FIDO2 assertion: %s", sym_fido_strerr(r)); - - r = sym_fido_assert_set_hmac_salt(a, salt->salt, salt->salt_size); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set salt on FIDO2 assertion: %s", sym_fido_strerr(r)); - - r = sym_fido_assert_set_rp(a, "io.systemd.home"); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 assertion ID: %s", sym_fido_strerr(r)); - - r = sym_fido_assert_set_clientdata_hash(a, (const unsigned char[32]) {}, 32); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 assertion client data hash: %s", sym_fido_strerr(r)); - - r = sym_fido_assert_allow_cred(a, salt->credential.id, salt->credential.size); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to add FIDO2 assertion credential ID: %s", sym_fido_strerr(r)); - - r = sym_fido_assert_set_up(a, h->fido2_user_presence_permitted <= 0 ? FIDO_OPT_FALSE : FIDO_OPT_TRUE); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 assertion user presence: %s", sym_fido_strerr(r)); - - log_info("Asking FIDO2 token for authentication."); - - r = sym_fido_dev_get_assert(d, a, NULL); /* try without pin first */ - if (r == FIDO_ERR_PIN_REQUIRED) { - char **i; - - /* OK, we needed a pin, try with all pins in turn */ - STRV_FOREACH(i, secret->token_pin) { - r = sym_fido_dev_get_assert(d, a, *i); - if (r != FIDO_ERR_PIN_INVALID) - break; - } - } - - switch (r) { - case FIDO_OK: - break; - case FIDO_ERR_NO_CREDENTIALS: - return log_error_errno(SYNTHETIC_ERRNO(EBADSLT), - "Wrong security token; needed credentials not present on token."); - case FIDO_ERR_PIN_REQUIRED: - return log_error_errno(SYNTHETIC_ERRNO(ENOANO), - "Security token requires PIN."); - case FIDO_ERR_PIN_AUTH_BLOCKED: - return log_error_errno(SYNTHETIC_ERRNO(EOWNERDEAD), - "PIN of security token is blocked, please remove/reinsert token."); - case FIDO_ERR_PIN_INVALID: - return log_error_errno(SYNTHETIC_ERRNO(ENOLCK), - "PIN of security token incorrect."); - case FIDO_ERR_UP_REQUIRED: - return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), - "User presence required."); - case FIDO_ERR_ACTION_TIMEOUT: - return log_error_errno(SYNTHETIC_ERRNO(ENOSTR), - "Token action timeout. (User didn't interact with token quickly enough.)"); - default: - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to ask token for assertion: %s", sym_fido_strerr(r)); - } - - hmac = sym_fido_assert_hmac_secret_ptr(a, 0); - if (!hmac) - return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve HMAC secret."); - - hmac_size = sym_fido_assert_hmac_secret_len(a, 0); + r = fido2_use_hmac_hash( + NULL, + "io.systemd.home", + salt->salt, salt->salt_size, + salt->credential.id, salt->credential.size, + secret->token_pin, + h->fido2_user_presence_permitted > 0, + &hmac, + &hmac_size); + if (r < 0) + return r; r = base64mem(hmac, hmac_size, ret); if (r < 0) @@ -144,59 +40,3 @@ static int fido2_use_specific_token( return 0; } - -int fido2_use_token(UserRecord *h, UserRecord *secret, const Fido2HmacSalt *salt, char **ret) { - size_t allocated = 64, found = 0; - fido_dev_info_t *di = NULL; - int r; - - r = dlopen_libfido2(); - if (r < 0) - return log_error_errno(r, "FIDO2 support is not installed."); - - di = sym_fido_dev_info_new(allocated); - if (!di) - return log_oom(); - - r = sym_fido_dev_info_manifest(di, allocated, &found); - if (r == FIDO_ERR_INTERNAL) { - /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ - r = log_debug_errno(SYNTHETIC_ERRNO(EAGAIN), "Got FIDO_ERR_INTERNAL, assuming no devices."); - goto finish; - } - if (r != FIDO_OK) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); - goto finish; - } - - for (size_t i = 0; i < found; i++) { - const fido_dev_info_t *entry; - const char *path; - - entry = sym_fido_dev_info_ptr(di, i); - if (!entry) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to get device information for FIDO device %zu.", i); - goto finish; - } - - path = sym_fido_dev_info_path(entry); - if (!path) { - r = log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to query FIDO device path."); - goto finish; - } - - r = fido2_use_specific_token(path, h, secret, salt, ret); - if (!IN_SET(r, - -EBADSLT, /* device doesn't understand our credential hash */ - -ENODEV /* device is not a FIDO2 device with HMAC-SECRET */)) - goto finish; - } - - r = -EAGAIN; - -finish: - sym_fido_dev_info_free(&di, allocated); - return r; -} diff --git a/src/shared/libfido2-util.c b/src/shared/libfido2-util.c index 5438ffc369..01e060114c 100644 --- a/src/shared/libfido2-util.c +++ b/src/shared/libfido2-util.c @@ -121,6 +121,239 @@ int dlopen_libfido2(void) { return 1; } +static int fido2_use_hmac_hash_specific_token( + const char *path, + const char *rp_id, + const void *salt, + size_t salt_size, + const void *cid, + size_t cid_size, + char **pins, + bool up, /* user presence permitted */ + void **ret_hmac, + size_t *ret_hmac_size) { + + _cleanup_(fido_cbor_info_free_wrapper) fido_cbor_info_t *di = NULL; + _cleanup_(fido_assert_free_wrapper) fido_assert_t *a = NULL; + _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL; + _cleanup_(erase_and_freep) void *hmac_copy = NULL; + bool found_extension = false; + size_t n, hmac_size; + const void *hmac; + char **e; + int r; + + assert(path); + assert(rp_id); + assert(salt); + assert(cid); + assert(ret_hmac); + assert(ret_hmac_size); + + d = sym_fido_dev_new(); + if (!d) + return log_oom(); + + r = sym_fido_dev_open(d, path); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to open FIDO2 device %s: %s", path, sym_fido_strerr(r)); + + if (!sym_fido_dev_is_fido2(d)) + return log_error_errno(SYNTHETIC_ERRNO(ENODEV), + "Specified device %s is not a FIDO2 device.", path); + + di = sym_fido_cbor_info_new(); + if (!di) + return log_oom(); + + r = sym_fido_dev_get_cbor_info(d, di); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to get CBOR device info for %s: %s", path, sym_fido_strerr(r)); + + e = sym_fido_cbor_info_extensions_ptr(di); + n = sym_fido_cbor_info_extensions_len(di); + + for (size_t i = 0; i < n; i++) + if (streq(e[i], "hmac-secret")) { + found_extension = true; + break; + } + + if (!found_extension) + return log_error_errno(SYNTHETIC_ERRNO(ENODEV), + "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", path); + + a = sym_fido_assert_new(); + if (!a) + return log_oom(); + + r = sym_fido_assert_set_extensions(a, FIDO_EXT_HMAC_SECRET); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to enable HMAC-SECRET extension on FIDO2 assertion: %s", sym_fido_strerr(r)); + + r = sym_fido_assert_set_hmac_salt(a, salt, salt_size); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to set salt on FIDO2 assertion: %s", sym_fido_strerr(r)); + + r = sym_fido_assert_set_rp(a, rp_id); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to set FIDO2 assertion ID: %s", sym_fido_strerr(r)); + + r = sym_fido_assert_set_clientdata_hash(a, (const unsigned char[32]) {}, 32); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to set FIDO2 assertion client data hash: %s", sym_fido_strerr(r)); + + r = sym_fido_assert_allow_cred(a, cid, cid_size); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to add FIDO2 assertion credential ID: %s", sym_fido_strerr(r)); + + r = sym_fido_assert_set_up(a, FIDO_OPT_FALSE); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to set FIDO2 assertion user presence: %s", sym_fido_strerr(r)); + + log_info("Asking FIDO2 token for authentication."); + + r = sym_fido_dev_get_assert(d, a, NULL); /* try without pin and without up first */ + if (r == FIDO_ERR_UP_REQUIRED && up) { + r = sym_fido_assert_set_up(a, FIDO_OPT_TRUE); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to set FIDO2 assertion user presence: %s", sym_fido_strerr(r)); + + log_info("Security token requires user presence."); + + r = sym_fido_dev_get_assert(d, a, NULL); /* try without pin but with up now */ + } + if (r == FIDO_ERR_PIN_REQUIRED) { + char **i; + + /* OK, we needed a pin, try with all pins in turn */ + STRV_FOREACH(i, pins) { + r = sym_fido_dev_get_assert(d, a, *i); + if (r != FIDO_ERR_PIN_INVALID) + break; + } + } + + switch (r) { + case FIDO_OK: + break; + case FIDO_ERR_NO_CREDENTIALS: + return log_error_errno(SYNTHETIC_ERRNO(EBADSLT), + "Wrong security token; needed credentials not present on token."); + case FIDO_ERR_PIN_REQUIRED: + return log_error_errno(SYNTHETIC_ERRNO(ENOANO), + "Security token requires PIN."); + case FIDO_ERR_PIN_AUTH_BLOCKED: + return log_error_errno(SYNTHETIC_ERRNO(EOWNERDEAD), + "PIN of security token is blocked, please remove/reinsert token."); + case FIDO_ERR_PIN_INVALID: + return log_error_errno(SYNTHETIC_ERRNO(ENOLCK), + "PIN of security token incorrect."); + case FIDO_ERR_UP_REQUIRED: + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), + "User presence required."); + case FIDO_ERR_ACTION_TIMEOUT: + return log_error_errno(SYNTHETIC_ERRNO(ENOSTR), + "Token action timeout. (User didn't interact with token quickly enough.)"); + default: + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to ask token for assertion: %s", sym_fido_strerr(r)); + } + + hmac = sym_fido_assert_hmac_secret_ptr(a, 0); + if (!hmac) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve HMAC secret."); + + hmac_size = sym_fido_assert_hmac_secret_len(a, 0); + + hmac_copy = memdup(hmac, hmac_size); + if (!hmac_copy) + return log_oom(); + + *ret_hmac = TAKE_PTR(hmac_copy); + *ret_hmac_size = hmac_size; + return 0; +} + +int fido2_use_hmac_hash( + const char *device, + const char *rp_id, + const void *salt, + size_t salt_size, + const void *cid, + size_t cid_size, + char **pins, + bool up, /* user presence permitted */ + void **ret_hmac, + size_t *ret_hmac_size) { + + size_t allocated = 64, found = 0; + fido_dev_info_t *di = NULL; + int r; + + r = dlopen_libfido2(); + if (r < 0) + return log_error_errno(r, "FIDO2 support is not installed."); + + if (device) + return fido2_use_hmac_hash_specific_token(device, rp_id, salt, salt_size, cid, cid_size, pins, up, ret_hmac, ret_hmac_size); + + di = sym_fido_dev_info_new(allocated); + if (!di) + return log_oom(); + + r = sym_fido_dev_info_manifest(di, allocated, &found); + if (r == FIDO_ERR_INTERNAL) { + /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ + r = log_debug_errno(SYNTHETIC_ERRNO(EAGAIN), "Got FIDO_ERR_INTERNAL, assuming no devices."); + goto finish; + } + if (r != FIDO_OK) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); + goto finish; + } + + for (size_t i = 0; i < found; i++) { + const fido_dev_info_t *entry; + const char *path; + + entry = sym_fido_dev_info_ptr(di, i); + if (!entry) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to get device information for FIDO device %zu.", i); + goto finish; + } + + path = sym_fido_dev_info_path(entry); + if (!path) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to query FIDO device path."); + goto finish; + } + + r = fido2_use_hmac_hash_specific_token(path, rp_id, salt, salt_size, cid, cid_size, pins, up, ret_hmac, ret_hmac_size); + if (!IN_SET(r, + -EBADSLT, /* device doesn't understand our credential hash */ + -ENODEV /* device is not a FIDO2 device with HMAC-SECRET */)) + goto finish; + } + + r = -EAGAIN; + +finish: + sym_fido_dev_info_free(&di, allocated); + return r; +} + #define FIDO2_SALT_SIZE 32 int fido2_generate_hmac_hash( diff --git a/src/shared/libfido2-util.h b/src/shared/libfido2-util.h index 869168ee7b..c80554c1b4 100644 --- a/src/shared/libfido2-util.h +++ b/src/shared/libfido2-util.h @@ -69,6 +69,18 @@ static inline void fido_cred_free_wrapper(fido_cred_t **p) { sym_fido_cred_free(p); } +int fido2_use_hmac_hash( + const char *device, + const char *rp_id, + const void *salt, + size_t salt_size, + const void *cid, + size_t cid_size, + char **pins, + bool up, /* user presence permitted */ + void **ret_hmac, + size_t *ret_hmac_size); + int fido2_generate_hmac_hash( const char *device, const char *rp_id, From e3fb662b6710134e3d52ea46ef2eb2c412c377c4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 4 Dec 2020 10:19:47 +0100 Subject: [PATCH 16/30] fido2: don't use up/uv/rk when device doesn't support it Apparently devices are supposed to generate failures if we try to turn off features they don't have. Thus don't. Prompted-by: https://github.com/systemd/systemd/issues/17784#issuecomment-737730395 --- src/shared/libfido2-util.c | 208 ++++++++++++++++++++++++------------- src/shared/libfido2-util.h | 3 + 2 files changed, 137 insertions(+), 74 deletions(-) diff --git a/src/shared/libfido2-util.c b/src/shared/libfido2-util.c index 01e060114c..879429f63c 100644 --- a/src/shared/libfido2-util.c +++ b/src/shared/libfido2-util.c @@ -29,6 +29,9 @@ size_t (*sym_fido_cbor_info_extensions_len)(const fido_cbor_info_t *) = NULL; char **(*sym_fido_cbor_info_extensions_ptr)(const fido_cbor_info_t *) = NULL; void (*sym_fido_cbor_info_free)(fido_cbor_info_t **) = NULL; fido_cbor_info_t* (*sym_fido_cbor_info_new)(void) = NULL; +size_t (*sym_fido_cbor_info_options_len)(const fido_cbor_info_t *) = NULL; +char** (*sym_fido_cbor_info_options_name_ptr)(const fido_cbor_info_t *) = NULL; +const bool* (*sym_fido_cbor_info_options_value_ptr)(const fido_cbor_info_t *) = NULL; void (*sym_fido_cred_free)(fido_cred_t **) = NULL; size_t (*sym_fido_cred_id_len)(const fido_cred_t *) = NULL; const unsigned char* (*sym_fido_cred_id_ptr)(const fido_cred_t *) = NULL; @@ -85,6 +88,9 @@ int dlopen_libfido2(void) { DLSYM_ARG(fido_cbor_info_extensions_ptr), DLSYM_ARG(fido_cbor_info_free), DLSYM_ARG(fido_cbor_info_new), + DLSYM_ARG(fido_cbor_info_options_len), + DLSYM_ARG(fido_cbor_info_options_name_ptr), + DLSYM_ARG(fido_cbor_info_options_value_ptr), DLSYM_ARG(fido_cred_free), DLSYM_ARG(fido_cred_id_len), DLSYM_ARG(fido_cred_id_ptr), @@ -121,6 +127,86 @@ int dlopen_libfido2(void) { return 1; } +static int verify_features( + fido_dev_t *d, + const char *path, + bool *ret_has_rk, + bool *ret_has_client_pin, + bool *ret_has_up, + bool *ret_has_uv) { + + _cleanup_(fido_cbor_info_free_wrapper) fido_cbor_info_t *di = NULL; + bool found_extension = false; + char **e, **o; + const bool *b; + bool has_rk = false, has_client_pin = false, has_up = true, has_uv = false; /* Defaults are per table in 5.4 in FIDO2 spec */ + size_t n; + int r; + + assert(d); + assert(path); + + if (!sym_fido_dev_is_fido2(d)) + return log_error_errno(SYNTHETIC_ERRNO(ENODEV), + "Specified device %s is not a FIDO2 device.", path); + + di = sym_fido_cbor_info_new(); + if (!di) + return log_oom(); + + r = sym_fido_dev_get_cbor_info(d, di); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to get CBOR device info for %s: %s", path, sym_fido_strerr(r)); + + e = sym_fido_cbor_info_extensions_ptr(di); + n = sym_fido_cbor_info_extensions_len(di); + for (size_t i = 0; i < n; i++) { + log_debug("FIDO2 device implements extension: %s", e[i]); + if (streq(e[i], "hmac-secret")) + found_extension = true; + } + + o = sym_fido_cbor_info_options_name_ptr(di); + b = sym_fido_cbor_info_options_value_ptr(di); + n = sym_fido_cbor_info_options_len(di); + for (size_t i = 0; i < n; i++) { + log_debug("FIDO2 device implements option %s: %s", o[i], yes_no(b[i])); + if (streq(o[i], "rk")) + has_rk = b[i]; + if (streq(o[i], "clientPin")) + has_client_pin = b[i]; + if (streq(o[i], "up")) + has_up = b[i]; + if (streq(o[i], "uv")) + has_uv = b[i]; + } + + if (!found_extension) + return log_error_errno(SYNTHETIC_ERRNO(ENODEV), + "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", path); + + log_debug("Has rk ('Resident Key') support: %s\n" + "Has clientPin support: %s\n" + "Has up ('User Presence') support: %s\n" + "Has uv ('User Verification') support: %s\n", + yes_no(has_rk), + yes_no(has_client_pin), + yes_no(has_up), + yes_no(has_uv)); + + if (ret_has_rk) + *ret_has_rk = has_rk; + if (ret_has_client_pin) + *ret_has_client_pin = has_client_pin; + if (ret_has_up) + *ret_has_up = has_up; + if (ret_has_uv) + *ret_has_uv = has_uv; + + return 0; +} + static int fido2_use_hmac_hash_specific_token( const char *path, const char *rp_id, @@ -133,14 +219,12 @@ static int fido2_use_hmac_hash_specific_token( void **ret_hmac, size_t *ret_hmac_size) { - _cleanup_(fido_cbor_info_free_wrapper) fido_cbor_info_t *di = NULL; _cleanup_(fido_assert_free_wrapper) fido_assert_t *a = NULL; _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL; _cleanup_(erase_and_freep) void *hmac_copy = NULL; - bool found_extension = false; - size_t n, hmac_size; + bool has_up, has_client_pin; + size_t hmac_size; const void *hmac; - char **e; int r; assert(path); @@ -159,31 +243,9 @@ static int fido2_use_hmac_hash_specific_token( return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to open FIDO2 device %s: %s", path, sym_fido_strerr(r)); - if (!sym_fido_dev_is_fido2(d)) - return log_error_errno(SYNTHETIC_ERRNO(ENODEV), - "Specified device %s is not a FIDO2 device.", path); - - di = sym_fido_cbor_info_new(); - if (!di) - return log_oom(); - - r = sym_fido_dev_get_cbor_info(d, di); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to get CBOR device info for %s: %s", path, sym_fido_strerr(r)); - - e = sym_fido_cbor_info_extensions_ptr(di); - n = sym_fido_cbor_info_extensions_len(di); - - for (size_t i = 0; i < n; i++) - if (streq(e[i], "hmac-secret")) { - found_extension = true; - break; - } - - if (!found_extension) - return log_error_errno(SYNTHETIC_ERRNO(ENODEV), - "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", path); + r = verify_features(d, path, NULL, &has_client_pin, &has_up, NULL); + if (r < 0) + return r; a = sym_fido_assert_new(); if (!a) @@ -214,15 +276,21 @@ static int fido2_use_hmac_hash_specific_token( return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to add FIDO2 assertion credential ID: %s", sym_fido_strerr(r)); - r = sym_fido_assert_set_up(a, FIDO_OPT_FALSE); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to set FIDO2 assertion user presence: %s", sym_fido_strerr(r)); + if (has_up) { + r = sym_fido_assert_set_up(a, FIDO_OPT_FALSE); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to set FIDO2 assertion user presence: %s", sym_fido_strerr(r)); + } log_info("Asking FIDO2 token for authentication."); r = sym_fido_dev_get_assert(d, a, NULL); /* try without pin and without up first */ if (r == FIDO_ERR_UP_REQUIRED && up) { + + if (!has_up) + log_warning("Weird, device asked for User Presence check, but does not advertise it as feature. Ignoring."); + r = sym_fido_assert_set_up(a, FIDO_OPT_TRUE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), @@ -235,6 +303,9 @@ static int fido2_use_hmac_hash_specific_token( if (r == FIDO_ERR_PIN_REQUIRED) { char **i; + if (!has_client_pin) + log_warning("Weird, device asked for client PIN, but does not advertise it as feature. Ignoring."); + /* OK, we needed a pin, try with all pins in turn */ STRV_FOREACH(i, pins) { r = sym_fido_dev_get_assert(d, a, *i); @@ -370,17 +441,15 @@ int fido2_generate_hmac_hash( void **ret_secret, size_t *ret_secret_size, char **ret_usedpin) { - _cleanup_(fido_cbor_info_free_wrapper) fido_cbor_info_t *di = NULL; _cleanup_(erase_and_freep) void *salt = NULL, *secret_copy = NULL; _cleanup_(fido_assert_free_wrapper) fido_assert_t *a = NULL; _cleanup_(fido_cred_free_wrapper) fido_cred_t *c = NULL; _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL; _cleanup_(erase_and_freep) char *used_pin = NULL; + bool has_rk, has_client_pin, has_up, has_uv; _cleanup_free_ char *cid_copy = NULL; - size_t n, cid_size, secret_size; - bool found_extension = false; + size_t cid_size, secret_size; const void *cid, *secret; - char **e; int r; assert(device); @@ -427,31 +496,9 @@ int fido2_generate_hmac_hash( return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to open FIDO2 device %s: %s", device, sym_fido_strerr(r)); - if (!sym_fido_dev_is_fido2(d)) - return log_error_errno(SYNTHETIC_ERRNO(ENODEV), - "Specified device %s is not a FIDO2 device.", device); - - di = sym_fido_cbor_info_new(); - if (!di) - return log_oom(); - - r = sym_fido_dev_get_cbor_info(d, di); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to get CBOR device info for %s: %s", device, sym_fido_strerr(r)); - - e = sym_fido_cbor_info_extensions_ptr(di); - n = sym_fido_cbor_info_extensions_len(di); - - for (size_t i = 0; i < n; i++) - if (streq(e[i], "hmac-secret")) { - found_extension = true; - break; - } - - if (!found_extension) - return log_error_errno(SYNTHETIC_ERRNO(ENODEV), - "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", device); + r = verify_features(d, device, &has_rk, &has_client_pin, &has_up, &has_uv); + if (r < 0) + return r; c = sym_fido_cred_new(); if (!c) @@ -487,15 +534,19 @@ int fido2_generate_hmac_hash( return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set FIDO2 client data hash: %s", sym_fido_strerr(r)); - r = sym_fido_cred_set_rk(c, FIDO_OPT_FALSE); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to turn off FIDO2 resident key option of credential: %s", sym_fido_strerr(r)); + if (has_rk) { + r = sym_fido_cred_set_rk(c, FIDO_OPT_FALSE); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to turn off FIDO2 resident key option of credential: %s", sym_fido_strerr(r)); + } - r = sym_fido_cred_set_uv(c, FIDO_OPT_FALSE); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to turn off FIDO2 user verification option of credential: %s", sym_fido_strerr(r)); + if (has_uv) { + r = sym_fido_cred_set_uv(c, FIDO_OPT_FALSE); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to turn off FIDO2 user verification option of credential: %s", sym_fido_strerr(r)); + } log_info("Initializing FIDO2 credential on security token."); @@ -509,6 +560,9 @@ int fido2_generate_hmac_hash( _cleanup_(strv_free_erasep) char **pin = NULL; char **i; + if (!has_client_pin) + log_warning("Weird, device asked for client PIN, but does not advertise it as feature. Ignoring."); + r = ask_password_auto("Please enter security token PIN:", askpw_icon_name, NULL, "fido2-pin", USEC_INFINITY, 0, &pin); if (r < 0) return log_error_errno(r, "Failed to acquire user PIN: %m"); @@ -582,15 +636,21 @@ int fido2_generate_hmac_hash( return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to add FIDO2 assertion credential ID: %s", sym_fido_strerr(r)); - r = sym_fido_assert_set_up(a, FIDO_OPT_FALSE); - if (r != FIDO_OK) - return log_error_errno(SYNTHETIC_ERRNO(EIO), - "Failed to turn off FIDO2 assertion user presence: %s", sym_fido_strerr(r)); + if (has_up) { + r = sym_fido_assert_set_up(a, FIDO_OPT_FALSE); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to turn off FIDO2 assertion user presence: %s", sym_fido_strerr(r)); + } log_info("Generating secret key on FIDO2 security token."); r = sym_fido_dev_get_assert(d, a, used_pin); if (r == FIDO_ERR_UP_REQUIRED) { + + if (!has_up) + log_warning("Weird, device asked for User Presence check, but does not advertise it as feature. Ignoring."); + r = sym_fido_assert_set_up(a, FIDO_OPT_TRUE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), diff --git a/src/shared/libfido2-util.h b/src/shared/libfido2-util.h index c80554c1b4..3648ea44c7 100644 --- a/src/shared/libfido2-util.h +++ b/src/shared/libfido2-util.h @@ -20,6 +20,9 @@ extern size_t (*sym_fido_cbor_info_extensions_len)(const fido_cbor_info_t *); extern char **(*sym_fido_cbor_info_extensions_ptr)(const fido_cbor_info_t *); extern void (*sym_fido_cbor_info_free)(fido_cbor_info_t **); extern fido_cbor_info_t* (*sym_fido_cbor_info_new)(void); +extern size_t (*sym_fido_cbor_info_options_len)(const fido_cbor_info_t *); +extern char** (*sym_fido_cbor_info_options_name_ptr)(const fido_cbor_info_t *); +extern const bool* (*sym_fido_cbor_info_options_value_ptr)(const fido_cbor_info_t *); extern void (*sym_fido_cred_free)(fido_cred_t **); extern size_t (*sym_fido_cred_id_len)(const fido_cred_t *); extern const unsigned char* (*sym_fido_cred_id_ptr)(const fido_cred_t *); From 2bc5c425e65a140f2eaf31bdf23a277e32ce38a7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 26 Nov 2020 12:46:10 +0100 Subject: [PATCH 17/30] cryptsetup: add fido2 support --- meson.build | 7 +- src/cryptsetup/cryptsetup-fido2.c | 190 ++++++++++++++++++++++++++++ src/cryptsetup/cryptsetup-fido2.h | 71 +++++++++++ src/cryptsetup/cryptsetup.c | 197 +++++++++++++++++++++++++++++- 4 files changed, 460 insertions(+), 5 deletions(-) create mode 100644 src/cryptsetup/cryptsetup-fido2.c create mode 100644 src/cryptsetup/cryptsetup-fido2.h diff --git a/meson.build b/meson.build index 9a7f9be3cb..c64edda8e3 100644 --- a/meson.build +++ b/meson.build @@ -2367,9 +2367,10 @@ executable( if conf.get('HAVE_LIBCRYPTSETUP') == 1 systemd_cryptsetup_sources = files(''' - src/cryptsetup/cryptsetup-pkcs11.h + src/cryptsetup/cryptsetup-fido2.h src/cryptsetup/cryptsetup-keyfile.c src/cryptsetup/cryptsetup-keyfile.h + src/cryptsetup/cryptsetup-pkcs11.h src/cryptsetup/cryptsetup.c '''.split()) @@ -2377,6 +2378,10 @@ if conf.get('HAVE_LIBCRYPTSETUP') == 1 systemd_cryptsetup_sources += files('src/cryptsetup/cryptsetup-pkcs11.c') endif + if conf.get('HAVE_LIBFIDO2') == 1 + systemd_cryptsetup_sources += files('src/cryptsetup/cryptsetup-fido2.c') + endif + executable( 'systemd-cryptsetup', systemd_cryptsetup_sources, diff --git a/src/cryptsetup/cryptsetup-fido2.c b/src/cryptsetup/cryptsetup-fido2.c new file mode 100644 index 0000000000..5edda0cf9d --- /dev/null +++ b/src/cryptsetup/cryptsetup-fido2.c @@ -0,0 +1,190 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "ask-password-api.h" +#include "cryptsetup-fido2.h" +#include "fileio.h" +#include "hexdecoct.h" +#include "json.h" +#include "libfido2-util.h" +#include "parse-util.h" +#include "random-util.h" +#include "strv.h" + +int acquire_fido2_key( + const char *volume_name, + const char *friendly_name, + const char *device, + const char *rp_id, + const void *cid, + size_t cid_size, + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + const void *key_data, + size_t key_data_size, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + AskPasswordFlags flags = ASK_PASSWORD_PUSH_CACHE | ASK_PASSWORD_ACCEPT_CACHED; + _cleanup_strv_free_erase_ char **pins = NULL; + _cleanup_free_ void *loaded_salt = NULL; + const char *salt; + size_t salt_size; + char *e; + int r; + + assert(cid); + assert(key_file || key_data); + + if (key_data) { + salt = key_data; + salt_size = key_data_size; + } else { + _cleanup_free_ char *bindname = NULL; + + /* If we read the salt via AF_UNIX, make this client recognizable */ + if (asprintf(&bindname, "@%" PRIx64"/cryptsetup-fido2/%s", random_u64(), volume_name) < 0) + return log_oom(); + + r = read_full_file_full( + AT_FDCWD, key_file, + key_file_offset == 0 ? UINT64_MAX : key_file_offset, + key_file_size == 0 ? SIZE_MAX : key_file_size, + READ_FULL_FILE_CONNECT_SOCKET, + bindname, + (char**) &loaded_salt, &salt_size); + if (r < 0) + return r; + + salt = loaded_salt; + } + + e = getenv("PIN"); + if (e) { + pins = strv_new(e); + if (!pins) + return log_oom(); + + string_erase(e); + if (unsetenv("PIN") < 0) + return log_error_errno(errno, "Failed to unset $PIN: %m"); + } + + for (;;) { + r = fido2_use_hmac_hash( + device, + rp_id ?: "io.systemd.cryptsetup", + salt, salt_size, + cid, cid_size, + pins, + /* up= */ true, + ret_decrypted_key, + ret_decrypted_key_size); + if (!IN_SET(r, + -ENOANO, /* needs pin */ + -ENOLCK)) /* pin incorrect */ + return r; + + pins = strv_free_erase(pins); + + r = ask_password_auto("Please enter security token PIN:", "drive-harddisk", NULL, "fido2-pin", until, flags, &pins); + if (r < 0) + return log_error_errno(r, "Failed to ask for user pasword: %m"); + + flags &= ~ASK_PASSWORD_ACCEPT_CACHED; + } +} + +int find_fido2_auto_data( + struct crypt_device *cd, + char **ret_rp_id, + void **ret_salt, + size_t *ret_salt_size, + void **ret_cid, + size_t *ret_cid_size, + int *ret_keyslot) { + + _cleanup_free_ void *cid = NULL, *salt = NULL; + size_t cid_size = 0, salt_size = 0; + _cleanup_free_ char *rp = NULL; + int r, keyslot = -1; + + assert(cd); + assert(ret_salt); + assert(ret_salt_size); + assert(ret_cid); + assert(ret_cid_size); + assert(ret_keyslot); + + /* Loads FIDO2 metadata from LUKS2 JSON token headers. */ + + for (int token = 0; token < LUKS2_TOKENS_MAX; token ++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + JsonVariant *w; + + r = cryptsetup_get_token_as_json(cd, token, "systemd-fido2", &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"); + + if (cid) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), + "Multiple FIDO2 tokens enrolled, cannot automatically determine token."); + + w = json_variant_by_key(v, "fido2-credential"); + if (!w || !json_variant_is_string(w)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "FIDO2 token data lacks 'fido2-credential' field."); + + r = unbase64mem(json_variant_string(w), (size_t) -1, &cid, &cid_size); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid base64 data in 'fido2-credential' field."); + + w = json_variant_by_key(v, "fido2-salt"); + if (!w || !json_variant_is_string(w)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "FIDO2 token data lacks 'fido2-salt' field."); + + assert(!salt); + assert(salt_size == 0); + r = unbase64mem(json_variant_string(w), (size_t) -1, &salt, &salt_size); + if (r < 0) + return log_error_errno(r, "Failed to decode base64 encoded salt."); + + assert(keyslot < 0); + keyslot = cryptsetup_get_keyslot_from_token(v); + if (keyslot < 0) + return log_error_errno(keyslot, "Failed to extract keyslot index from FIDO2 JSON data: %m"); + + w = json_variant_by_key(v, "fido2-rp"); + if (w) { + /* The "rp" field is optional. */ + + if (!json_variant_is_string(w)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "FIDO2 token data's 'fido2-rp' field is not a string."); + + assert(!rp); + rp = strdup(json_variant_string(w)); + if (!rp) + return log_oom(); + } + } + + if (!cid) + return log_error_errno(SYNTHETIC_ERRNO(ENXIO), + "No valid FIDO2 token data found."); + + log_info("Automatically discovered security FIDO2 token unlocks volume."); + + *ret_rp_id = TAKE_PTR(rp); + *ret_cid = TAKE_PTR(cid); + *ret_cid_size = cid_size; + *ret_salt = TAKE_PTR(salt); + *ret_salt_size = salt_size; + *ret_keyslot = keyslot; + return 0; +} diff --git a/src/cryptsetup/cryptsetup-fido2.h b/src/cryptsetup/cryptsetup-fido2.h new file mode 100644 index 0000000000..92093ba38b --- /dev/null +++ b/src/cryptsetup/cryptsetup-fido2.h @@ -0,0 +1,71 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "cryptsetup-util.h" +#include "log.h" +#include "time-util.h" + +#if HAVE_LIBFIDO2 + +int acquire_fido2_key( + const char *volume_name, + const char *friendly_name, + const char *device, + const char *rp_id, + const void *cid, + size_t cid_size, + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + const void *key_data, + size_t key_data_size, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size); + +int find_fido2_auto_data( + struct crypt_device *cd, + char **ret_rp_id, + void **ret_salt, + size_t *ret_salt_size, + void **ret_cid, + size_t *ret_cid_size, + int *ret_keyslot); + +#else + +static inline int acquire_fido2_key( + const char *volume_name, + const char *friendly_name, + const char *device, + const char *rp_id, + const void *cid, + size_t cid_size, + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + const void *key_data, + size_t key_data_size, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "FIDO2 token support not available."); +} + +static inline int find_fido2_auto_data( + struct crypt_device *cd, + char **ret_rp_id, + void **ret_salt, + size_t *ret_salt_size, + void **ret_cid, + size_t *ret_cid_size, + int *ret_keyslot) { + + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "FIDO2 token support not available."); +} +#endif diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index bc88d7102a..d9df81b74a 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -11,6 +11,7 @@ #include "alloc-util.h" #include "ask-password-api.h" +#include "cryptsetup-fido2.h" #include "cryptsetup-keyfile.h" #include "cryptsetup-pkcs11.h" #include "cryptsetup-util.h" @@ -20,6 +21,7 @@ #include "fs-util.h" #include "fstab-util.h" #include "hexdecoct.h" +#include "libfido2-util.h" #include "log.h" #include "main-func.h" #include "memory-util.h" @@ -65,12 +67,20 @@ static uint64_t arg_skip = 0; static usec_t arg_timeout = USEC_INFINITY; static char *arg_pkcs11_uri = NULL; static bool arg_pkcs11_uri_auto = false; +static char *arg_fido2_device = NULL; +static bool arg_fido2_device_auto = false; +static void *arg_fido2_cid = NULL; +static size_t arg_fido2_cid_size = 0; +static char *arg_fido2_rp_id = NULL; STATIC_DESTRUCTOR_REGISTER(arg_cipher, freep); STATIC_DESTRUCTOR_REGISTER(arg_hash, freep); STATIC_DESTRUCTOR_REGISTER(arg_header, freep); STATIC_DESTRUCTOR_REGISTER(arg_tcrypt_keyfiles, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_uri, freep); +STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, freep); +STATIC_DESTRUCTOR_REGISTER(arg_fido2_cid, freep); +STATIC_DESTRUCTOR_REGISTER(arg_fido2_rp_id, freep); /* Options Debian's crypttab knows we don't: @@ -277,6 +287,46 @@ static int parse_one_option(const char *option) { arg_pkcs11_uri_auto = false; } + } else if ((val = startswith(option, "fido2-device="))) { + + if (streq(val, "auto")) { + arg_fido2_device = mfree(arg_fido2_device); + arg_fido2_device_auto = true; + } else { + r = free_and_strdup(&arg_fido2_device, val); + if (r < 0) + return log_oom(); + + arg_fido2_device_auto = false; + } + + } else if ((val = startswith(option, "fido2-cid="))) { + + if (streq(val, "auto")) + arg_fido2_cid = mfree(arg_fido2_cid); + else { + _cleanup_free_ void *cid = NULL; + size_t cid_size; + + r = unbase64mem(val, (size_t) -1, &cid, &cid_size); + if (r < 0) + return log_error_errno(r, "Failed to decode FIDO2 CID data: %m"); + + free(arg_fido2_cid); + arg_fido2_cid = TAKE_PTR(cid); + arg_fido2_cid_size = cid_size; + } + + /* Turn on FIDO2 as side-effect, if not turned on yet. */ + if (!arg_fido2_device && !arg_fido2_device_auto) + arg_fido2_device_auto = true; + + } else if ((val = startswith(option, "fido2-rp="))) { + + r = free_and_strdup(&arg_fido2_rp_id, val); + if (r < 0) + return log_oom(); + } else if ((val = startswith(option, "try-empty-password="))) { r = parse_boolean(val); @@ -506,10 +556,10 @@ static int attach_tcrypt( assert(name); assert(key_file || key_data || !strv_isempty(passwords)); - if (arg_pkcs11_uri || arg_pkcs11_uri_auto) + if (arg_pkcs11_uri || arg_pkcs11_uri_auto || arg_fido2_device || arg_fido2_device_auto) /* Ask for a regular password */ return log_error_errno(SYNTHETIC_ERRNO(EAGAIN), - "Sorry, but tcrypt devices are currently not supported in conjunction with pkcs11 support."); + "Sorry, but tcrypt devices are currently not supported in conjunction with pkcs11/fido2 support."); if (arg_tcrypt_hidden) params.flags |= CRYPT_TCRYPT_HIDDEN_HEADER; @@ -595,6 +645,141 @@ static int make_security_device_monitor(sd_event *event, sd_device_monitor **ret return 0; } +static int attach_luks_or_plain_or_bitlk_by_fido2( + struct crypt_device *cd, + const char *name, + const char *key_file, + const void *key_data, + size_t key_data_size, + usec_t until, + uint32_t flags, + bool pass_volume_key) { + + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL; + _cleanup_(erase_and_freep) void *decrypted_key = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + _cleanup_free_ void *discovered_salt = NULL, *discovered_cid = NULL; + size_t discovered_salt_size, discovered_cid_size, cid_size, decrypted_key_size; + _cleanup_free_ char *friendly = NULL, *discovered_rp_id = NULL; + int keyslot = arg_key_slot, r; + const char *rp_id; + const void *cid; + + assert(cd); + assert(name); + assert(arg_fido2_device || arg_fido2_device_auto); + + if (arg_fido2_cid) { + if (!key_file && !key_data) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "FIDO2 mode selected but no key file specified, refusing."); + + rp_id = arg_fido2_rp_id; + cid = arg_fido2_cid; + cid_size = arg_fido2_cid_size; + } else { + r = find_fido2_auto_data( + cd, + &discovered_rp_id, + &discovered_salt, + &discovered_salt_size, + &discovered_cid, + &discovered_cid_size, + &keyslot); + + if (IN_SET(r, -ENOTUNIQ, -ENXIO)) + return log_debug_errno(SYNTHETIC_ERRNO(EAGAIN), + "Automatic FIDO2 metadata discovery was not possible because missing or not unique, falling back to traditional unlocking."); + if (r < 0) + return r; + + rp_id = discovered_rp_id; + key_data = discovered_salt; + key_data_size = discovered_salt_size; + cid = discovered_cid; + cid_size = discovered_cid_size; + } + + friendly = friendly_disk_name(crypt_get_device_name(cd), name); + if (!friendly) + return log_oom(); + + for (;;) { + bool processed = false; + + r = acquire_fido2_key( + name, + friendly, + arg_fido2_device, + rp_id, + cid, cid_size, + key_file, arg_keyfile_size, arg_keyfile_offset, + key_data, key_data_size, + until, + &decrypted_key, &decrypted_key_size); + if (r >= 0) + break; + if (r != -EAGAIN) /* EAGAIN means: token not found */ + return r; + + if (!monitor) { + /* We didn't find the token. In this case, watch for it via udev. Let's + * create an event loop and monitor first. */ + + assert(!event); + + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to allocate event loop: %m"); + + r = make_security_device_monitor(event, &monitor); + if (r < 0) + return r; + + log_notice("Security token not present for unlocking volume %s, please plug it in.", friendly); + + /* Let's immediately rescan in case the token appeared in the time we needed + * to create and configure the monitor */ + continue; + } + + for (;;) { + /* Wait for one event, and then eat all subsequent events until there are no + * further ones */ + r = sd_event_run(event, processed ? 0 : UINT64_MAX); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + if (r == 0) + break; + + processed = true; + } + + log_debug("Got one or more potentially relevant udev events, rescanning FIDO2..."); + } + + if (pass_volume_key) + r = crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); + else { + _cleanup_(erase_and_freep) char *base64_encoded = NULL; + + /* Before using this key as passphrase we base64 encode it, for compat with homed */ + + r = base64mem(decrypted_key, decrypted_key_size, &base64_encoded); + if (r < 0) + return log_oom(); + + r = crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, strlen(base64_encoded), flags); + } + if (r == -EPERM) { + log_error_errno(r, "Failed to activate with FIDO2 decrypted key. (Key incorrect?)"); + return -EAGAIN; /* log actual error, but return EAGAIN */ + } + if (r < 0) + return log_error_errno(r, "Failed to activate with FIDO2 acquired key: %m"); + + return 0; +} + static int attach_luks_or_plain_or_bitlk_by_pkcs11( struct crypt_device *cd, const char *name, @@ -898,6 +1083,8 @@ static int attach_luks_or_plain_or_bitlk( crypt_get_volume_key_size(cd)*8, crypt_get_device_name(cd)); + if (arg_fido2_device || arg_fido2_device_auto) + return attach_luks_or_plain_or_bitlk_by_fido2(cd, name, key_file, key_data, key_data_size, until, flags, pass_volume_key); if (arg_pkcs11_uri || arg_pkcs11_uri_auto) return attach_luks_or_plain_or_bitlk_by_pkcs11(cd, name, key_file, key_data, key_data_size, until, flags, pass_volume_key); if (key_data) @@ -1118,14 +1305,14 @@ static int run(int argc, char *argv[]) { /* When we were able to acquire multiple keys, let's always process them in this order: * - * 1. A key acquired via PKCS#11 token + * 1. A key acquired via PKCS#11 or FIDO2 token * 2. The discovered key: i.e. key_data + key_data_size * 3. The configured key: i.e. key_file + arg_keyfile_offset + arg_keyfile_size * 4. The empty password, in case arg_try_empty_password is set * 5. We enquire the user for a password */ - if (!key_file && !key_data && !arg_pkcs11_uri && !arg_pkcs11_uri_auto) { + if (!key_file && !key_data && !arg_pkcs11_uri && !arg_pkcs11_uri_auto && !arg_fido2_device && !arg_fido2_device_auto) { if (arg_try_empty_password) { /* Hmm, let's try an empty password now, but only once */ @@ -1164,6 +1351,8 @@ static int run(int argc, char *argv[]) { key_data_size = 0; arg_pkcs11_uri = mfree(arg_pkcs11_uri); arg_pkcs11_uri_auto = false; + arg_fido2_device = mfree(arg_fido2_device); + arg_fido2_device_auto = false; } if (arg_tries != 0 && tries >= arg_tries) From 8710a6818e2ce15a5840b72b7b832f5745f22254 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 24 Nov 2020 13:41:47 +0100 Subject: [PATCH 18/30] cryptenroll: add new "systemd-cryptenroll" tool for enrolling FIDO2+PKCS#11 security tokens --- meson.build | 30 +++ src/cryptenroll/cryptenroll-fido2.c | 88 ++++++ src/cryptenroll/cryptenroll-fido2.h | 16 ++ src/cryptenroll/cryptenroll-password.c | 105 ++++++++ src/cryptenroll/cryptenroll-password.h | 8 + src/cryptenroll/cryptenroll-pkcs11.c | 99 +++++++ src/cryptenroll/cryptenroll-pkcs11.h | 16 ++ src/cryptenroll/cryptenroll-recovery.c | 101 +++++++ src/cryptenroll/cryptenroll-recovery.h | 8 + src/cryptenroll/cryptenroll.c | 358 +++++++++++++++++++++++++ 10 files changed, 829 insertions(+) create mode 100644 src/cryptenroll/cryptenroll-fido2.c create mode 100644 src/cryptenroll/cryptenroll-fido2.h create mode 100644 src/cryptenroll/cryptenroll-password.c create mode 100644 src/cryptenroll/cryptenroll-password.h create mode 100644 src/cryptenroll/cryptenroll-pkcs11.c create mode 100644 src/cryptenroll/cryptenroll-pkcs11.h create mode 100644 src/cryptenroll/cryptenroll-recovery.c create mode 100644 src/cryptenroll/cryptenroll-recovery.h create mode 100644 src/cryptenroll/cryptenroll.c diff --git a/meson.build b/meson.build index c64edda8e3..f434d68542 100644 --- a/meson.build +++ b/meson.build @@ -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 index 0000000000..1b3ae8d67c --- /dev/null +++ b/src/cryptenroll/cryptenroll-fido2.c @@ -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 index 0000000000..936792071f --- /dev/null +++ b/src/cryptenroll/cryptenroll-fido2.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#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 index 0000000000..e08f564d3f --- /dev/null +++ b/src/cryptenroll/cryptenroll-password.c @@ -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 index 0000000000..ddeee1318f --- /dev/null +++ b/src/cryptenroll/cryptenroll-password.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#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 index 0000000000..15ae6c9420 --- /dev/null +++ b/src/cryptenroll/cryptenroll-pkcs11.c @@ -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 index 0000000000..b6d28bd92c --- /dev/null +++ b/src/cryptenroll/cryptenroll-pkcs11.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#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 0000000000..3204c463a5 --- /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 "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 index 0000000000..9bf4f2e489 --- /dev/null +++ b/src/cryptenroll/cryptenroll-recovery.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#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 index 0000000000..e08f810b9b --- /dev/null +++ b/src/cryptenroll/cryptenroll.c @@ -0,0 +1,358 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#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); From 1403d48d6189f71d450cf9cfda1ac8494e16b20c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 30 Nov 2020 16:23:15 +0100 Subject: [PATCH 19/30] sort-util: make cmp_int() generic, so that we can reuse it elsewhere --- src/basic/fd-util.c | 4 ---- src/basic/sort-util.c | 4 ++++ src/basic/sort-util.h | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/basic/fd-util.c b/src/basic/fd-util.c index 07a7b3a306..a03ba83e19 100644 --- a/src/basic/fd-util.c +++ b/src/basic/fd-util.c @@ -211,10 +211,6 @@ static int get_max_fd(void) { return (int) (m - 1); } -static int cmp_int(const int *a, const int *b) { - return CMP(*a, *b); -} - int close_all_fds(const int except[], size_t n_except) { static bool have_close_range = true; /* Assume we live in the future */ _cleanup_closedir_ DIR *d = NULL; diff --git a/src/basic/sort-util.c b/src/basic/sort-util.c index 92d7b8588d..a9c68b7e3e 100644 --- a/src/basic/sort-util.c +++ b/src/basic/sort-util.c @@ -27,3 +27,7 @@ void *xbsearch_r(const void *key, const void *base, size_t nmemb, size_t size, } return NULL; } + +int cmp_int(const int *a, const int *b) { + return CMP(*a, *b); +} diff --git a/src/basic/sort-util.h b/src/basic/sort-util.h index 1d194a1f04..49586a4a24 100644 --- a/src/basic/sort-util.h +++ b/src/basic/sort-util.h @@ -68,3 +68,5 @@ static inline void qsort_r_safe(void *base, size_t nmemb, size_t size, __compar_ int (*_func_)(const typeof(p[0])*, const typeof(p[0])*, typeof(userdata)) = func; \ qsort_r_safe((p), (n), sizeof((p)[0]), (__compar_d_fn_t) _func_, userdata); \ }) + +int cmp_int(const int *a, const int *b); From 2d64d2b95538e00278d5a38c38171019dafab486 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 1 Dec 2020 13:45:32 +0100 Subject: [PATCH 20/30] json: add APIs for quickly inserting hex blobs into as JSON strings This is similar to the base64 support, but fixed-size hash values are typically preferably presented as series of hex values, hence store them here like that too. --- src/shared/json.c | 51 +++++++++++++++++++++++++++++++++++++++++++++++ src/shared/json.h | 4 ++++ 2 files changed, 55 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index 6ea246a22b..6beb56cfa0 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -433,6 +433,19 @@ int json_variant_new_base64(JsonVariant **ret, const void *p, size_t n) { return json_variant_new_stringn(ret, s, k); } +int json_variant_new_hex(JsonVariant **ret, const void *p, size_t n) { + _cleanup_free_ char *s = NULL; + + assert_return(ret, -EINVAL); + assert_return(n == 0 || p, -EINVAL); + + s = hexmem(p, n); + if (!s) + return -ENOMEM; + + return json_variant_new_stringn(ret, s, n*2); +} + int json_variant_new_id128(JsonVariant **ret, sd_id128_t id) { char s[SD_ID128_STRING_MAX]; @@ -3603,6 +3616,36 @@ int json_buildv(JsonVariant **ret, va_list ap) { break; } + case _JSON_BUILD_HEX: { + const void *p; + size_t n; + + if (!IN_SET(current->expect, EXPECT_TOPLEVEL, EXPECT_OBJECT_VALUE, EXPECT_ARRAY_ELEMENT)) { + r = -EINVAL; + goto finish; + } + + p = va_arg(ap, const void *); + n = va_arg(ap, size_t); + + if (current->n_suppress == 0) { + r = json_variant_new_hex(&add, p, n); + if (r < 0) + goto finish; + } + + n_subtract = 1; + + if (current->expect == EXPECT_TOPLEVEL) + current->expect = EXPECT_END; + else if (current->expect == EXPECT_OBJECT_VALUE) + current->expect = EXPECT_OBJECT_KEY; + else + assert(current->expect == EXPECT_ARRAY_ELEMENT); + + break; + } + case _JSON_BUILD_ID128: { sd_id128_t id; @@ -4405,6 +4448,14 @@ int json_variant_unbase64(JsonVariant *v, void **ret, size_t *ret_size) { return unbase64mem(json_variant_string(v), (size_t) -1, ret, ret_size); } +int json_variant_unhex(JsonVariant *v, void **ret, size_t *ret_size) { + + if (!json_variant_is_string(v)) + return -EINVAL; + + return unhexmem(json_variant_string(v), (size_t) -1, ret, ret_size); +} + static const char* const json_variant_type_table[_JSON_VARIANT_TYPE_MAX] = { [JSON_VARIANT_STRING] = "string", [JSON_VARIANT_INTEGER] = "integer", diff --git a/src/shared/json.h b/src/shared/json.h index 0809f3187e..abc0dccddb 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -58,6 +58,7 @@ typedef enum JsonVariantType { int json_variant_new_stringn(JsonVariant **ret, const char *s, size_t n); int json_variant_new_base64(JsonVariant **ret, const void *p, size_t n); +int json_variant_new_hex(JsonVariant **ret, const void *p, size_t n); int json_variant_new_integer(JsonVariant **ret, intmax_t i); int json_variant_new_unsigned(JsonVariant **ret, uintmax_t u); int json_variant_new_real(JsonVariant **ret, long double d); @@ -227,6 +228,7 @@ enum { _JSON_BUILD_LITERAL, _JSON_BUILD_STRV, _JSON_BUILD_BASE64, + _JSON_BUILD_HEX, _JSON_BUILD_ID128, _JSON_BUILD_BYTE_ARRAY, _JSON_BUILD_MAX, @@ -249,6 +251,7 @@ enum { #define JSON_BUILD_LITERAL(l) _JSON_BUILD_LITERAL, ({ const char *_x = l; _x; }) #define JSON_BUILD_STRV(l) _JSON_BUILD_STRV, ({ char **_x = l; _x; }) #define JSON_BUILD_BASE64(p, n) _JSON_BUILD_BASE64, ({ const void *_x = p; _x; }), ({ size_t _y = n; _y; }) +#define JSON_BUILD_HEX(p, n) _JSON_BUILD_HEX, ({ const void *_x = p; _x; }), ({ size_t _y = n; _y; }) #define JSON_BUILD_ID128(id) _JSON_BUILD_ID128, ({ sd_id128_t _x = id; _x; }) #define JSON_BUILD_BYTE_ARRAY(v, n) _JSON_BUILD_BYTE_ARRAY, ({ const void *_x = v; _x; }), ({ size_t _y = n; _y; }) @@ -351,6 +354,7 @@ int json_log_internal(JsonVariant *variant, int level, int error, const char *fi }) int json_variant_unbase64(JsonVariant *v, void **ret, size_t *ret_size); +int json_variant_unhex(JsonVariant *v, void **ret, size_t *ret_size); const char *json_variant_type_to_string(JsonVariantType t); JsonVariantType json_variant_type_from_string(const char *s); From 5e521624f292e2d469abe5e256db374dc17136ba Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 28 Nov 2020 15:27:34 +0100 Subject: [PATCH 21/30] cryptenroll: add support for TPM2 enrolling --- meson.build | 18 + meson_options.txt | 2 + src/cryptenroll/cryptenroll-tpm2.c | 131 ++++ src/cryptenroll/cryptenroll-tpm2.h | 16 + src/cryptenroll/cryptenroll.c | 61 ++ src/shared/meson.build | 2 + src/shared/tpm2-util.c | 998 +++++++++++++++++++++++++++++ src/shared/tpm2-util.h | 51 ++ 8 files changed, 1279 insertions(+) create mode 100644 src/cryptenroll/cryptenroll-tpm2.c create mode 100644 src/cryptenroll/cryptenroll-tpm2.h create mode 100644 src/shared/tpm2-util.c create mode 100644 src/shared/tpm2-util.h diff --git a/meson.build b/meson.build index f434d68542..70696a724b 100644 --- a/meson.build +++ b/meson.build @@ -1185,6 +1185,17 @@ else endif conf.set10('HAVE_LIBFIDO2', have) +want_tpm2 = get_option('tpm2') +if want_tpm2 != 'false' and not skip_deps + tpm2 = dependency('tss2-esys tss2-rc tss2-mu', + required : want_tpm2 == 'true') + have = tpm2.found() +else + have = false + tpm2 = [] +endif +conf.set10('HAVE_TPM2', have) + want_elfutils = get_option('elfutils') if want_elfutils != 'false' and not skip_deps libdw = dependency('libdw', @@ -2428,6 +2439,7 @@ if conf.get('HAVE_LIBCRYPTSETUP') == 1 src/cryptenroll/cryptenroll-pkcs11.h src/cryptenroll/cryptenroll-recovery.c src/cryptenroll/cryptenroll-recovery.h + src/cryptenroll/cryptenroll-tpm2.h src/cryptenroll/cryptenroll.c '''.split()) @@ -2439,12 +2451,17 @@ if conf.get('HAVE_LIBCRYPTSETUP') == 1 systemd_cryptenroll_sources += files('src/cryptenroll/cryptenroll-fido2.c') endif + if conf.get('HAVE_TPM2') == 1 + systemd_cryptenroll_sources += files('src/cryptenroll/cryptenroll-tpm2.c') + endif + executable( 'systemd-cryptenroll', systemd_cryptenroll_sources, include_directories : includes, link_with : [libshared], dependencies : [libcryptsetup, + libdl, libopenssl, libp11kit], install_rpath : rootlibexecdir, @@ -3770,6 +3787,7 @@ foreach tuple : [ ['libfdisk'], ['p11kit'], ['libfido2'], + ['tpm2'], ['AUDIT'], ['IMA'], ['AppArmor'], diff --git a/meson_options.txt b/meson_options.txt index eed3596b9b..6e104d21c3 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -327,6 +327,8 @@ option('p11kit', type : 'combo', choices : ['auto', 'true', 'false'], description : 'p11kit support') option('libfido2', type : 'combo', choices : ['auto', 'true', 'false'], description : 'FIDO2 support') +option('tpm2', type : 'combo', choices : ['auto', 'true', 'false'], + description : 'TPM2 support') option('elfutils', type : 'combo', choices : ['auto', 'true', 'false'], description : 'elfutils support') option('zlib', type : 'combo', choices : ['auto', 'true', 'false'], diff --git a/src/cryptenroll/cryptenroll-tpm2.c b/src/cryptenroll/cryptenroll-tpm2.c new file mode 100644 index 0000000000..211f8f9874 --- /dev/null +++ b/src/cryptenroll/cryptenroll-tpm2.c @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "cryptenroll-tpm2.h" +#include "hexdecoct.h" +#include "json.h" +#include "memory-util.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 < LUKS2_TOKENS_MAX; 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) + return log_error_errno(keyslot, "Failed to determine keyslot of JSON token: %m"); + + 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_t) -1, &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 */ +} + +int enroll_tpm2(struct crypt_device *cd, + const void *volume_key, + size_t volume_key_size, + const char *device, + uint32_t pcr_mask) { + + _cleanup_(erase_and_freep) void *secret = NULL, *secret2 = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *a = NULL; + _cleanup_(erase_and_freep) char *base64_encoded = NULL; + size_t secret_size, secret2_size, blob_size, hash_size; + _cleanup_free_ void *blob = NULL, *hash = NULL; + const char *node; + int r, keyslot; + + assert(cd); + assert(volume_key); + assert(volume_key_size > 0); + assert(pcr_mask < (1U << TPM2_PCRS_MAX)); /* Support 24 PCR banks */ + + assert_se(node = crypt_get_device_name(cd)); + + r = tpm2_seal(device, pcr_mask, &secret, &secret_size, &blob, &blob_size, &hash, &hash_size); + if (r < 0) + return r; + + /* Let's see if we already have this specific PCR policy hash enrolled, if so, exit early. */ + r = search_policy_hash(cd, hash, hash_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 */ + } + + /* Quick verification that everything is in order, we are not in a hurry after all. */ + log_debug("Unsealing for verification..."); + r = tpm2_unseal(device, pcr_mask, blob, blob_size, hash, hash_size, &secret2, &secret2_size); + if (r < 0) + return r; + + 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. */ + 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 TPM2 key to %s: %m", node); + + r = tpm2_make_luks2_json(keyslot, pcr_mask, blob, blob_size, hash, hash_size, &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 0000000000..d5dd1b0003 --- /dev/null +++ b/src/cryptenroll/cryptenroll-tpm2.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "cryptsetup-util.h" +#include "log.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 pcr_mask); +#else +static inline int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t pcr_mask) { + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "TPM2 key enrollment not supported."); +} +#endif diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index e08f810b9b..e3a660d021 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -7,22 +7,26 @@ #include "cryptenroll-password.h" #include "cryptenroll-pkcs11.h" #include "cryptenroll-recovery.h" +#include "cryptenroll-tpm2.h" #include "cryptsetup-util.h" #include "escape.h" #include "libfido2-util.h" #include "main-func.h" #include "memory-util.h" +#include "parse-util.h" #include "path-util.h" #include "pkcs11-util.h" #include "pretty-print.h" #include "strv.h" #include "terminal-util.h" +#include "tpm2-util.h" typedef enum EnrollType { ENROLL_PASSWORD, ENROLL_RECOVERY, ENROLL_PKCS11, ENROLL_FIDO2, + ENROLL_TPM2, _ENROLL_TYPE_MAX, _ENROLL_TYPE_INVALID = -1, } EnrollType; @@ -31,6 +35,7 @@ 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 uint32_t arg_tpm2_pcr_mask = UINT32_MAX; static char *arg_node = NULL; STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, freep); @@ -56,6 +61,10 @@ static int help(void) { " Specify PKCS#11 security token URI\n" " --fido2-device=PATH\n" " Enroll a FIDO2-HMAC security token\n" + " --tpm2-device=PATH\n" + " Enroll a TPM2 device\n" + " --tpm2-pcrs=PCR1,PCR2,PCR3,…\n" + " Specifiy TPM2 PCRs to seal against\n" "\nSee the %s for details.\n" , program_invocation_short_name , ansi_highlight(), ansi_normal() @@ -73,6 +82,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_RECOVERY_KEY, ARG_PKCS11_TOKEN_URI, ARG_FIDO2_DEVICE, + ARG_TPM2_DEVICE, + ARG_TPM2_PCRS, }; static const struct option options[] = { @@ -82,6 +93,8 @@ static int parse_argv(int argc, char *argv[]) { { "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 }, + { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, + { "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS }, {} }; @@ -169,6 +182,47 @@ static int parse_argv(int argc, char *argv[]) { 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_PCRS: { + uint32_t mask; + + if (isempty(optarg)) { + arg_tpm2_pcr_mask = 0; + break; + } + + r = tpm2_parse_pcrs(optarg, &mask); + if (r < 0) + return r; + + if (arg_tpm2_pcr_mask == UINT32_MAX) + arg_tpm2_pcr_mask = mask; + else + arg_tpm2_pcr_mask |= mask; + + break; + } + case '?': return -EINVAL; @@ -193,6 +247,9 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; + if (arg_tpm2_pcr_mask == UINT32_MAX) + arg_tpm2_pcr_mask = TPM2_PCR_MASK_DEFAULT; + return 1; } @@ -348,6 +405,10 @@ static int run(int argc, char *argv[]) { r = enroll_fido2(cd, vk, vks, arg_fido2_device); break; + case ENROLL_TPM2: + r = enroll_tpm2(cd, vk, vks, arg_tpm2_device, arg_tpm2_pcr_mask); + break; + default: return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Operation not implemented yet."); } diff --git a/src/shared/meson.build b/src/shared/meson.build index 45b8d1b07c..ec4f3e882a 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -237,6 +237,8 @@ shared_sources = files(''' tmpfile-util-label.h tomoyo-util.c tomoyo-util.h + tpm2-util.c + tpm2-util.h udev-util.c udev-util.h uid-range.c diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c new file mode 100644 index 0000000000..8a0f45c2db --- /dev/null +++ b/src/shared/tpm2-util.c @@ -0,0 +1,998 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "extract-word.h" +#include "parse-util.h" +#include "tpm2-util.h" + +#if HAVE_TPM2 +#include "alloc-util.h" +#include "dirent-util.h" +#include "dlfcn-util.h" +#include "fd-util.h" +#include "format-table.h" +#include "fs-util.h" +#include "hexdecoct.h" +#include "memory-util.h" +#include "random-util.h" +#include "time-util.h" + +static void *libtss2_esys_dl = NULL; +static void *libtss2_rc_dl = NULL; +static void *libtss2_mu_dl = NULL; + +TSS2_RC (*sym_Esys_Create)(ESYS_CONTEXT *esysContext, ESYS_TR parentHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_SENSITIVE_CREATE *inSensitive, const TPM2B_PUBLIC *inPublic, const TPM2B_DATA *outsideInfo, const TPML_PCR_SELECTION *creationPCR, TPM2B_PRIVATE **outPrivate, TPM2B_PUBLIC **outPublic, TPM2B_CREATION_DATA **creationData, TPM2B_DIGEST **creationHash, TPMT_TK_CREATION **creationTicket) = NULL; +TSS2_RC (*sym_Esys_CreatePrimary)(ESYS_CONTEXT *esysContext, ESYS_TR primaryHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_SENSITIVE_CREATE *inSensitive, const TPM2B_PUBLIC *inPublic, const TPM2B_DATA *outsideInfo, const TPML_PCR_SELECTION *creationPCR, ESYS_TR *objectHandle, TPM2B_PUBLIC **outPublic, TPM2B_CREATION_DATA **creationData, TPM2B_DIGEST **creationHash, TPMT_TK_CREATION **creationTicket) = NULL; +void (*sym_Esys_Finalize)(ESYS_CONTEXT **context) = NULL; +TSS2_RC (*sym_Esys_FlushContext)(ESYS_CONTEXT *esysContext, ESYS_TR flushHandle) = NULL; +void (*sym_Esys_Free)(void *ptr) = NULL; +TSS2_RC (*sym_Esys_GetRandom)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, UINT16 bytesRequested, TPM2B_DIGEST **randomBytes) = NULL; +TSS2_RC (*sym_Esys_Initialize)(ESYS_CONTEXT **esys_context, TSS2_TCTI_CONTEXT *tcti, TSS2_ABI_VERSION *abiVersion) = NULL; +TSS2_RC (*sym_Esys_Load)(ESYS_CONTEXT *esysContext, ESYS_TR parentHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_PRIVATE *inPrivate, const TPM2B_PUBLIC *inPublic, ESYS_TR *objectHandle) = NULL; +TSS2_RC (*sym_Esys_PolicyGetDigest)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, TPM2B_DIGEST **policyDigest) = NULL; +TSS2_RC (*sym_Esys_PolicyPCR)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_DIGEST *pcrDigest, const TPML_PCR_SELECTION *pcrs) = NULL; +TSS2_RC (*sym_Esys_StartAuthSession)(ESYS_CONTEXT *esysContext, ESYS_TR tpmKey, ESYS_TR bind, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_NONCE *nonceCaller, TPM2_SE sessionType, const TPMT_SYM_DEF *symmetric, TPMI_ALG_HASH authHash, ESYS_TR *sessionHandle) = NULL; +TSS2_RC (*sym_Esys_Startup)(ESYS_CONTEXT *esysContext, TPM2_SU startupType) = NULL; +TSS2_RC (*sym_Esys_Unseal)(ESYS_CONTEXT *esysContext, ESYS_TR itemHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, TPM2B_SENSITIVE_DATA **outData) = NULL; + +const char* (*sym_Tss2_RC_Decode)(TSS2_RC rc) = NULL; + +TSS2_RC (*sym_Tss2_MU_TPM2B_PRIVATE_Marshal)(TPM2B_PRIVATE const *src, uint8_t buffer[], size_t buffer_size, size_t *offset) = NULL; +TSS2_RC (*sym_Tss2_MU_TPM2B_PRIVATE_Unmarshal)(uint8_t const buffer[], size_t buffer_size, size_t *offset, TPM2B_PRIVATE *dest) = NULL; +TSS2_RC (*sym_Tss2_MU_TPM2B_PUBLIC_Marshal)(TPM2B_PUBLIC const *src, uint8_t buffer[], size_t buffer_size, size_t *offset) = NULL; +TSS2_RC (*sym_Tss2_MU_TPM2B_PUBLIC_Unmarshal)(uint8_t const buffer[], size_t buffer_size, size_t *offset, TPM2B_PUBLIC *dest) = NULL; + +int dlopen_tpm2(void) { + int r, k = 0; + + if (!libtss2_esys_dl) { + _cleanup_(dlclosep) void *dl = NULL; + + dl = dlopen("libtss2-esys.so.0", RTLD_LAZY); + if (!dl) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "TPM2 support is not installed: %s", dlerror()); + + r = dlsym_many_and_warn( + dl, + LOG_DEBUG, + DLSYM_ARG(Esys_Create), + DLSYM_ARG(Esys_CreatePrimary), + DLSYM_ARG(Esys_Finalize), + DLSYM_ARG(Esys_FlushContext), + DLSYM_ARG(Esys_Free), + DLSYM_ARG(Esys_GetRandom), + DLSYM_ARG(Esys_Initialize), + DLSYM_ARG(Esys_Load), + DLSYM_ARG(Esys_PolicyGetDigest), + DLSYM_ARG(Esys_PolicyPCR), + DLSYM_ARG(Esys_StartAuthSession), + DLSYM_ARG(Esys_Startup), + DLSYM_ARG(Esys_Unseal), + NULL); + if (r < 0) + return r; + + libtss2_esys_dl = TAKE_PTR(dl); + k++; + } + + if (!libtss2_rc_dl) { + _cleanup_(dlclosep) void *dl = NULL; + + dl = dlopen("libtss2-rc.so.0", RTLD_LAZY); + if (!dl) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "TPM2 support is not installed: %s", dlerror()); + + r = dlsym_many_and_warn( + dl, + LOG_DEBUG, + DLSYM_ARG(Tss2_RC_Decode), + NULL); + if (r < 0) + return r; + + libtss2_rc_dl = TAKE_PTR(dl); + k++; + } + + if (!libtss2_mu_dl) { + _cleanup_(dlclosep) void *dl = NULL; + + dl = dlopen("libtss2-mu.so.0", RTLD_LAZY); + if (!dl) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "TPM2 support is not installed: %s", dlerror()); + + r = dlsym_many_and_warn( + dl, + LOG_DEBUG, + DLSYM_ARG(Tss2_MU_TPM2B_PRIVATE_Marshal), + DLSYM_ARG(Tss2_MU_TPM2B_PRIVATE_Unmarshal), + DLSYM_ARG(Tss2_MU_TPM2B_PUBLIC_Marshal), + DLSYM_ARG(Tss2_MU_TPM2B_PUBLIC_Unmarshal), + NULL); + if (r < 0) + return r; + + libtss2_mu_dl = TAKE_PTR(dl); + k++; + } + + return k; +} + +struct tpm2_context { + ESYS_CONTEXT *esys_context; + void *tcti_dl; + TSS2_TCTI_CONTEXT *tcti_context; +}; + +static void tpm2_context_destroy(struct tpm2_context *c) { + assert(c); + + if (c->esys_context) + sym_Esys_Finalize(&c->esys_context); + + c->tcti_context = mfree(c->tcti_context); + + if (c->tcti_dl) { + dlclose(c->tcti_dl); + c->tcti_dl = NULL; + } +} + +static inline void Esys_Finalize_wrapper(ESYS_CONTEXT **c) { + /* A wrapper around Esys_Finalize() for use with _cleanup_(). Only reasons we need this wrapper is + * because the function itself warn logs if we'd pass a pointer to NULL, and we don't want that. */ + if (*c) + sym_Esys_Finalize(c); +} + +static inline void Esys_Freep(void *p) { + if (*(void**) p) + sym_Esys_Free(*(void**) p); +} + +static ESYS_TR flush_context_verbose(ESYS_CONTEXT *c, ESYS_TR handle) { + TSS2_RC rc; + + if (!c || handle == ESYS_TR_NONE) + return ESYS_TR_NONE; + + rc = sym_Esys_FlushContext(c, handle); + if (rc != TSS2_RC_SUCCESS) /* We ignore failures here (besides debug logging), since this is called + * in error paths, where we cannot do anything about failures anymore. And + * when it is called in successful codepaths by this time we already did + * what we wanted to do, and got the results we wanted so there's no + * reason to make this fail more loudly than necessary. */ + log_debug("Failed to get flush context of TPM, ignoring: %s", sym_Tss2_RC_Decode(rc)); + + return ESYS_TR_NONE; +} + +static int tpm2_init(const char *device, struct tpm2_context *ret) { + _cleanup_(Esys_Finalize_wrapper) ESYS_CONTEXT *c = NULL; + _cleanup_free_ TSS2_TCTI_CONTEXT *tcti = NULL; + _cleanup_(dlclosep) void *dl = NULL; + TSS2_RC rc; + int r; + + r = dlopen_tpm2(); + if (r < 0) + return log_error_errno(r, "TPM2 support not installed: %m"); + + if (!device) + device = secure_getenv("SYSTEMD_TPM2_DEVICE"); + + if (device) { + const char *param, *driver, *fn; + const TSS2_TCTI_INFO* info; + TSS2_TCTI_INFO_FUNC func; + size_t sz = 0; + + param = strchr(device, ':'); + if (param) { + driver = strndupa(device, param - device); + param++; + } else { + driver = "device"; + param = device; + } + + fn = strjoina("libtss2-tcti-", driver, ".so.0"); + + dl = dlopen(fn, RTLD_NOW); + if (!dl) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to load %s: %s", fn, dlerror()); + + func = dlsym(dl, TSS2_TCTI_INFO_SYMBOL); + if (!func) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to find TCTI info symbol " TSS2_TCTI_INFO_SYMBOL ": %s", + dlerror()); + + info = func(); + if (!info) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unable to get TCTI info data."); + + + log_debug("Loaded TCTI module '%s' (%s) [Version %" PRIu32 "]", info->name, info->description, info->version); + + rc = info->init(NULL, &sz, NULL); + if (rc != TPM2_RC_SUCCESS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to initialize TCTI context: %s", sym_Tss2_RC_Decode(rc)); + + tcti = malloc0(sz); + if (!tcti) + return log_oom(); + + rc = info->init(tcti, &sz, device); + if (rc != TPM2_RC_SUCCESS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to initialize TCTI context: %s", sym_Tss2_RC_Decode(rc)); + } + + rc = sym_Esys_Initialize(&c, tcti, NULL); + if (rc != TSS2_RC_SUCCESS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to initialize TPM context: %s", sym_Tss2_RC_Decode(rc)); + + rc = sym_Esys_Startup(c, TPM2_SU_CLEAR); + if (rc == TPM2_RC_INITIALIZE) + log_debug("TPM already started up."); + else if (rc == TSS2_RC_SUCCESS) + log_debug("TPM successfully started up."); + else + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to start up TPM: %s", sym_Tss2_RC_Decode(rc)); + + *ret = (struct tpm2_context) { + .esys_context = TAKE_PTR(c), + .tcti_context = TAKE_PTR(tcti), + .tcti_dl = TAKE_PTR(dl), + }; + + return 0; +} + +static int tpm2_credit_random(ESYS_CONTEXT *c) { + size_t rps, done = 0; + TSS2_RC rc; + int r; + + assert(c); + + /* Pulls some entropy from the TPM and adds it into the kernel RNG pool. That way we can say that the + * key we will ultimately generate with the kernel random pool is at least as good as the TPM's RNG, + * but likely better. Note that we don't trust the TPM RNG very much, hence do not actually credit + * any entropy. */ + + for (rps = random_pool_size(); rps > 0;) { + _cleanup_(Esys_Freep) TPM2B_DIGEST *buffer = NULL; + + rc = sym_Esys_GetRandom( + c, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + MIN(rps, 32U), /* 32 is supposedly a safe choice, given that AES 256bit keys are this long, and TPM2 baseline requires support for those. */ + &buffer); + if (rc != TSS2_RC_SUCCESS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to acquire entropy from TPM: %s", sym_Tss2_RC_Decode(rc)); + + if (buffer->size == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Zero-sized entropy returned from TPM."); + + r = random_write_entropy(-1, buffer->buffer, buffer->size, false); + if (r < 0) + return log_error_errno(r, "Failed wo write entropy to kernel: %m"); + + done += buffer->size; + rps = LESS_BY(rps, buffer->size); + } + + log_debug("Added %zu bytes of entropy to the kernel random pool.", done); + return 0; +} + +static int tpm2_make_primary( + ESYS_CONTEXT *c, + ESYS_TR *ret_primary) { + + static const TPM2B_SENSITIVE_CREATE primary_sensitive = {}; + static const TPM2B_PUBLIC primary_template = { + .size = sizeof(TPMT_PUBLIC), + .publicArea = { + .type = TPM2_ALG_ECC, + .nameAlg = TPM2_ALG_SHA256, + .objectAttributes = TPMA_OBJECT_RESTRICTED|TPMA_OBJECT_DECRYPT|TPMA_OBJECT_FIXEDTPM|TPMA_OBJECT_FIXEDPARENT|TPMA_OBJECT_SENSITIVEDATAORIGIN|TPMA_OBJECT_USERWITHAUTH, + .parameters = { + .eccDetail = { + .symmetric = { + .algorithm = TPM2_ALG_AES, + .keyBits.aes = 128, + .mode.aes = TPM2_ALG_CFB, + }, + .scheme.scheme = TPM2_ALG_NULL, + .curveID = TPM2_ECC_NIST_P256, + .kdf.scheme = TPM2_ALG_NULL, + }, + }, + }, + }; + static const TPML_PCR_SELECTION creation_pcr = {}; + ESYS_TR primary = ESYS_TR_NONE; + TSS2_RC rc; + + log_debug("Creating primary key on TPM."); + + rc = sym_Esys_CreatePrimary( + c, + ESYS_TR_RH_OWNER, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + &primary_sensitive, + &primary_template, + NULL, + &creation_pcr, + &primary, + NULL, + NULL, + NULL, + NULL); + + if (rc != TSS2_RC_SUCCESS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to generate primary key in TPM: %s", sym_Tss2_RC_Decode(rc)); + + log_debug("Successfully created primary key on TPM."); + + *ret_primary = primary; + return 0; +} + +static int tpm2_make_pcr_session( + ESYS_CONTEXT *c, + uint32_t pcr_mask, + ESYS_TR *ret_session, + TPM2B_DIGEST **ret_policy_digest) { + + static const TPMT_SYM_DEF symmetric = { + .algorithm = TPM2_ALG_AES, + .keyBits = { + .aes = 128 + }, + .mode = { + .aes = TPM2_ALG_CFB, + } + }; + TPML_PCR_SELECTION pcr_selection = { + .count = 1, + .pcrSelections[0].hash = TPM2_ALG_SHA256, + .pcrSelections[0].sizeofSelect = 3, + .pcrSelections[0].pcrSelect[0] = pcr_mask & 0xFF, + .pcrSelections[0].pcrSelect[1] = (pcr_mask >> 8) & 0xFF, + .pcrSelections[0].pcrSelect[2] = (pcr_mask >> 16) & 0xFF, + }; + _cleanup_(Esys_Freep) TPM2B_DIGEST *policy_digest = NULL; + ESYS_TR session = ESYS_TR_NONE; + TSS2_RC rc; + int r; + + assert(c); + + log_debug("Starting authentication session."); + + rc = sym_Esys_StartAuthSession( + c, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + NULL, + TPM2_SE_POLICY, + &symmetric, + TPM2_ALG_SHA256, + &session); + if (rc != TSS2_RC_SUCCESS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to open session in TPM: %s", sym_Tss2_RC_Decode(rc)); + + log_debug("Configuring PCR policy."); + + rc = sym_Esys_PolicyPCR( + c, + session, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + NULL, + &pcr_selection); + if (rc != TSS2_RC_SUCCESS) { + r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to add PCR policy to TPM: %s", sym_Tss2_RC_Decode(rc)); + goto finish; + } + + if (DEBUG_LOGGING || ret_policy_digest) { + log_debug("Acquiring policy digest."); + + rc = sym_Esys_PolicyGetDigest( + c, + session, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + &policy_digest); + + if (rc != TSS2_RC_SUCCESS) { + r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to get policy digest from TPM: %s", sym_Tss2_RC_Decode(rc)); + goto finish; + } + + if (DEBUG_LOGGING) { + _cleanup_free_ char *h = NULL; + + h = hexmem(policy_digest->buffer, policy_digest->size); + if (!h) { + r = log_oom(); + goto finish; + } + + log_debug("Session policy digest: %s", h); + } + } + + if (ret_session) { + *ret_session = session; + session = ESYS_TR_NONE; + } + + if (ret_policy_digest) + *ret_policy_digest = TAKE_PTR(policy_digest); + + r = 0; + +finish: + session = flush_context_verbose(c, session); + return r; +} + +int tpm2_seal( + const char *device, + uint32_t pcr_mask, + void **ret_secret, + size_t *ret_secret_size, + void **ret_blob, + size_t *ret_blob_size, + void **ret_pcr_hash, + size_t *ret_pcr_hash_size) { + + _cleanup_(tpm2_context_destroy) struct tpm2_context c = {}; + _cleanup_(Esys_Freep) TPM2B_DIGEST *policy_digest = NULL; + _cleanup_(Esys_Freep) TPM2B_PRIVATE *private = NULL; + _cleanup_(Esys_Freep) TPM2B_PUBLIC *public = NULL; + static const TPML_PCR_SELECTION creation_pcr = {}; + _cleanup_(erase_and_freep) void *secret = NULL; + _cleanup_free_ void *blob = NULL, *hash = NULL; + TPM2B_SENSITIVE_CREATE hmac_sensitive; + ESYS_TR primary = ESYS_TR_NONE; + TPM2B_PUBLIC hmac_template; + size_t k, blob_size; + usec_t start; + TSS2_RC rc; + int r; + + assert(ret_secret); + assert(ret_secret_size); + assert(ret_blob); + assert(ret_blob_size); + assert(ret_pcr_hash); + assert(ret_pcr_hash_size); + + assert(pcr_mask < (UINT32_C(1) << TPM2_PCRS_MAX)); /* Support 24 PCR banks */ + + /* So here's what we do here: we connect to the TPM2 chip. It persistently contains a "seed" key that + * is randomized when the TPM2 is first initialized or reset and remains stable across boots. We + * generate a "primary" key pair derived from that (RSA). Given the seed remains fixed this will + * result in the same key pair whenever we specify the exact same parameters for it. We then create a + * PCR-bound policy session, which calculates a hash on the current PCR values of the indexes we + * specify. We then generate a randomized key on the host (which is the key we actually enroll in the + * LUKS2 keyslots), which we upload into the TPM2, where it is encrypted with the "primary" key, + * taking the PCR policy session into account. We then download the encrypted key from the TPM2 + * ("sealing") and marshall it into binary form, which is ultimately placed in the LUKS2 JSON header. + * + * The TPM2 "seed" key and "primary" keys never leave the TPM2 chip (and cannot be extracted at + * all). The random key we enroll in LUKS2 we generate on the host using the Linux random device. It + * is stored in the LUKS2 JSON only in encrypted form with the "primary" key of the TPM2 chip, thus + * binding the unlocking to the TPM2 chip. */ + + start = now(CLOCK_MONOTONIC); + + r = tpm2_init(device, &c); + if (r < 0) + return r; + + r = tpm2_make_primary(c.esys_context, &primary); + if (r < 0) + return r; + + r = tpm2_make_pcr_session(c.esys_context, pcr_mask, NULL, &policy_digest); + if (r < 0) + goto finish; + + /* We use a keyed hash object (i.e. HMAC) to store the secret key we want to use for unlocking the + * LUKS2 volume with. We don't ever use for HMAC/keyed hash operations however, we just use it + * because it's a key type that is universally supported and suitable for symmetric binary blobs. */ + hmac_template = (TPM2B_PUBLIC) { + .size = sizeof(TPMT_PUBLIC), + .publicArea = { + .type = TPM2_ALG_KEYEDHASH, + .nameAlg = TPM2_ALG_SHA256, + .objectAttributes = TPMA_OBJECT_FIXEDTPM | TPMA_OBJECT_FIXEDPARENT, + .parameters = { + .keyedHashDetail = { + .scheme.scheme = TPM2_ALG_NULL, + }, + }, + .unique = { + .keyedHash = { + .size = 32, + }, + }, + .authPolicy = *policy_digest, + }, + }; + + hmac_sensitive = (TPM2B_SENSITIVE_CREATE) { + .size = sizeof(hmac_sensitive.sensitive), + .sensitive.data.size = 32, + }; + assert(sizeof(hmac_sensitive.sensitive.data.buffer) >= hmac_sensitive.sensitive.data.size); + + (void) tpm2_credit_random(c.esys_context); + + log_debug("Generating secret key data."); + + r = genuine_random_bytes(hmac_sensitive.sensitive.data.buffer, hmac_sensitive.sensitive.data.size, RANDOM_BLOCK); + if (r < 0) { + log_error_errno(r, "Failed to generate secret key: %m"); + goto finish; + } + + log_debug("Creating HMAC key."); + + rc = sym_Esys_Create( + c.esys_context, + primary, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + &hmac_sensitive, + &hmac_template, + NULL, + &creation_pcr, + &private, + &public, + NULL, + NULL, + NULL); + if (rc != TSS2_RC_SUCCESS) { + r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to generate HMAC key in TPM: %s", sym_Tss2_RC_Decode(rc)); + goto finish; + } + + secret = memdup(hmac_sensitive.sensitive.data.buffer, hmac_sensitive.sensitive.data.size); + explicit_bzero_safe(hmac_sensitive.sensitive.data.buffer, hmac_sensitive.sensitive.data.size); + if (!secret) { + r = log_oom(); + goto finish; + } + + log_debug("Marshalling private and public part of HMAC key."); + + k = ALIGN8(sizeof(*private)) + ALIGN8(sizeof(*public)); /* Some roughly sensible start value */ + for (;;) { + _cleanup_free_ void *buf = NULL; + size_t offset = 0; + + buf = malloc(k); + if (!buf) { + r = log_oom(); + goto finish; + } + + rc = sym_Tss2_MU_TPM2B_PRIVATE_Marshal(private, buf, k, &offset); + if (rc == TSS2_RC_SUCCESS) { + rc = sym_Tss2_MU_TPM2B_PUBLIC_Marshal(public, buf, k, &offset); + if (rc == TSS2_RC_SUCCESS) { + blob = TAKE_PTR(buf); + blob_size = offset; + break; + } + } + if (rc != TSS2_MU_RC_INSUFFICIENT_BUFFER) { + r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to marshal private/public key: %s", sym_Tss2_RC_Decode(rc)); + goto finish; + } + + if (k > SIZE_MAX / 2) { + r = log_oom(); + goto finish; + } + + k *= 2; + } + + hash = memdup(policy_digest->buffer, policy_digest->size); + if (!hash) + return log_oom(); + + if (DEBUG_LOGGING) { + char buf[FORMAT_TIMESPAN_MAX]; + log_debug("Completed TPM2 key sealing in %s.", format_timespan(buf, sizeof(buf), now(CLOCK_MONOTONIC) - start, 1)); + } + + *ret_secret = TAKE_PTR(secret); + *ret_secret_size = hmac_sensitive.sensitive.data.size; + *ret_blob = TAKE_PTR(blob); + *ret_blob_size = blob_size; + *ret_pcr_hash = TAKE_PTR(hash); + *ret_pcr_hash_size = policy_digest->size; + + r = 0; + +finish: + primary = flush_context_verbose(c.esys_context, primary); + return r; +} + +int tpm2_unseal( + const char *device, + uint32_t pcr_mask, + const void *blob, + size_t blob_size, + const void *known_policy_hash, + size_t known_policy_hash_size, + void **ret_secret, + size_t *ret_secret_size) { + + _cleanup_(tpm2_context_destroy) struct tpm2_context c = {}; + ESYS_TR primary = ESYS_TR_NONE, session = ESYS_TR_NONE, hmac_key = ESYS_TR_NONE; + _cleanup_(Esys_Freep) TPM2B_SENSITIVE_DATA* unsealed = NULL; + _cleanup_(Esys_Freep) TPM2B_DIGEST *policy_digest = NULL; + _cleanup_(erase_and_freep) char *secret = NULL; + TPM2B_PRIVATE private = {}; + TPM2B_PUBLIC public = {}; + size_t offset = 0; + TSS2_RC rc; + usec_t start; + int r; + + assert(blob); + assert(blob_size > 0); + assert(known_policy_hash_size == 0 || known_policy_hash); + assert(ret_secret); + assert(ret_secret_size); + + assert(pcr_mask < (UINT32_C(1) << TPM2_PCRS_MAX)); /* Support 24 PCR banks */ + + /* So here's what we do here: We connect to the TPM2 chip. As we do when sealing we generate a + * "primary" key on the TPM2 chip, with the same parameters as well as a PCR-bound policy + * session. Given we pass the same parameters, this will result in the same "primary" key, and same + * policy hash (the latter of course, only if the PCR values didn't change in between). We unmarshal + * the encrypted key we stored in the LUKS2 JSON token header and upload it into the TPM2, where it + * is decrypted if the seed and the PCR policy were right ("unsealing"). We then download the result, + * and use it to unlock the LUKS2 volume. */ + + start = now(CLOCK_MONOTONIC); + + log_debug("Unmarshalling private part of HMAC key."); + + rc = sym_Tss2_MU_TPM2B_PRIVATE_Unmarshal(blob, blob_size, &offset, &private); + if (rc != TSS2_RC_SUCCESS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to unmarshal private key: %s", sym_Tss2_RC_Decode(rc)); + + log_debug("Unmarshalling public part of HMAC key."); + + rc = sym_Tss2_MU_TPM2B_PUBLIC_Unmarshal(blob, blob_size, &offset, &public); + if (rc != TSS2_RC_SUCCESS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to unmarshal public key: %s", sym_Tss2_RC_Decode(rc)); + + r = tpm2_init(device, &c); + if (r < 0) + return r; + + r = tpm2_make_pcr_session(c.esys_context, pcr_mask, &session, &policy_digest); + if (r < 0) + goto finish; + + /* If we know the policy hash to expect, and it doesn't match, we can shortcut things here, and not + * wait until the TPM2 tells us to go away. */ + if (known_policy_hash_size > 0 && + memcmp_nn(policy_digest->buffer, policy_digest->size, known_policy_hash, known_policy_hash_size) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), + "Current policy digest does not match stored policy digest, cancelling TPM2 authentication attempt."); + + r = tpm2_make_primary(c.esys_context, &primary); + if (r < 0) + return r; + + log_debug("Loading HMAC key into TPM."); + + rc = sym_Esys_Load( + c.esys_context, + primary, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE, + &private, + &public, + &hmac_key); + if (rc != TSS2_RC_SUCCESS) { + r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to load HMAC key in TPM: %s", sym_Tss2_RC_Decode(rc)); + goto finish; + } + + log_debug("Unsealing HMAC key."); + + rc = sym_Esys_Unseal( + c.esys_context, + hmac_key, + session, + ESYS_TR_NONE, + ESYS_TR_NONE, + &unsealed); + if (rc != TSS2_RC_SUCCESS) { + r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to unseal HMAC key in TPM: %s", sym_Tss2_RC_Decode(rc)); + goto finish; + } + + secret = memdup(unsealed->buffer, unsealed->size); + explicit_bzero_safe(unsealed->buffer, unsealed->size); + if (!secret) { + r = log_oom(); + goto finish; + } + + if (DEBUG_LOGGING) { + char buf[FORMAT_TIMESPAN_MAX]; + log_debug("Completed TPM2 key unsealing in %s.", format_timespan(buf, sizeof(buf), now(CLOCK_MONOTONIC) - start, 1)); + } + + *ret_secret = TAKE_PTR(secret); + *ret_secret_size = unsealed->size; + + r = 0; + +finish: + primary = flush_context_verbose(c.esys_context, primary); + session = flush_context_verbose(c.esys_context, session); + hmac_key = flush_context_verbose(c.esys_context, hmac_key); + return r; +} + +#endif + +int tpm2_list_devices(void) { +#if HAVE_TPM2 + _cleanup_(table_unrefp) Table *t = NULL; + _cleanup_(closedirp) DIR *d = NULL; + int r; + + r = dlopen_tpm2(); + if (r < 0) + return log_error_errno(r, "TPM2 support is not installed."); + + t = table_new("path", "device", "driver"); + if (!t) + return log_oom(); + + d = opendir("/sys/class/tpmrm"); + if (!d) { + log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, "Failed to open /sys/class/tpmrm: %m"); + if (errno != ENOENT) + return -errno; + } else { + for (;;) { + _cleanup_free_ char *device_path = NULL, *device = NULL, *driver_path = NULL, *driver = NULL, *node = NULL; + struct dirent *de; + + de = readdir_no_dot(d); + if (!de) + break; + + device_path = path_join("/sys/class/tpmrm", de->d_name, "device"); + if (!device_path) + return log_oom(); + + r = readlink_malloc(device_path, &device); + if (r < 0) + log_debug_errno(r, "Failed to read device symlink %s, ignoring: %m", device_path); + else { + driver_path = path_join(device_path, "driver"); + if (!driver_path) + return log_oom(); + + r = readlink_malloc(driver_path, &driver); + if (r < 0) + log_debug_errno(r, "Failed to read driver symlink %s, ignoring: %m", driver_path); + } + + node = path_join("/dev", de->d_name); + if (!node) + return log_oom(); + + r = table_add_many( + t, + TABLE_PATH, node, + TABLE_STRING, device ? last_path_component(device) : NULL, + TABLE_STRING, driver ? last_path_component(driver) : NULL); + if (r < 0) + return table_log_add_error(r); + } + } + + if (table_get_rows(t) <= 1) { + log_info("No suitable TPM2 devices found."); + return 0; + } + + r = table_print(t, stdout); + if (r < 0) + return log_error_errno(r, "Failed to show device table: %m"); + + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "TPM2 not supported on this build."); +#endif +} + +int tpm2_find_device_auto( + int log_level, /* log level when no device is found */ + char **ret) { +#if HAVE_TPM2 + _cleanup_(closedirp) DIR *d = NULL; + int r; + + r = dlopen_tpm2(); + if (r < 0) + return log_error_errno(r, "TPM2 support is not installed."); + + d = opendir("/sys/class/tpmrm"); + if (!d) { + log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, + "Failed to open /sys/class/tpmrm: %m"); + if (errno != ENOENT) + return -errno; + } else { + _cleanup_free_ char *node = NULL; + + for (;;) { + struct dirent *de; + + de = readdir_no_dot(d); + if (!de) + break; + + if (node) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), + "More than one TPM2 (tpmrm) device found."); + + node = path_join("/dev", de->d_name); + if (!node) + return log_oom(); + } + + if (node) { + *ret = TAKE_PTR(node); + return 0; + } + } + + return log_full_errno(log_level, SYNTHETIC_ERRNO(ENODEV), "No TPM2 (tpmrm) device found."); +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "TPM2 not supported on this build."); +#endif +} + +int tpm2_parse_pcrs(const char *s, uint32_t *ret) { + const char *p = s; + uint32_t mask = 0; + int r; + + /* Parses a comma-separated list of PCR indexes */ + + for (;;) { + _cleanup_free_ char *pcr = NULL; + unsigned n; + + r = extract_first_word(&p, &pcr, ",", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == 0) + break; + if (r < 0) + return log_error_errno(r, "Failed to parse PCR list: %s", s); + + r = safe_atou(pcr, &n); + if (r < 0) + return log_error_errno(r, "Failed to parse PCR number: %s", pcr); + if (n >= TPM2_PCRS_MAX) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), + "PCR number out of range (valid range 0…23): %u", n); + + mask |= UINT32_C(1) << n; + } + + *ret = mask; + return 0; +} + +int tpm2_make_luks2_json( + int keyslot, + uint32_t pcr_mask, + const void *blob, + size_t blob_size, + const void *policy_hash, + size_t policy_hash_size, + JsonVariant **ret) { + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *a = NULL; + _cleanup_free_ char *keyslot_as_string = NULL; + JsonVariant* pcr_array[TPM2_PCRS_MAX]; + unsigned n_pcrs = 0; + int r; + + assert(blob || blob_size == 0); + assert(policy_hash || policy_hash_size == 0); + + if (asprintf(&keyslot_as_string, "%i", keyslot) < 0) + return -ENOMEM; + + for (unsigned i = 0; i < ELEMENTSOF(pcr_array); i++) { + if ((pcr_mask & (UINT32_C(1) << i)) == 0) + continue; + + r = json_variant_new_integer(pcr_array + n_pcrs, i); + if (r < 0) { + json_variant_unref_many(pcr_array, n_pcrs); + return -ENOMEM; + } + + n_pcrs++; + } + + r = json_variant_new_array(&a, pcr_array, n_pcrs); + json_variant_unref_many(pcr_array, n_pcrs); + if (r < 0) + return -ENOMEM; + + r = json_build(&v, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("type", JSON_BUILD_STRING("systemd-tpm2")), + JSON_BUILD_PAIR("keyslots", JSON_BUILD_ARRAY(JSON_BUILD_STRING(keyslot_as_string))), + JSON_BUILD_PAIR("tpm2-blob", JSON_BUILD_BASE64(blob, blob_size)), + JSON_BUILD_PAIR("tpm2-pcrs", JSON_BUILD_VARIANT(a)), + JSON_BUILD_PAIR("tpm2-policy-hash", JSON_BUILD_HEX(policy_hash, policy_hash_size)))); + if (r < 0) + return r; + + if (ret) + *ret = TAKE_PTR(v); + + return keyslot; +} diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h new file mode 100644 index 0000000000..82cd186e11 --- /dev/null +++ b/src/shared/tpm2-util.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "json.h" +#include "macro.h" + +#if HAVE_TPM2 + +#include +#include +#include + +extern TSS2_RC (*sym_Esys_Create)(ESYS_CONTEXT *esysContext, ESYS_TR parentHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_SENSITIVE_CREATE *inSensitive, const TPM2B_PUBLIC *inPublic, const TPM2B_DATA *outsideInfo, const TPML_PCR_SELECTION *creationPCR, TPM2B_PRIVATE **outPrivate, TPM2B_PUBLIC **outPublic, TPM2B_CREATION_DATA **creationData, TPM2B_DIGEST **creationHash, TPMT_TK_CREATION **creationTicket); +extern TSS2_RC (*sym_Esys_CreatePrimary)(ESYS_CONTEXT *esysContext, ESYS_TR primaryHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_SENSITIVE_CREATE *inSensitive, const TPM2B_PUBLIC *inPublic, const TPM2B_DATA *outsideInfo, const TPML_PCR_SELECTION *creationPCR, ESYS_TR *objectHandle, TPM2B_PUBLIC **outPublic, TPM2B_CREATION_DATA **creationData, TPM2B_DIGEST **creationHash, TPMT_TK_CREATION **creationTicket); +extern void (*sym_Esys_Finalize)(ESYS_CONTEXT **context); +extern TSS2_RC (*sym_Esys_FlushContext)(ESYS_CONTEXT *esysContext, ESYS_TR flushHandle); +extern void (*sym_Esys_Free)(void *ptr); +extern TSS2_RC (*sym_Esys_GetRandom)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, UINT16 bytesRequested, TPM2B_DIGEST **randomBytes); +extern TSS2_RC (*sym_Esys_Initialize)(ESYS_CONTEXT **esys_context, TSS2_TCTI_CONTEXT *tcti, TSS2_ABI_VERSION *abiVersion); +extern TSS2_RC (*sym_Esys_Load)(ESYS_CONTEXT *esysContext, ESYS_TR parentHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_PRIVATE *inPrivate, const TPM2B_PUBLIC *inPublic, ESYS_TR *objectHandle); +extern TSS2_RC (*sym_Esys_PolicyGetDigest)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, TPM2B_DIGEST **policyDigest); +extern TSS2_RC (*sym_Esys_PolicyPCR)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_DIGEST *pcrDigest, const TPML_PCR_SELECTION *pcrs); +extern TSS2_RC (*sym_Esys_StartAuthSession)(ESYS_CONTEXT *esysContext, ESYS_TR tpmKey, ESYS_TR bind, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_NONCE *nonceCaller, TPM2_SE sessionType, const TPMT_SYM_DEF *symmetric, TPMI_ALG_HASH authHash, ESYS_TR *sessionHandle); +extern TSS2_RC (*sym_Esys_Startup)(ESYS_CONTEXT *esysContext, TPM2_SU startupType); +extern TSS2_RC (*sym_Esys_Unseal)(ESYS_CONTEXT *esysContext, ESYS_TR itemHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, TPM2B_SENSITIVE_DATA **outData); + +extern const char* (*sym_Tss2_RC_Decode)(TSS2_RC rc); + +extern TSS2_RC (*sym_Tss2_MU_TPM2B_PRIVATE_Marshal)(TPM2B_PRIVATE const *src, uint8_t buffer[], size_t buffer_size, size_t *offset); +extern TSS2_RC (*sym_Tss2_MU_TPM2B_PRIVATE_Unmarshal)(uint8_t const buffer[], size_t buffer_size, size_t *offset, TPM2B_PRIVATE *dest); +extern TSS2_RC (*sym_Tss2_MU_TPM2B_PUBLIC_Marshal)(TPM2B_PUBLIC const *src, uint8_t buffer[], size_t buffer_size, size_t *offset); +extern TSS2_RC (*sym_Tss2_MU_TPM2B_PUBLIC_Unmarshal)(uint8_t const buffer[], size_t buffer_size, size_t *offset, TPM2B_PUBLIC *dest); + +int dlopen_tpm2(void); + +int tpm2_seal(const char *device, uint32_t pcr_mask, void **ret_secret, size_t *ret_secret_size, void **ret_blob, size_t *ret_blob_size, void **ret_pcr_hash, size_t *ret_pcr_hash_size); +int tpm2_unseal(const char *device, uint32_t pcr_mask, const void *blob, size_t blob_size, const void *pcr_hash, size_t pcr_hash_size, void **ret_secret, size_t *ret_secret_size); + +#endif + +int tpm2_list_devices(void); +int tpm2_find_device_auto(int log_level, char **ret); + +int tpm2_parse_pcrs(const char *s, uint32_t *ret); + +int tpm2_make_luks2_json(int keyslot, uint32_t pcr_mask, const void *blob, size_t blob_size, const void *policy_hash, size_t policy_hash_size, JsonVariant **ret); + +#define TPM2_PCRS_MAX 24 + +/* Default to PCR 7 only */ +#define TPM2_PCR_MASK_DEFAULT (UINT32_C(1) << 7) From d2fafc423d87680437284c933d15d9edefd207a6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 28 Nov 2020 22:59:21 +0100 Subject: [PATCH 22/30] cryptenroll: support listing and wiping tokens --- meson.build | 5 + src/cryptenroll/cryptenroll-list.c | 126 ++++++++ src/cryptenroll/cryptenroll-list.h | 6 + src/cryptenroll/cryptenroll-wipe.c | 445 +++++++++++++++++++++++++++++ src/cryptenroll/cryptenroll-wipe.h | 12 + src/cryptenroll/cryptenroll.c | 142 +++++++-- src/cryptenroll/cryptenroll.h | 26 ++ 7 files changed, 740 insertions(+), 22 deletions(-) create mode 100644 src/cryptenroll/cryptenroll-list.c create mode 100644 src/cryptenroll/cryptenroll-list.h create mode 100644 src/cryptenroll/cryptenroll-wipe.c create mode 100644 src/cryptenroll/cryptenroll-wipe.h create mode 100644 src/cryptenroll/cryptenroll.h diff --git a/meson.build b/meson.build index 70696a724b..21ab85518f 100644 --- a/meson.build +++ b/meson.build @@ -2434,13 +2434,18 @@ if conf.get('HAVE_LIBCRYPTSETUP') == 1 systemd_cryptenroll_sources = files(''' src/cryptenroll/cryptenroll-fido2.h + src/cryptenroll/cryptenroll-list.c + src/cryptenroll/cryptenroll-list.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-tpm2.h + src/cryptenroll/cryptenroll-wipe.c + src/cryptenroll/cryptenroll-wipe.h src/cryptenroll/cryptenroll.c + src/cryptenroll/cryptenroll.h '''.split()) if conf.get('HAVE_P11KIT') == 1 and conf.get('HAVE_OPENSSL') == 1 diff --git a/src/cryptenroll/cryptenroll-list.c b/src/cryptenroll/cryptenroll-list.c new file mode 100644 index 0000000000..3171973395 --- /dev/null +++ b/src/cryptenroll/cryptenroll-list.c @@ -0,0 +1,126 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cryptenroll-list.h" +#include "cryptenroll.h" +#include "format-table.h" +#include "parse-util.h" + +int list_enrolled(struct crypt_device *cd) { + + struct keyslot_metadata { + int slot; + const char *type; + } *keyslot_metadata = NULL; + size_t n_keyslot_metadata = 0, n_keyslot_metadata_allocated = 0; + _cleanup_(table_unrefp) Table *t = NULL; + 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_allocated, 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 < LUKS2_TOKENS_MAX; 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 filed 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 0000000000..d322988f79 --- /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-wipe.c b/src/cryptenroll/cryptenroll-wipe.c new file mode 100644 index 0000000000..3c4a4e6acb --- /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_freep) 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 < LUKS2_TOKENS_MAX; 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 < LUKS2_TOKENS_MAX; 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, als 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_freep) 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("Unexpected wipe scope"); + } + + /* 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 0000000000..5bcd78391a --- /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 index e3a660d021..fb41bbe4aa 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -4,10 +4,13 @@ #include "ask-password-api.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 "escape.h" #include "libfido2-util.h" @@ -17,32 +20,55 @@ #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-util.h" -typedef enum EnrollType { - ENROLL_PASSWORD, - ENROLL_RECOVERY, - ENROLL_PKCS11, - ENROLL_FIDO2, - ENROLL_TPM2, - _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 uint32_t arg_tpm2_pcr_mask = UINT32_MAX; 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 */ + +assert_cc(sizeof(arg_wipe_slots_mask) * 8 >= _ENROLL_TYPE_MAX); 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 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; @@ -65,6 +91,8 @@ static int help(void) { " Enroll a TPM2 device\n" " --tpm2-pcrs=PCR1,PCR2,PCR3,…\n" " Specifiy TPM2 PCRs to seal against\n" + " --wipe-slot=SLOT1,SLOT2,…\n" + " Wipe specified slots\n" "\nSee the %s for details.\n" , program_invocation_short_name , ansi_highlight(), ansi_normal() @@ -84,6 +112,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_FIDO2_DEVICE, ARG_TPM2_DEVICE, ARG_TPM2_PCRS, + ARG_WIPE_SLOT, }; static const struct option options[] = { @@ -95,6 +124,7 @@ static int parse_argv(int argc, char *argv[]) { { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE }, { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, { "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS }, + { "wipe-slot", required_argument, NULL, ARG_WIPE_SLOT }, {} }; @@ -223,6 +253,60 @@ static int parse_argv(int argc, char *argv[]) { 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, sizeof(int), arg_n_wipe_slots + 1); + if (!a) + return log_oom(); + + arg_wipe_slots = a; + arg_wipe_slots[arg_n_wipe_slots++] = (int) n; + } + } + break; + } + case '?': return -EINVAL; @@ -231,10 +315,6 @@ static int parse_argv(int argc, char *argv[]) { } } - 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."); @@ -373,7 +453,7 @@ 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; + int slot, r; log_show_color(true); log_parse_environment(); @@ -383,37 +463,55 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; - r = prepare_luks(&cd, &vk, &vks); + 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: - r = enroll_password(cd, vk, vks); + slot = enroll_password(cd, vk, vks); break; case ENROLL_RECOVERY: - r = enroll_recovery(cd, vk, vks); + slot = enroll_recovery(cd, vk, vks); break; case ENROLL_PKCS11: - r = enroll_pkcs11(cd, vk, vks, arg_pkcs11_token_uri); + slot = enroll_pkcs11(cd, vk, vks, arg_pkcs11_token_uri); break; case ENROLL_FIDO2: - r = enroll_fido2(cd, vk, vks, arg_fido2_device); + slot = enroll_fido2(cd, vk, vks, arg_fido2_device); break; case ENROLL_TPM2: - r = enroll_tpm2(cd, vk, vks, arg_tpm2_device, arg_tpm2_pcr_mask); + slot = enroll_tpm2(cd, vk, vks, arg_tpm2_device, arg_tpm2_pcr_mask); 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; - return r; + /* 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 0000000000..f8b81f17a5 --- /dev/null +++ b/src/cryptenroll/cryptenroll.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef enum EnrollType { + ENROLL_PASSWORD, + ENROLL_RECOVERY, + ENROLL_PKCS11, + ENROLL_FIDO2, + ENROLL_TPM2, + _ENROLL_TYPE_MAX, + _ENROLL_TYPE_INVALID = -1, +} EnrollType; + +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 = -1, +} 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); From 18843ecc2a81a7d0d7c124fc5d9f9eed8b17bd1d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 4 Dec 2020 17:26:37 +0100 Subject: [PATCH 23/30] cryptsetup: add support for TPM2 unlocking of volumes --- meson.build | 5 + src/cryptsetup/cryptsetup-tpm2.c | 168 ++++++++++++++++++++++ src/cryptsetup/cryptsetup-tpm2.h | 74 ++++++++++ src/cryptsetup/cryptsetup.c | 232 ++++++++++++++++++++++++++++++- 4 files changed, 475 insertions(+), 4 deletions(-) create mode 100644 src/cryptsetup/cryptsetup-tpm2.c create mode 100644 src/cryptsetup/cryptsetup-tpm2.h diff --git a/meson.build b/meson.build index 21ab85518f..2f8463ab8f 100644 --- a/meson.build +++ b/meson.build @@ -2382,6 +2382,7 @@ if conf.get('HAVE_LIBCRYPTSETUP') == 1 src/cryptsetup/cryptsetup-keyfile.c src/cryptsetup/cryptsetup-keyfile.h src/cryptsetup/cryptsetup-pkcs11.h + src/cryptsetup/cryptsetup-tpm2.h src/cryptsetup/cryptsetup.c '''.split()) @@ -2393,6 +2394,10 @@ if conf.get('HAVE_LIBCRYPTSETUP') == 1 systemd_cryptsetup_sources += files('src/cryptsetup/cryptsetup-fido2.c') endif + if conf.get('HAVE_TPM2') == 1 + systemd_cryptsetup_sources += files('src/cryptsetup/cryptsetup-tpm2.c') + endif + executable( 'systemd-cryptsetup', systemd_cryptsetup_sources, diff --git a/src/cryptsetup/cryptsetup-tpm2.c b/src/cryptsetup/cryptsetup-tpm2.c new file mode 100644 index 0000000000..af176c8806 --- /dev/null +++ b/src/cryptsetup/cryptsetup-tpm2.c @@ -0,0 +1,168 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "cryptsetup-tpm2.h" +#include "fileio.h" +#include "hexdecoct.h" +#include "json.h" +#include "parse-util.h" +#include "random-util.h" +#include "tpm2-util.h" + +int acquire_tpm2_key( + const char *volume_name, + const char *device, + uint32_t pcr_mask, + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + const void *key_data, + size_t key_data_size, + const void *policy_hash, + size_t policy_hash_size, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + _cleanup_free_ void *loaded_blob = NULL; + _cleanup_free_ char *auto_device = NULL; + size_t blob_size; + const void *blob; + int r; + + if (!device) { + r = tpm2_find_device_auto(LOG_DEBUG, &auto_device); + if (r == -ENODEV) + return -EAGAIN; /* Tell the caller to wait for a TPM2 device to show up */ + if (r < 0) + return r; + + device = auto_device; + } + + if (key_data) { + blob = key_data; + blob_size = key_data_size; + } else { + _cleanup_free_ char *bindname = NULL; + + /* If we read the salt via AF_UNIX, make this client recognizable */ + if (asprintf(&bindname, "@%" PRIx64"/cryptsetup-tpm2/%s", random_u64(), volume_name) < 0) + return log_oom(); + + r = read_full_file_full( + AT_FDCWD, key_file, + key_file_offset == 0 ? UINT64_MAX : key_file_offset, + key_file_size == 0 ? SIZE_MAX : key_file_size, + READ_FULL_FILE_CONNECT_SOCKET, + bindname, + (char**) &loaded_blob, &blob_size); + if (r < 0) + return r; + + blob = loaded_blob; + } + + return tpm2_unseal(device, pcr_mask, blob, blob_size, policy_hash, policy_hash_size, ret_decrypted_key, ret_decrypted_key_size); +} + +int find_tpm2_auto_data( + struct crypt_device *cd, + uint32_t search_pcr_mask, + int start_token, + uint32_t *ret_pcr_mask, + void **ret_blob, + size_t *ret_blob_size, + void **ret_policy_hash, + size_t *ret_policy_hash_size, + int *ret_keyslot, + int *ret_token) { + + _cleanup_free_ void *blob = NULL, *policy_hash = NULL; + size_t blob_size = 0, policy_hash_size = 0; + int r, keyslot = -1, token = -1; + uint32_t pcr_mask = 0; + + assert(cd); + + for (token = start_token; token < LUKS2_TOKENS_MAX; token++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + JsonVariant *w, *e; + + 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"); + + w = json_variant_by_key(v, "tpm2-pcrs"); + if (!w || !json_variant_is_array(w)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "TPM2 token data lacks 'tpm2-pcrs' field."); + + assert(pcr_mask == 0); + JSON_VARIANT_ARRAY_FOREACH(e, w) { + uintmax_t u; + + if (!json_variant_is_number(e)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "TPM2 PCR is not a number."); + + u = json_variant_unsigned(e); + if (u >= TPM2_PCRS_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "TPM2 PCR number out of range."); + + pcr_mask |= UINT32_C(1) << u; + } + + if (search_pcr_mask != UINT32_MAX && + search_pcr_mask != pcr_mask) /* PCR mask doesn't match what is configured, ignore this entry */ + continue; + + assert(!blob); + w = json_variant_by_key(v, "tpm2-blob"); + if (!w || !json_variant_is_string(w)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "TPM2 token data lacks 'tpm2-blob' field."); + + r = unbase64mem(json_variant_string(w), (size_t) -1, &blob, &blob_size); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid base64 data in 'tpm2-blob' field."); + + assert(!policy_hash); + 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_t) -1, &policy_hash, &policy_hash_size); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid base64 data in 'tpm2-policy-hash' field."); + + assert(keyslot < 0); + keyslot = cryptsetup_get_keyslot_from_token(v); + if (keyslot < 0) + return log_error_errno(keyslot, "Failed to extract keyslot index from TPM2 JSON data: %m"); + + break; + } + + if (!blob) + return log_error_errno(SYNTHETIC_ERRNO(ENXIO), + "No valid TPM2 token data found."); + + if (start_token <= 0) + log_info("Automatically discovered security TPM2 token unlocks volume."); + + *ret_pcr_mask = pcr_mask; + *ret_blob = TAKE_PTR(blob); + *ret_blob_size = blob_size; + *ret_policy_hash = TAKE_PTR(policy_hash); + *ret_policy_hash_size = policy_hash_size; + *ret_keyslot = keyslot; + *ret_token = token; + + return 0; +} diff --git a/src/cryptsetup/cryptsetup-tpm2.h b/src/cryptsetup/cryptsetup-tpm2.h new file mode 100644 index 0000000000..8ddf301a63 --- /dev/null +++ b/src/cryptsetup/cryptsetup-tpm2.h @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "cryptsetup-util.h" +#include "log.h" +#include "time-util.h" + +#if HAVE_TPM2 + +int acquire_tpm2_key( + const char *volume_name, + const char *device, + uint32_t pcr_mask, + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + const void *key_data, + size_t key_data_size, + const void *policy_hash, + size_t policy_hash_size, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size); + +int find_tpm2_auto_data( + struct crypt_device *cd, + uint32_t search_pcr_mask, + int start_token, + uint32_t *ret_pcr_mask, + void **ret_blob, + size_t *ret_blob_size, + void **ret_policy_hash, + size_t *ret_policy_hash_size, + int *ret_keyslot, + int *ret_token); + +#else + +static inline int acquire_tpm2_key( + const char *volume_name, + const char *device, + uint32_t pcr_mask, + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + const void *key_data, + size_t key_data_size, + const void *policy_hash, + size_t policy_hash_size, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "TPM2 support not available."); +} + +static inline int find_tpm2_auto_data( + struct crypt_device *cd, + uint32_t search_pcr_mask, + int start_token, + uint32_t *ret_pcr_mask, + void **ret_blob, + size_t *ret_blob_size, + void **ret_policy_hash, + size_t *ret_policy_hash_size, + int *ret_keyslot, + int *ret_token) { + + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "TPM2 support not available."); +} + +#endif diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index d9df81b74a..538cda9c86 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -14,6 +14,7 @@ #include "cryptsetup-fido2.h" #include "cryptsetup-keyfile.h" #include "cryptsetup-pkcs11.h" +#include "cryptsetup-tpm2.h" #include "cryptsetup-util.h" #include "device-util.h" #include "escape.h" @@ -34,6 +35,7 @@ #include "random-util.h" #include "string-util.h" #include "strv.h" +#include "tpm2-util.h" /* internal helper */ #define ANY_LUKS "LUKS" @@ -72,6 +74,9 @@ static bool arg_fido2_device_auto = false; static void *arg_fido2_cid = NULL; static size_t arg_fido2_cid_size = 0; static char *arg_fido2_rp_id = NULL; +static char *arg_tpm2_device = NULL; +static bool arg_tpm2_device_auto = false; +static uint32_t arg_tpm2_pcr_mask = UINT32_MAX; STATIC_DESTRUCTOR_REGISTER(arg_cipher, freep); STATIC_DESTRUCTOR_REGISTER(arg_hash, freep); @@ -81,6 +86,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_uri, freep); STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_fido2_cid, freep); STATIC_DESTRUCTOR_REGISTER(arg_fido2_rp_id, freep); +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); /* Options Debian's crypttab knows we don't: @@ -327,6 +333,36 @@ static int parse_one_option(const char *option) { if (r < 0) return log_oom(); + } else if ((val = startswith(option, "tpm2-device="))) { + + if (streq(val, "auto")) { + arg_tpm2_device = mfree(arg_tpm2_device); + arg_tpm2_device_auto = true; + } else { + r = free_and_strdup(&arg_tpm2_device, val); + if (r < 0) + return log_oom(); + + arg_tpm2_device_auto = false; + } + + } else if ((val = startswith(option, "tpm2-pcrs="))) { + + if (isempty(val)) + arg_tpm2_pcr_mask = 0; + else { + uint32_t mask; + + r = tpm2_parse_pcrs(val, &mask); + if (r < 0) + return r; + + if (arg_tpm2_pcr_mask == UINT32_MAX) + arg_tpm2_pcr_mask = mask; + else + arg_tpm2_pcr_mask |= mask; + } + } else if ((val = startswith(option, "try-empty-password="))) { r = parse_boolean(val); @@ -556,10 +592,10 @@ static int attach_tcrypt( assert(name); assert(key_file || key_data || !strv_isempty(passwords)); - if (arg_pkcs11_uri || arg_pkcs11_uri_auto || arg_fido2_device || arg_fido2_device_auto) + if (arg_pkcs11_uri || arg_pkcs11_uri_auto || arg_fido2_device || arg_fido2_device_auto || arg_tpm2_device || arg_tpm2_device_auto) /* Ask for a regular password */ return log_error_errno(SYNTHETIC_ERRNO(EAGAIN), - "Sorry, but tcrypt devices are currently not supported in conjunction with pkcs11/fido2 support."); + "Sorry, but tcrypt devices are currently not supported in conjunction with pkcs11/fido2/tpm2 support."); if (arg_tcrypt_hidden) params.flags |= CRYPT_TCRYPT_HIDDEN_HEADER; @@ -907,6 +943,190 @@ static int attach_luks_or_plain_or_bitlk_by_pkcs11( return 0; } +static int make_tpm2_device_monitor(sd_event *event, sd_device_monitor **ret) { + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL; + int r; + + assert(ret); + + r = sd_device_monitor_new(&monitor); + if (r < 0) + return log_error_errno(r, "Failed to allocate device monitor: %m"); + + r = sd_device_monitor_filter_add_match_subsystem_devtype(monitor, "tpmrm", NULL); + if (r < 0) + return log_error_errno(r, "Failed to configure device monitor: %m"); + + r = sd_device_monitor_attach_event(monitor, event); + if (r < 0) + return log_error_errno(r, "Failed to attach device monitor: %m"); + + r = sd_device_monitor_start(monitor, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + + *ret = TAKE_PTR(monitor); + return 0; +} + +static int attach_luks_or_plain_or_bitlk_by_tpm2( + struct crypt_device *cd, + const char *name, + const char *key_file, + const void *key_data, + size_t key_data_size, + usec_t until, + uint32_t flags, + bool pass_volume_key) { + + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL; + _cleanup_(erase_and_freep) void *decrypted_key = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + _cleanup_free_ char *friendly = NULL; + int keyslot = arg_key_slot, r; + size_t decrypted_key_size; + + assert(cd); + assert(name); + assert(arg_tpm2_device || arg_tpm2_device_auto); + + friendly = friendly_disk_name(crypt_get_device_name(cd), name); + if (!friendly) + return log_oom(); + + for (;;) { + bool processed = false; + + if (key_file || key_data) { + /* If key data is specified, use that */ + + r = acquire_tpm2_key( + name, + arg_tpm2_device, + arg_tpm2_pcr_mask == UINT32_MAX ? TPM2_PCR_MASK_DEFAULT : arg_tpm2_pcr_mask, + key_file, arg_keyfile_size, arg_keyfile_offset, + key_data, key_data_size, + NULL, 0, /* we don't know the policy hash */ + &decrypted_key, &decrypted_key_size); + if (r >= 0) + break; + if (r != -EAGAIN) /* EAGAIN means: no tpm2 chip found */ + return r; + } else { + _cleanup_free_ void *blob = NULL, *policy_hash = NULL; + size_t blob_size, policy_hash_size; + bool found_some = false; + int token = 0; /* first token to look at */ + + /* If no key data is specified, look for it in the header. In order to support + * software upgrades we'll iterate through all suitable tokens, maybe one of them + * works. */ + + for (;;) { + uint32_t pcr_mask; + + r = find_tpm2_auto_data( + cd, + arg_tpm2_pcr_mask, /* if != UINT32_MAX we'll only look for tokens with this PCR mask */ + token, /* search for the token with this index, or any later index than this */ + &pcr_mask, + &blob, &blob_size, + &policy_hash, &policy_hash_size, + &keyslot, + &token); + if (r == -ENXIO) { + /* No futher TPM2 tokens found in the LUKS2 header.*/ + if (found_some) + return log_debug_errno(SYNTHETIC_ERRNO(EAGAIN), + "No TPM2 metadata matching the current system state found in LUKS2 header, falling back to traditional unlocking."); + else + return log_debug_errno(SYNTHETIC_ERRNO(EAGAIN), + "No TPM2 metadata enrolled in LUKS2 header, falling back to traditional unlocking."); + } + if (r < 0) + return r; + + found_some = true; + + r = acquire_tpm2_key( + name, + arg_tpm2_device, + pcr_mask, + NULL, 0, 0, /* no key file */ + blob, blob_size, + policy_hash, policy_hash_size, + &decrypted_key, &decrypted_key_size); + if (r != -EPERM) + break; + + token++; /* try a different token next time */ + } + + if (r >= 0) + break; + if (r != -EAGAIN) /* EAGAIN means: no tpm2 chip found */ + return r; + } + + if (!monitor) { + /* We didn't find the TPM2 device. In this case, watch for it via udev. Let's create + * an event loop and monitor first. */ + + assert(!event); + + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to allocate event loop: %m"); + + r = make_tpm2_device_monitor(event, &monitor); + if (r < 0) + return r; + + log_info("TPM2 device not present for unlocking %s, waiting for it to become available.", friendly); + + /* Let's immediately rescan in case the device appeared in the time we needed + * to create and configure the monitor */ + continue; + } + + for (;;) { + /* Wait for one event, and then eat all subsequent events until there are no + * further ones */ + r = sd_event_run(event, processed ? 0 : UINT64_MAX); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + if (r == 0) + break; + + processed = true; + } + + log_debug("Got one or more potentially relevant udev events, rescanning for TPM2..."); + } + + if (pass_volume_key) + r = crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); + else { + _cleanup_(erase_and_freep) char *base64_encoded = NULL; + + /* Before using this key as passphrase we base64 encode it, for compat with homed */ + + r = base64mem(decrypted_key, decrypted_key_size, &base64_encoded); + if (r < 0) + return log_oom(); + + r = crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, strlen(base64_encoded), flags); + } + if (r == -EPERM) { + log_error_errno(r, "Failed to activate with TPM2 decrypted key. (Key incorrect?)"); + return -EAGAIN; /* log actual error, but return EAGAIN */ + } + if (r < 0) + return log_error_errno(r, "Failed to activate with TPM2 acquired key: %m"); + + return 0; +} + static int attach_luks_or_plain_or_bitlk_by_key_data( struct crypt_device *cd, const char *name, @@ -1083,6 +1303,8 @@ static int attach_luks_or_plain_or_bitlk( crypt_get_volume_key_size(cd)*8, crypt_get_device_name(cd)); + if (arg_tpm2_device || arg_tpm2_device_auto) + return attach_luks_or_plain_or_bitlk_by_tpm2(cd, name, key_file, key_data, key_data_size, until, flags, pass_volume_key); if (arg_fido2_device || arg_fido2_device_auto) return attach_luks_or_plain_or_bitlk_by_fido2(cd, name, key_file, key_data, key_data_size, until, flags, pass_volume_key); if (arg_pkcs11_uri || arg_pkcs11_uri_auto) @@ -1305,14 +1527,14 @@ static int run(int argc, char *argv[]) { /* When we were able to acquire multiple keys, let's always process them in this order: * - * 1. A key acquired via PKCS#11 or FIDO2 token + * 1. A key acquired via PKCS#11 or FIDO2 token, or TPM2 chip * 2. The discovered key: i.e. key_data + key_data_size * 3. The configured key: i.e. key_file + arg_keyfile_offset + arg_keyfile_size * 4. The empty password, in case arg_try_empty_password is set * 5. We enquire the user for a password */ - if (!key_file && !key_data && !arg_pkcs11_uri && !arg_pkcs11_uri_auto && !arg_fido2_device && !arg_fido2_device_auto) { + if (!key_file && !key_data && !arg_pkcs11_uri && !arg_pkcs11_uri_auto && !arg_fido2_device && !arg_fido2_device_auto && !arg_tpm2_device && !arg_tpm2_device_auto) { if (arg_try_empty_password) { /* Hmm, let's try an empty password now, but only once */ @@ -1353,6 +1575,8 @@ static int run(int argc, char *argv[]) { arg_pkcs11_uri_auto = false; arg_fido2_device = mfree(arg_fido2_device); arg_fido2_device_auto = false; + arg_tpm2_device = mfree(arg_tpm2_device); + arg_tpm2_device_auto = false; } if (arg_tries != 0 && tries >= arg_tries) From 5f0ab16198890424d8db95effc84bc4905e8eaf3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 8 Dec 2020 14:39:49 +0100 Subject: [PATCH 24/30] string-table: add private version of lookup macro with boolean fallback --- src/basic/string-table.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/basic/string-table.h b/src/basic/string-table.h index ddc9b9a559..ae4ea145d3 100644 --- a/src/basic/string-table.h +++ b/src/basic/string-table.h @@ -84,6 +84,7 @@ ssize_t string_table_lookup(const char * const *table, size_t len, const char *k #define DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(name,type) _DEFINE_STRING_TABLE_LOOKUP_FROM_STRING(name,type,static) #define DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(name,type,yes) _DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(name,type,yes,) +#define DEFINE_PRIVATE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(name,type,yes) _DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(name,type,yes,static) /* For string conversions where numbers are also acceptable */ #define DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(name,type,max) \ From 889914ef6cfe4519b70fa2f724ed201b1c90ad1e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 8 Dec 2020 15:12:29 +0100 Subject: [PATCH 25/30] repart: optionally lock encrypted partitions to TPM2 This useful for bootstrapping encrypted systems: on first boot let's create a /var/ partition that is locked to the local TPM2. --- src/partition/repart.c | 161 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 141 insertions(+), 20 deletions(-) diff --git a/src/partition/repart.c b/src/partition/repart.c index ddb9476cec..02d7f227f8 100644 --- a/src/partition/repart.c +++ b/src/partition/repart.c @@ -34,6 +34,7 @@ #include "format-util.h" #include "fs-util.h" #include "gpt.h" +#include "hexdecoct.h" #include "id128-util.h" #include "json.h" #include "list.h" @@ -54,9 +55,11 @@ #include "specifier.h" #include "stat-util.h" #include "stdio-util.h" +#include "string-table.h" #include "string-util.h" #include "strv.h" #include "terminal-util.h" +#include "tpm2-util.h" #include "user-util.h" #include "utf8.h" @@ -107,15 +110,27 @@ static bool arg_json = false; static JsonFormatFlags arg_json_format_flags = 0; static void *arg_key = NULL; static size_t arg_key_size = 0; +static char *arg_tpm2_device = NULL; +static uint32_t arg_tpm2_pcr_mask = UINT32_MAX; STATIC_DESTRUCTOR_REGISTER(arg_root, freep); STATIC_DESTRUCTOR_REGISTER(arg_definitions, freep); STATIC_DESTRUCTOR_REGISTER(arg_key, erase_and_freep); +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); typedef struct Partition Partition; typedef struct FreeArea FreeArea; typedef struct Context Context; +typedef enum EncryptMode { + ENCRYPT_OFF, + ENCRYPT_KEY_FILE, + ENCRYPT_TPM2, + ENCRYPT_KEY_FILE_TPM2, + _ENCRYPT_MODE_MAX, + _ENCRYPT_MODE_INVALID = -1, +} EncryptMode; + struct Partition { char *definition_path; @@ -149,7 +164,7 @@ struct Partition { char *format; char **copy_files; - bool encrypt; + EncryptMode encrypt; LIST_FIELDS(Partition, partitions); }; @@ -177,6 +192,15 @@ struct Context { sd_id128_t seed; }; +static const char *encrypt_mode_table[_ENCRYPT_MODE_MAX] = { + [ENCRYPT_OFF] = "off", + [ENCRYPT_KEY_FILE] = "key-file", + [ENCRYPT_TPM2] = "tpm2", + [ENCRYPT_KEY_FILE_TPM2] = "key-file+tpm2", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(encrypt_mode, EncryptMode, ENCRYPT_KEY_FILE); + static uint64_t round_down_size(uint64_t v, uint64_t p) { return (v / p) * p; } @@ -388,12 +412,12 @@ static uint64_t partition_min_size(const Partition *p) { if (!PARTITION_EXISTS(p)) { uint64_t d = 0; - if (p->encrypt) + if (p->encrypt != ENCRYPT_OFF) d += round_up_size(LUKS2_METADATA_SIZE, 4096); if (p->copy_blocks_size != UINT64_MAX) d += round_up_size(p->copy_blocks_size, 4096); - else if (p->format || p->encrypt) { + else if (p->format || p->encrypt != ENCRYPT_OFF) { uint64_t f; /* If we shall synthesize a file system, take minimal fs size into account (assumed to be 4K if not known) */ @@ -1137,6 +1161,8 @@ static int config_parse_copy_files( return 0; } +static DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT(config_parse_encrypt, encrypt_mode, EncryptMode, ENCRYPT_OFF, "Invalid encryption mode"); + static int partition_read_definition(Partition *p, const char *path) { ConfigTableItem table[] = { @@ -1154,7 +1180,7 @@ static int partition_read_definition(Partition *p, const char *path) { { "Partition", "CopyBlocks", config_parse_path, 0, &p->copy_blocks_path }, { "Partition", "Format", config_parse_fstype, 0, &p->format }, { "Partition", "CopyFiles", config_parse_copy_files, 0, p }, - { "Partition", "Encrypt", config_parse_bool, 0, &p->encrypt }, + { "Partition", "Encrypt", config_parse_encrypt, 0, &p->encrypt }, {} }; int r; @@ -1188,7 +1214,7 @@ static int partition_read_definition(Partition *p, const char *path) { return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL), "Format=swap and CopyFiles= cannot be combined, refusing."); - if (!p->format && (!strv_isempty(p->copy_files) || (p->encrypt && !p->copy_blocks_path))) { + if (!p->format && (!strv_isempty(p->copy_files) || (p->encrypt != ENCRYPT_OFF && !p->copy_blocks_path))) { /* Pick "ext4" as file system if we are configured to copy files or encrypt the device */ p->format = strdup("ext4"); if (!p->format) @@ -2376,7 +2402,9 @@ static int partition_encrypt( int r; assert(p); - assert(p->encrypt); + assert(p->encrypt != ENCRYPT_OFF); + + log_debug("Encryption mode for partition %" PRIu64 ": %s", p->partno, encrypt_mode_to_string(p->encrypt)); r = dlopen_cryptsetup(); if (r < 0) @@ -2425,15 +2453,61 @@ static int partition_encrypt( if (r < 0) return log_error_errno(r, "Failed to LUKS2 format future partition: %m"); - r = sym_crypt_keyslot_add_by_volume_key( - cd, - CRYPT_ANY_SLOT, - volume_key, - volume_key_size, - strempty(arg_key), - arg_key_size); - if (r < 0) - return log_error_errno(r, "Failed to add LUKS2 key: %m"); + if (IN_SET(p->encrypt, ENCRYPT_KEY_FILE, ENCRYPT_KEY_FILE_TPM2)) { + r = sym_crypt_keyslot_add_by_volume_key( + cd, + CRYPT_ANY_SLOT, + volume_key, + volume_key_size, + strempty(arg_key), + arg_key_size); + if (r < 0) + return log_error_errno(r, "Failed to add LUKS2 key: %m"); + } + + if (IN_SET(p->encrypt, ENCRYPT_TPM2, ENCRYPT_KEY_FILE_TPM2)) { +#if HAVE_TPM2 + _cleanup_(erase_and_freep) char *base64_encoded = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(erase_and_freep) void *secret = NULL; + _cleanup_free_ void *blob = NULL, *hash = NULL; + size_t secret_size, blob_size, hash_size; + int keyslot; + + r = tpm2_seal(arg_tpm2_device, arg_tpm2_pcr_mask, &secret, &secret_size, &blob, &blob_size, &hash, &hash_size); + if (r < 0) + return log_error_errno(r, "Failed to seal to TPM2: %m"); + + 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 = sym_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 TPM2 key to %s: %m", node); + + r = tpm2_make_luks2_json(keyslot, arg_tpm2_pcr_mask, blob, blob_size, hash, hash_size, &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"); +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Support for TPM2 enrollment not enabled."); +#endif + } r = sym_crypt_activate_by_volume_key( cd, @@ -2521,7 +2595,7 @@ static int context_copy_blocks(Context *context) { if (whole_fd < 0) assert_se((whole_fd = fdisk_get_devfd(context->fdisk_context)) >= 0); - if (p->encrypt) { + if (p->encrypt != ENCRYPT_OFF) { r = loop_device_make(whole_fd, O_RDWR, p->offset, p->new_size, 0, &d); if (r < 0) return log_error_errno(r, "Failed to make loopback device of future partition %" PRIu64 ": %m", p->partno); @@ -2554,7 +2628,7 @@ static int context_copy_blocks(Context *context) { if (fsync(target_fd) < 0) return log_error_errno(r, "Failed to synchronize copied data blocks: %m"); - if (p->encrypt) { + if (p->encrypt != ENCRYPT_OFF) { encrypted_dev_fd = safe_close(encrypted_dev_fd); r = deactivate_luks(cd, encrypted); @@ -2743,7 +2817,7 @@ static int context_mkfs(Context *context) { if (r < 0) return log_error_errno(r, "Failed to lock loopback device: %m"); - if (p->encrypt) { + if (p->encrypt != ENCRYPT_OFF) { r = partition_encrypt(p, d->node, &cd, &encrypted, &encrypted_dev_fd); if (r < 0) return log_error_errno(r, "Failed to encrypt device: %m"); @@ -2773,7 +2847,7 @@ static int context_mkfs(Context *context) { log_info("Successfully formatted future partition %" PRIu64 ".", p->partno); /* The file system is now created, no need to delay udev further */ - if (p->encrypt) + if (p->encrypt != ENCRYPT_OFF) if (flock(encrypted_dev_fd, LOCK_UN) < 0) return log_error_errno(errno, "Failed to unlock LUKS device: %m"); @@ -2788,7 +2862,7 @@ static int context_mkfs(Context *context) { * if we don't sync before detaching a block device the in-flight sectors possibly won't hit * the disk. */ - if (p->encrypt) { + if (p->encrypt != ENCRYPT_OFF) { if (fsync(encrypted_dev_fd) < 0) return log_error_errno(r, "Failed to synchronize LUKS volume: %m"); encrypted_dev_fd = safe_close(encrypted_dev_fd); @@ -3420,6 +3494,9 @@ static int help(void) { " --root=PATH Operate relative to root path\n" " --definitions=DIR Find partitions in specified directory\n" " --key-file=PATH Key to use when encrypting partitions\n" + " --tpm2-device=PATH Path to TPM2 device node to use\n" + " --tpm2-pcrs=PCR1,PCR2,…\n" + " TPM2 PCR indexes to use for TPM2 enrollment\n" " --seed=UUID 128bit seed UUID to derive all UUIDs from\n" " --size=BYTES Grow loopback file to specified size\n" " --json=pretty|short|off\n" @@ -3449,6 +3526,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_SIZE, ARG_JSON, ARG_KEY_FILE, + ARG_TPM2_DEVICE, + ARG_TPM2_PCRS, }; static const struct option options[] = { @@ -3466,6 +3545,8 @@ static int parse_argv(int argc, char *argv[]) { { "size", required_argument, NULL, ARG_SIZE }, { "json", required_argument, NULL, ARG_JSON }, { "key-file", required_argument, NULL, ARG_KEY_FILE }, + { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, + { "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS }, {} }; @@ -3635,6 +3716,43 @@ static int parse_argv(int argc, char *argv[]) { break; } + case ARG_TPM2_DEVICE: { + _cleanup_free_ char *device = NULL; + + if (streq(optarg, "list")) + return tpm2_list_devices(); + + if (!streq(optarg, "auto")) { + device = strdup(optarg); + if (!device) + return log_oom(); + } + + free(arg_tpm2_device); + arg_tpm2_device = TAKE_PTR(device); + break; + } + + case ARG_TPM2_PCRS: { + uint32_t mask; + + if (isempty(optarg)) { + arg_tpm2_pcr_mask = 0; + break; + } + + r = tpm2_parse_pcrs(optarg, &mask); + if (r < 0) + return r; + + if (arg_tpm2_pcr_mask == UINT32_MAX) + arg_tpm2_pcr_mask = mask; + else + arg_tpm2_pcr_mask |= mask; + + break; + } + case '?': return -EINVAL; @@ -3667,6 +3785,9 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "A path to a device node or loopback file must be specified when --empty=force, --empty=require or --empty=create are used."); + if (arg_tpm2_pcr_mask == UINT32_MAX) + arg_tpm2_pcr_mask = TPM2_PCR_MASK_DEFAULT; + return 1; } From a60d5b2f38b2e74bd8273ce81031b7b828c90202 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 9 Dec 2020 21:13:58 +0100 Subject: [PATCH 26/30] test: add tpm2 and fido2 libs to dlopen test --- src/test/test-dlopen-so.c | 10 ++++++++++ test/test-functions | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/test/test-dlopen-so.c b/src/test/test-dlopen-so.c index 6436dc600f..8e1ce6a0d0 100644 --- a/src/test/test-dlopen-so.c +++ b/src/test/test-dlopen-so.c @@ -5,11 +5,13 @@ #include "cryptsetup-util.h" #include "idn-util.h" +#include "libfido2-util.h" #include "macro.h" #include "main-func.h" #include "pwquality-util.h" #include "qrcode-util.h" #include "tests.h" +#include "tpm2-util.h" static int run(int argc, char **argv) { test_setup_logging(LOG_DEBUG); @@ -34,6 +36,14 @@ static int run(int argc, char **argv) { assert_se(dlopen_qrencode() >= 0); #endif +#if HAVE_TPM2 + assert_se(dlopen_tpm2() >= 0); +#endif + +#if HAVE_LIBFIDO2 + assert_se(dlopen_libfido2() >= 0); +#endif + return 0; } diff --git a/test/test-functions b/test/test-functions index ce2c42b954..482cb7b490 100644 --- a/test/test-functions +++ b/test/test-functions @@ -697,7 +697,7 @@ install_missing_libraries() { # A number of dependencies is now optional via dlopen, so the install # script will not pick them up, since it looks at linkage. - for lib in libcryptsetup libidn libidn2 pwquality libqrencode; do + for lib in libcryptsetup libidn libidn2 pwquality libqrencode tss2-esys tss2-rc tss2-mu libfido2; do if pkg-config --exists ${lib}; then path=$(pkg-config --variable=libdir ${lib}) if ! [[ ${lib} =~ ^lib ]]; then From 1abaa197814f21fa452eee2f9cf32cfc770908f4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 17 Dec 2020 13:55:50 +0100 Subject: [PATCH 27/30] fido2: when listing fido2/hmac-secret devices, actually validate feature set --- src/shared/libfido2-util.c | 49 ++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/shared/libfido2-util.c b/src/shared/libfido2-util.c index 879429f63c..22b8aba07e 100644 --- a/src/shared/libfido2-util.c +++ b/src/shared/libfido2-util.c @@ -130,6 +130,7 @@ int dlopen_libfido2(void) { static int verify_features( fido_dev_t *d, const char *path, + int log_level, /* the log level to use when device is not FIDO2 with hmac-secret */ bool *ret_has_rk, bool *ret_has_client_pin, bool *ret_has_up, @@ -147,7 +148,8 @@ static int verify_features( assert(path); if (!sym_fido_dev_is_fido2(d)) - return log_error_errno(SYNTHETIC_ERRNO(ENODEV), + return log_full_errno(log_level, + SYNTHETIC_ERRNO(ENODEV), "Specified device %s is not a FIDO2 device.", path); di = sym_fido_cbor_info_new(); @@ -183,7 +185,8 @@ static int verify_features( } if (!found_extension) - return log_error_errno(SYNTHETIC_ERRNO(ENODEV), + return log_full_errno(log_level, + SYNTHETIC_ERRNO(ENODEV), "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", path); log_debug("Has rk ('Resident Key') support: %s\n" @@ -243,7 +246,7 @@ static int fido2_use_hmac_hash_specific_token( return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to open FIDO2 device %s: %s", path, sym_fido_strerr(r)); - r = verify_features(d, path, NULL, &has_client_pin, &has_up, NULL); + r = verify_features(d, path, LOG_ERR, NULL, &has_client_pin, &has_up, NULL); if (r < 0) return r; @@ -496,7 +499,7 @@ int fido2_generate_hmac_hash( return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to open FIDO2 device %s: %s", device, sym_fido_strerr(r)); - r = verify_features(d, device, &has_rk, &has_client_pin, &has_up, &has_uv); + r = verify_features(d, device, LOG_ERR, &has_rk, &has_client_pin, &has_up, &has_uv); if (r < 0) return r; @@ -697,6 +700,30 @@ int fido2_generate_hmac_hash( } #endif +#if HAVE_LIBFIDO2 +static int check_device_is_fido2_with_hmac_secret(const char *path) { + _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL; + int r; + + d = sym_fido_dev_new(); + if (!d) + return log_oom(); + + r = sym_fido_dev_open(d, path); + if (r != FIDO_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Failed to open FIDO2 device %s: %s", path, sym_fido_strerr(r)); + + r = verify_features(d, path, LOG_DEBUG, NULL, NULL, NULL, NULL); + if (r == -ENODEV) /* Not a FIDO2 device, or not implementing 'hmac-secret' */ + return false; + if (r < 0) + return r; + + return true; +} +#endif + int fido2_list_devices(void) { #if HAVE_LIBFIDO2 _cleanup_(table_unrefp) Table *t = NULL; @@ -740,6 +767,12 @@ int fido2_list_devices(void) { goto finish; } + r = check_device_is_fido2_with_hmac_secret(sym_fido_dev_info_path(entry)); + if (r < 0) + goto finish; + if (!r) + continue; + r = table_add_many( t, TABLE_PATH, sym_fido_dev_info_path(entry), @@ -807,6 +840,14 @@ int fido2_find_device_auto(char **ret) { goto finish; } + r = check_device_is_fido2_with_hmac_secret(sym_fido_dev_info_path(entry)); + if (r < 0) + goto finish; + if (!r) { + r = log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "FIDO device discovered does not implement FIDO2 with 'hmac-secret' extension."); + goto finish; + } + path = sym_fido_dev_info_path(entry); if (!path) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), From cf1e172d58b0c0fb3e09ba9b5e6c60093b5b896c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 7 Dec 2020 17:18:52 +0100 Subject: [PATCH 28/30] man: document new features --- man/crypttab.xml | 281 ++++++++++++++++++++++---- man/fido2-crypttab.sh | 10 + man/repart.d.xml | 16 +- man/rules/meson.build | 1 + man/systemd-cryptenroll.xml | 284 +++++++++++++++++++++++++++ man/systemd-cryptsetup-generator.xml | 1 + man/systemd-cryptsetup@.service.xml | 8 +- man/systemd-repart.xml | 32 ++- man/tpm2-crypttab.sh | 10 + man/yubikey-crypttab.sh | 54 ++--- 10 files changed, 603 insertions(+), 94 deletions(-) create mode 100644 man/fido2-crypttab.sh create mode 100644 man/systemd-cryptenroll.xml create mode 100644 man/tpm2-crypttab.sh diff --git a/man/crypttab.xml b/man/crypttab.xml index 0c0f091025..2062a5b8e7 100644 --- a/man/crypttab.xml +++ b/man/crypttab.xml @@ -45,13 +45,12 @@ The first two fields are mandatory, the remaining two are optional. - Setting up encrypted block devices using this file supports - three encryption modes: LUKS, TrueCrypt and plain. See - cryptsetup8 - for more information about each mode. When no mode is specified in - the options field and the block device contains a LUKS signature, - it is opened as a LUKS device; otherwise, it is assumed to be in - raw dm-crypt (plain mode) format. + Setting up encrypted block devices using this file supports four encryption modes: LUKS, TrueCrypt, + BitLocker and plain. See cryptsetup8 for + more information about each mode. When no mode is specified in the options field and the block device + contains a LUKS signature, it is opened as a LUKS device; otherwise, it is assumed to be in raw dm-crypt + (plain mode) format. The four fields of /etc/crypttab are defined as follows: @@ -65,9 +64,10 @@ UUID= followed by the UUID. The third field specifies an absolute path to a file with the encryption - key. Optionally, the path may be followed by : and an fstab device specification - (e.g. starting with LABEL= or similar); in which case the path is taken relative to - the device file system root. If the field is not present or is none or + key. Optionally, the path may be followed by : and an + /etc/fstab style device specification (e.g. starting with + LABEL= or similar); in which case the path is taken relative to the specified + device's file system root. If the field is not present or is none or -, a key file named after the volume to unlock (i.e. the first column of the line), suffixed with .key is automatically loaded from the /etc/cryptsetup-keys.d/ and /run/cryptsetup-keys.d/ @@ -83,6 +83,60 @@ The fourth field, if present, is a comma-delimited list of options. The supported options are listed below. + + + + Key Acquisition + + Six different mechanisms for acquiring the decryption key or passphrase unlocking the encrypted + volume are supported. Specifically: + + + + Most prominently, the user may be queried interactively during volume activation + (i.e. typically at boot), asking them to type in the necessary passphrase(s). + + The (unencrypted) key may be read from a file on disk, possibly on removable media. The third field + of each line encodes the location, for details see above. + + The (unencrypted) key may be requested from another service, by specifying an + AF_UNIX file system socket in place of a key file in the third field. For details + see above and below. + + The key may be acquired via a PKCS#11 compatible hardware security token or + smartcard. In this case an encrypted key is stored on disk/removable media, acquired via + AF_UNIX, or stored in the LUKS2 JSON token metadata header. The encrypted key is + then decrypted by the PKCS#11 token with an RSA key stored on it, and then used to unlock the encrypted + volume. Use the option described below to use this mechanism. + + Similar, the key may be acquired via a FIDO2 compatible hardware security token (which + must implement the "hmac-secret" extension). In this case a (during enrollment) randomly generated key + is stored on disk/removable media, acquired via AF_UNIX, or stored in the LUKS2 + JSON token metadata header. The random key is hashed via a keyed hash function (HMAC) on the FIDO2 + token, using a secret key stored on the token that never leaves it. The resulting hash value is then + used as key to unlock the encrypted volume. Use the option described + below to use this mechanism. + + Similar, the key may be acquired via a TPM2 security chip. In this case a (during + enrollment) randomly generated key — encrypted by an asymmetric key derived from the TPM2 chip's seed + key — is stored on disk/removable media, acquired via AF_UNIX, or stored in the + LUKS2 JSON token metadata header. Use the option described below to use + this mechanism. + + + For the latter five mechanisms the source for the key material used for unlocking the volume is + primarily configured in the third field of each /etc/crypttab line, but may also + configured in /etc/cryptsetup-keys.d/ and + /run/cryptsetup-keys.d/ (see above) or in the LUKS2 JSON token header (in case of + the latter three). Use the + systemd-cryptenroll1 + tool to enroll PKCS#11, FIDO2 and TPM2 devices in LUKS2 volumes. + + + + Supported Options + + The following options may be used in the fourth field of each line: @@ -125,10 +179,10 @@ for possible values and the default value of this option. - Optionally, the path may be followed by : and an fstab device specification - (e.g. starting with UUID= or similar); in which case, the path is relative to the - device file system root. The device gets mounted automatically for LUKS device activation duration only. - + Optionally, the path may be followed by : and an + /etc/fstab device specification (e.g. starting with UUID= or + similar); in which case, the path is relative to the device file system root. The device gets mounted + automatically for LUKS device activation duration only. @@ -198,8 +252,8 @@ - Decrypt Bitlocker drive. Encryption parameters - are deduced by cryptsetup from Bitlocker header. + Decrypt BitLocker drive. Encryption parameters + are deduced by cryptsetup from BitLocker header. @@ -269,7 +323,7 @@ - Perform encryption using the same cpu that IO was submitted on. The default is to use + Perform encryption using the same CPU that IO was submitted on. The default is to use an unbound workqueue so that encryption work is automatically balanced between available CPUs. This requires kernel 4.0 or newer. @@ -451,15 +505,134 @@ - Takes a RFC7512 PKCS#11 URI - pointing to a private RSA key which is used to decrypt the key specified in the third column of the - line. This is useful for unlocking encrypted volumes through security tokens or smartcards. See below - for an example how to set up this mechanism for unlocking a LUKS volume with a YubiKey security - token. The specified URI can refer directly to a private RSA key stored on a token or alternatively + Takes either the special value auto or an RFC7512 PKCS#11 URI pointing to a private RSA key + which is used to decrypt the encrypted key specified in the third column of the line. This is useful + for unlocking encrypted volumes through PKCS#11 compatible security tokens or smartcards. See below + for an example how to set up this mechanism for unlocking a LUKS2 volume with a YubiKey security + token. + + If specified as auto the volume must be of type LUKS2 and must carry PKCS#11 + security token metadata in its LUKS2 JSON token section. In this mode the URI and the encrypted key + are automatically read from the LUKS2 JSON token header. Use + systemd-cryptenroll1 + as simple tool for enrolling PKCS#11 security tokens or smartcards in a way compatible with + auto. In this mode the third column of the line should remain empty (that is, + specified as -). + + The specified URI can refer directly to a private RSA key stored on a token or alternatively just to a slot or token, in which case a search for a suitable private RSA key will be performed. In - this case if multiple suitable objects are found the token is refused. The key configured in the - third column is passed as is to RSA decryption. The resulting decrypted key is then base64 encoded - before it is used to unlock the LUKS volume. + this case if multiple suitable objects are found the token is refused. The encrypted key configured + in the third column of the line is passed as is (i.e. in binary form, unprocessed) to RSA + decryption. The resulting decrypted key is then Base64 encoded before it is used to unlock the LUKS + volume. + + Use systemd-cryptenroll --pkcs11-token-uri=list to list all suitable PKCS#11 + security tokens currently plugged in, along with their URIs. + + Note that many newer security tokens that may be used as PKCS#11 security token typically also + implement the newer and simpler FIDO2 standard. Consider using + (described below) to enroll it via FIDO2 instead. Note that a security token enrolled via PKCS#11 + cannot be used to unlock the volume via FIDO2, unless also enrolled via FIDO2, and vice + versa. + + + + + + Takes either the special value auto or the path to a + hidraw device node (e.g. /dev/hidraw1) referring to a FIDO2 + security token that implements the hmac-secret extension (most current hardware + security tokens do). See below for an example how to set up this mechanism for unlocking an encrypted + volume with a FIDO2 security token. + + If specified as auto the FIDO2 token device is automatically discovered, as + it is plugged in. + + FIDO2 volume unlocking requires a client ID hash (CID) to be configured via + (see below) and a key to pass to the security token's HMAC functionality + (configured in the line's third column) to operate. If not configured and the volume is of type + LUKS2, the CID and the key are read from LUKS2 JSON token metadata instead. Use + systemd-cryptenroll1 + as simple tool for enrolling FIDO2 security tokens, compatible with this automatic mode, which is + only available for LUKS2 volumes. + + Use systemd-cryptenroll --fido2-device=list to list all suitable FIDO2 + security tokens currently plugged in, along with their device nodes. + + This option implements the following mechanism: the configured key is hashed via they HMAC + keyed hash function the FIDO2 device implements, keyed by a secret key embedded on the device. The + resulting hash value is Base64 encoded and used to unlock the LUKS2 volume. As it should not be + possible to extract the secret from the hardware token, it should not be possible to retrieve the + hashed key given the configured key — without possessing the hardware token. + + Note that many security tokens that implement FIDO2 also implement PKCS#11, suitable for + unlocking volumes via the option described above. Typically the newer, + simpler FIDO2 standard is preferable. + + + + + + Takes a Base64 encoded FIDO2 client ID to use for the FIDO2 unlock operation. If + specified, but is not, is + implied. If is used but is not, the volume + must be of LUKS2 type, and the CID is read from the LUKS2 JSON token header. Use + systemd-cryptenroll1 + for enrolling a FIDO2 token in the LUKS2 header compatible with this automatic + mode. + + + + + + Takes a string, configuring the FIDO2 Relying Party (rp) for the FIDO2 unlock + operation. If not specified io.systemd.cryptsetup is used, except if the the LUKS2 + JSON token header contains a different value. It should normally not be necessary to override + this. + + + + + + Takes either the special value auto or the path to a device node + (e.g. /dev/tpmrm0) referring to a TPM2 security chip. See below for an example + how to set up this mechanism for unlocking an encrypted volume with a TPM2 chip. + + Use (see below) to configure the set of TPM2 PCRs to bind the + volume unlocking to. Use + systemd-cryptenroll1 + as simple tool for enrolling TPM2 security chips in LUKS2 volumes. + + If specified as auto the TPM2 device is automatically discovered. Use + systemd-cryptenroll --tpm2-device=list to list all suitable TPM2 devices currently + available, along with their device nodes. + + This option implements the following mechanism: when enrolling a TPM2 device via + systemd-cryptenroll on a LUKS2 volume, a randomized key unlocking the volume is + generated on the host and loaded into the TPM2 chip where it is encrypted with an asymmetric + "primary" key pair derived from the TPM2's internal "seed" key. Neither the seed key nor the primary + key are permitted to ever leave the TPM2 chip — however, the now encrypted randomized key may. It is + saved in the LUKS2 volume JSON token header. When unlocking the encrypted volume, the primary key + pair is generated on the TPM2 chip again (which works as long as the chip's seed key is correctly + maintained by the TPM2 chip), which is then used to decrypt (on the TPM2 chip) the encrypted key from + the LUKS2 volume JSON token header saved there during enrollment. The resulting decrypted key is then + used to unlock the volume. When the randomized key is encrypted the current values of the selected + PCRs (see below) are included in the operation, so that different PCR state results in different + encrypted keys and the decrypted key can only be recovered if the same PCR state is + reproduced. + + + + + + Takes a comma separated list of numeric TPM2 PCR (i.e. "Platform Configuration + Register") indexes to bind the TPM2 volume unlocking to. This option is only useful when TPM2 + enrollment metadata is not available in the LUKS2 JSON token header already, the way + systemd-cryptenroll writes it there. If not used (and no metadata in the LUKS2 + JSON token header defines it), defaults to a list of a single entry: PCR 7. Assign an empty string to + encode a policy that binds the key to no PCRs, making the key accessible to local programs regardless + of the current PCR state. @@ -523,7 +696,7 @@ NUL RANDOM /cryptsetup/ VOLUME In other words: a NUL byte (as required for abstract namespace sockets), - followed by a random string (consisting of alphabenumeric characters only), followed by the literal + followed by a random string (consisting of alphanumeric characters only), followed by the literal string /cryptsetup/, followed by the name of the volume to acquire they key for. Example (for a volume myvol): @@ -533,11 +706,13 @@ name with getpeername2, and use it to determine which key to send, allowing a single listening socket to serve keys for a - multitude of volumes. If the PKCS#11 logic is used (see below) the socket source name is picked in - identical fashion, except that the literal string /cryptsetup-pkcs11/ is used. This is + multitude of volumes. If the PKCS#11 logic is used (see above) the socket source name is picked in + identical fashion, except that the literal string /cryptsetup-pkcs11/ is used (similar + for FIDO2: /cryptsetup-fido2/ and TPM2: /cryptsetup-tpm2/). This is done so that services providing key material know that not a secret key is requested but an encrypted key - that will be decrypted via the PKCS#11 logic to acquire the final secret key. + that will be decrypted via the PKCS#11/FIDO2/TPM2 logic to acquire the final secret key. + Examples @@ -556,25 +731,48 @@ external /dev/sda3 keyfile:LABEL=keydev keyfile-timeout=10s,cipher=xchac - Yubikey-based Volume Unlocking Example + Yubikey-based PKCS#11 Volume Unlocking Example The PKCS#11 logic allows hooking up any compatible security token that is capable of storing RSA - decryption keys. Here's an example how to set up a Yubikey security token for this purpose, using - ykmap1 - from the yubikey-manager project: + decryption keys for unlocking an encrypted volume. Here's an example how to set up a Yubikey security + token for this purpose on a LUKS2 volume, using ykmap1 from the + yubikey-manager project to initialize the token and + systemd-cryptenroll1 + to add it in the LUKS2 volume: -A few notes on the above: + A few notes on the above: - - We use RSA2048, which is the longest key size current Yubikeys support - LUKS key size must be shorter than 2048bit due to RSA padding, hence we use 128 bytes - We use Yubikey key slot 9d, since that's apparently the keyslot to use for decryption purposes, - see - documentation. - + + We use RSA2048, which is the longest key size current Yubikeys support + We use Yubikey key slot 9d, since that's apparently the keyslot to use for decryption purposes, + see + documentation. + + + + FIDO2 Volume Unlocking Example + + The FIDO2 logic allows using any compatible FIDO2 security token that implements the + hmac-secret extension for unlocking an encrypted volume. Here's an example how to + set up a FIDO2 security token for this purpose for a LUKS2 volume, using + systemd-cryptenroll1: + + + + + + TPM2 Volume Unlocking Example + + The TPM2 logic allows using any TPM2 chip supported by the Linux kernel for unlocking an + encrypted volume. Here's an example how to set up a TPM2 chip for this purpose for a LUKS2 volume, + using + systemd-cryptenroll1: + + @@ -584,6 +782,7 @@ external /dev/sda3 keyfile:LABEL=keydev keyfile-timeout=10s,cipher=xchac systemd1, systemd-cryptsetup@.service8, systemd-cryptsetup-generator8, + systemd-cryptenroll1, fstab5, cryptsetup8, mkswap8, diff --git a/man/fido2-crypttab.sh b/man/fido2-crypttab.sh new file mode 100644 index 0000000000..49e536cae8 --- /dev/null +++ b/man/fido2-crypttab.sh @@ -0,0 +1,10 @@ +# Enroll the security token in the LUKS2 volume. Replace /dev/sdXn by the +# partition to use (e.g. /dev/sda1). +sudo systemd-cryptenroll --fido2-device=auto /dev/sdXn + +# Test: Let's run systemd-cryptsetup to test if this worked. +sudo /usr/lib/systemd/systemd-cryptsetup attach mytest /dev/sdXn - fido2-device=auto + +# If that worked, let's now add the same line persistently to /etc/crypttab, +# for the future. +sudo bash -c 'echo "mytest /dev/sdXn - fido2-device=auto" >> /etc/crypttab' diff --git a/man/repart.d.xml b/man/repart.d.xml index 6e31843a08..66debd336f 100644 --- a/man/repart.d.xml +++ b/man/repart.d.xml @@ -492,12 +492,19 @@ Encrypt= - Takes a boolean parameter, defaulting to false. If true the partition will be + Takes one of off, key-file, + tpm2 and key-file+tpm2 (alternatively, also accepts a boolean + value, which is mapped to off when false, and key-file when + true). Defaults to off. If not off the partition will be formatted with a LUKS2 superblock, before the blocks configured with CopyBlocks= are copied in or the file system configured with Format= is created. - The LUKS2 UUID is automatically derived from the partition UUID in a stable fashion. A single - key is added to the LUKS2 superblock, configurable with the switch to + The LUKS2 UUID is automatically derived from the partition UUID in a stable fashion. If + key-file or key-file+tpm2 is used a key is added to the LUKS2 + superblock, configurable with the switch to + systemd-repart. If tpm2 or key-file+tpm2 is + used a key is added to the LUKS2 superblock that is enrolled to the local TPM2 chip, as configured + with the and options to systemd-repart. When used this slightly alters the size allocation logic as the implicit, minimal size limits @@ -627,7 +634,8 @@ SizeMaxBytes=64M systemd1, systemd-repart8, - sfdisk8 + sfdisk8, + systemd-cryptenroll1 diff --git a/man/rules/meson.build b/man/rules/meson.build index 97136fe758..cada4305a7 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -825,6 +825,7 @@ manpages = [ '8', ['systemd-coredump.socket', 'systemd-coredump@.service'], 'ENABLE_COREDUMP'], + ['systemd-cryptenroll', '1', [], 'HAVE_LIBCRYPTSETUP'], ['systemd-cryptsetup-generator', '8', [], 'HAVE_LIBCRYPTSETUP'], ['systemd-cryptsetup@.service', '8', diff --git a/man/systemd-cryptenroll.xml b/man/systemd-cryptenroll.xml new file mode 100644 index 0000000000..17a546b256 --- /dev/null +++ b/man/systemd-cryptenroll.xml @@ -0,0 +1,284 @@ + + + + + + + + systemd-cryptenroll + systemd + + + + systemd-cryptenroll + 1 + + + + systemd-cryptenroll + Enroll PKCS#11, FIDO2, TPM2 token/devices to LUKS2 encrypted volumes + + + + + systemd-cryptenroll OPTIONS DEVICE + + + + + Description + + systemd-cryptenroll is a tool for enrolling hardware security tokens and devices into a + LUKS2 encrypted volume, which may then be used to unlock the volume during boot. Specifically, it supports + tokens and credentials of the following kind to be enrolled: + + + PKCS#11 security tokens and smartcards that may carry an RSA key pair (e.g. various YubiKeys) + + FIDO2 security tokens that implement the hmac-secret extension (most FIDO2 keys, including YubiKeys) + + TPM2 security devices + + Recovery keys. These are similar to regular passphrases, however are randomly generated + on the computer and thus generally have higher entropy than user chosen passphrases. Their character + set has been designed to ensure they are easy to type in, while having high entropy. They may also be + scanned off screen using QR codes. Recovery keys may be used for unlocking LUKS2 volumes wherever + passphrases are accepted. They are intended to be used in combination with an enrolled hardware + security token, as a recovery option when the token is lost. + + Regular passphrases + + + In addition, the tool may be used to enumerate currently enrolled security tokens and wipe a subset + of them. The latter may be combined with the enrollment operation of a new security token, in order to + update or replace enrollments. + + The tool supports only LUKS2 volumes, as it stores token meta-information in the LUKS2 JSON token + area, which is not available in other encryption formats. + + + + Options + + The following options are understood: + + + + + + Enroll a regular password/passphrase. This command is mostly equivalent to + cryptsetup luksAddKey, however may be combined with + in one call, see below. + + + + + + Enroll a recovery key. Recovery keys are most identical to passphrases, but are + computer generated instead of human chosen, and thus have a guaranteed high entropy. The key uses a + character set that is easy to type in, and may be scanned off screen via a QR code. + + + + URI + + Enroll a PKCS#11 security token or smartcard (e.g. a YubiKey). Expects a PKCS#11 + smart card URI referring to the token. Alternatively the special value auto may + be specified, in order to automatically determine the URI of a currently plugged in security token + (of which there must be exactly one). The special value list may be used to + enumerate all suitable PKCS#11 tokens currently plugged in. The security token must contain an RSA + key pair which is used to encrypt the randomly generated key that is used to unlock the LUKS2 + volume. The encrypted key is then stored in the LUKS2 JSON token header area. + + In order to unlock a LUKS2 volume with an enrolled PKCS#11 security token, specify the + option in the respective /etc/crypttab line: + + myvolume /dev/sda1 - pkcs11-uri=auto + + See + crypttab5 for a + more comprehensive example of a systemd-cryptenroll invocation and its matching + /etc/crypttab line. + + + + PATH + + Enroll a FIDO2 security token that implements the hmac-secret + extension (e.g. a YubiKey). Expects a hidraw device referring to the FIDO2 + device (e.g. /dev/hidraw1). Alternatively the special value + auto may be specified, in order to automatically determine the device node of a + currently plugged in security token (of which there must be exactly one). The special value + list may be used to enumerate all suitable FIDO2 tokens currently plugged in. Note + that many hardware security tokens that implement FIDO2 also implement the older PKCS#11 + standard. Typically FIDO2 is preferable, given it's simpler to use and more modern. + + In order to unlock a LUKS2 volume with an enrolled FIDO2 security token, specify the + option in the respective /etc/crypttab line: + + myvolume /dev/sda1 - fido2-device=auto + + See + crypttab5 for a + more comprehensive example of a systemd-cryptenroll invocation and its matching + /etc/crypttab line. + + + + PATH + + Enroll a TPM2 security chip. Expects a device node path referring to the TPM2 chip + (e.g. /dev/tpmrm0). Alternatively the special value auto may + be specified, in order to automatically determine the device node of a currently discovered TPM2 + device (of which there must be exactly one). The special value list may be used to + enumerate all suitable TPM2 devices currently discovered. + + In order to unlock a LUKS2 volume with an enrolled TPM2 security chip, specify the + option in the respective /etc/crypttab line: + + myvolume /dev/sda1 - tpm2-device=auto + + See + crypttab5 for a + more comprehensive example of a systemd-cryptenroll invocation and its matching + /etc/crypttab line. + + Use (see below) to configure which TPM2 PCR indexes to bind the + enrollment to. + + + + PCR + + Configures the TPM2 PCRs (Platform Configuration Registers) to bind the enrollment + requested via to. Takes a comma separated list of numeric PCR indexes + in the range 0…23. If not used, defaults to PCR 7 only. If an empty string is specified, binds the + enrollment to no PCRs at all. PCRs allow binding the enrollment to specific software versions and + system state, so that the enrolled unlocking key is only accessible (may be "unsealed") if specific + trusted software and/or configuration is used. + + + Well-known PCR Definitions + + + + + + + + PCR + Explanation + + + + + + 0 + Core system firmware executable code; changes on firmware updates + + + + 1 + Core system firmware data/host platform configuration; typically contains serial and model numbers, changes on basic hardware/CPU/RAM replacements + + + + 2 + Extended or pluggable executable code; includes option ROMs on pluggable hardware + + + + 3 + Extended or pluggable firmware data; includes information about pluggable hardware + + + + 4 + Boot loader; changes on boot loader updates + + + + 5 + GPT/Partition table; changes when the partitions are added, modified or removed + + + + 6 + Power state events; changes on system suspend/sleep + + + + 7 + Secure boot state; changes when UEFI SecureBoot mode is enabled/disabled + + + + 8 + sd-boot8 measures the kernel command line in this PCR. + + + +
+ +
+ + + SLOT + + Wipes one or more LUKS2 key slots. Takes a comma separated list of numeric slot + indexes, or the special strings all (for wiping all key slots), + empty (for wiping all key slots that are unlocked by an empty passphrase), + password (for wiping all key slots that are unlocked by a traditional passphrase), + recovery (for wiping all key slots that are unlocked by a recovery key), + pkcs11 (for wiping all key slots that are unlocked by a PKCS#11 token), + fido2 (for wiping all key slots that are unlocked by a FIDO2 token), + tpm2 (for wiping all key slots that are unlocked by a TPM2 chip), or any + combination of these strings or numeric indexes, in which case all slots matching either are + wiped. As safety precaution an operation that wipes all slots without exception (so that the volume + cannot be unlocked at all anymore, unless the volume key is known) is refused. + + This switch may be used alone, in which case only the requested wipe operation is executed. It + may also be used in combination with any of the enrollment options listed above, in which case the + enrollment is completed first, and only when successful the wipe operation executed — and the newly + added slot is always excluded from the wiping. Combining enrollment and slot wiping may thus be used to + update existing enrollments: + + systemd-cryptenroll /dev/sda1 --wipe-slot=tpm2 --tpm2-device=auto + + The above command will enroll the TPM2 chip, and then wipe all previously crated TPM2 + enrollments on the LUKS2 volume, leaving only the newly created one. Combining wiping and enrollment + may also be used to replace enrollments of different types, for example for changing from a PKCS#11 + enrollment to a FIDO2 one: + + systemd-cryptenroll /dev/sda1 --wipe-slot=pkcs11 --fido2-device=auto + + Or for replacing an enrolled empty password by TPM2: + + systemd-cryptenroll /dev/sda1 --wipe-slot=empty --tpm2-device=auto + + + + + +
+ +
+ + + Exit status + + On success, 0 is returned, a non-zero failure code otherwise. + + + + See Also + + systemd1, + systemd-cryptsetup@.service8, + crypttab5, + cryptsetup8 + + + +
diff --git a/man/systemd-cryptsetup-generator.xml b/man/systemd-cryptsetup-generator.xml index 4284f78c4e..e5c193f692 100644 --- a/man/systemd-cryptsetup-generator.xml +++ b/man/systemd-cryptsetup-generator.xml @@ -228,6 +228,7 @@ systemd1, crypttab5, systemd-cryptsetup@.service8, + systemd-cryptenroll1, cryptsetup8, systemd-fstab-generator8
diff --git a/man/systemd-cryptsetup@.service.xml b/man/systemd-cryptsetup@.service.xml index 216db7467c..c70d6a9d3e 100644 --- a/man/systemd-cryptsetup@.service.xml +++ b/man/systemd-cryptsetup@.service.xml @@ -50,13 +50,14 @@ If a key file is explicitly configured (via the third column in - /etc/crypttab), a key read from it is used. If a PKCS#11 token is configured - (using the pkcs11-uri= option) the key is decrypted before use. + /etc/crypttab), a key read from it is used. If a PKCS#11 token, FIDO2 token or + TPM2 device is configured (using the pkcs11-uri=, fido2-device=, + tpm2-device= options) the key is decrypted before use.
If no key file is configured explicitly this way, a key file is automatically loaded from /etc/cryptsetup-keys.d/volume.key and /run/cryptsetup-keys.d/volume.key, if present. Here - too, if a PKCS#11 token is configured, any key found this way is decrypted before + too, if a PKCS#11/FIDO2/TPM2 token/device is configured, any key found this way is decrypted before use. If the try-empty-password option is specified it is then attempted @@ -77,6 +78,7 @@ systemd1, systemd-cryptsetup-generator8, crypttab5, + systemd-cryptenroll1, cryptsetup8 diff --git a/man/systemd-repart.xml b/man/systemd-repart.xml index 16add32b2d..858b5be66d 100644 --- a/man/systemd-repart.xml +++ b/man/systemd-repart.xml @@ -300,12 +300,23 @@ Takes a file system path. Configures the encryption key to use when setting up LUKS2 - volumes configured with the Encrypt= setting in partition files. Should refer to a - regular file containing the key, or an AF_UNIX stream socket in the file - system. In the latter case a connection is made to it and the key read from it. If this switch is not - specified the empty key (i.e. zero length key) is used. This behaviour is useful for setting up encrypted - partitions during early first boot that receive their user-supplied password only in a later setup - step. + volumes configured with the Encrypt=key-file setting in partition files. Should + refer to a regular file containing the key, or an AF_UNIX stream socket in the + file system. In the latter case a connection is made to it and the key read from it. If this switch + is not specified the empty key (i.e. zero length key) is used. This behaviour is useful for setting + up encrypted partitions during early first boot that receive their user-supplied password only in a + later setup step.
+
+ + + + + + Configures the TPM2 device and list of PCRs to use for LUKS2 volumes configured with + the Encrypt=tpm2 option. These options take the same parameters as the identically + named options to + systemd-cryptenroll1 + and have the same effect on partitions where TPM2 enrollment is requested. @@ -313,12 +324,19 @@ + + Exit status + + On success, 0 is returned, a non-zero failure code otherwise. + + See Also systemd1, repart.d5, - machine-id5 + machine-id5, + systemd-cryptenroll1 diff --git a/man/tpm2-crypttab.sh b/man/tpm2-crypttab.sh new file mode 100644 index 0000000000..41db2aeddd --- /dev/null +++ b/man/tpm2-crypttab.sh @@ -0,0 +1,10 @@ +# Enroll the TPM2 security chip in the LUKS2 volume, and bind it to PCR 7 +# only. Replace /dev/sdXn by the partition to use (e.g. /dev/sda1). +sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 /dev/sdXn + +# Test: Let's run systemd-cryptsetup to test if this worked. +sudo /usr/lib/systemd/systemd-cryptsetup attach mytest /dev/sdXn - tpm2-device=auto + +# If that worked, let's now add the same line persistently to /etc/crypttab, +# for the future. +sudo bash -c 'echo "mytest /dev/sdXn - tpm2-device=auto" >> /etc/crypttab' diff --git a/man/yubikey-crypttab.sh b/man/yubikey-crypttab.sh index 651246d6a1..05e581b32b 100644 --- a/man/yubikey-crypttab.sh +++ b/man/yubikey-crypttab.sh @@ -1,50 +1,26 @@ -# Make sure no one can read the files we generate but us -umask 077 - # Destroy any old key on the Yubikey (careful!) ykman piv reset -# Generate a new private/public key pair on the device, store the public key in 'pubkey.pem'. +# Generate a new private/public key pair on the device, store the public key in +# 'pubkey.pem'. ykman piv generate-key -a RSA2048 9d pubkey.pem # Create a self-signed certificate from this public key, and store it on the -# device. The "subject" should be an arbitrary string to identify the token in -# the p11tool output below. +# device. The "subject" should be an arbitrary user-chosen string to identify +# the token with. ykman piv generate-certificate --subject "Knobelei" 9d pubkey.pem -# Check if the newly create key on the Yubikey shows up as token in PKCS#11. Have a look at the output, and -# copy the resulting token URI to the clipboard. -p11tool --list-tokens - -# Generate a (secret) random key to use as LUKS decryption key. -dd if=/dev/urandom of=plaintext.bin bs=128 count=1 - -# Encode the secret key also as base64 text (with all whitespace removed) -base64 < plaintext.bin | tr -d '\n\r\t ' > plaintext.base64 - -# Encrypt this newly generated (binary) LUKS decryption key using the public key whose private key is on the -# Yubikey, store the result in /etc/cryptsetup-keys.d/mytest.key, where we'll look for it during boot. -mkdir -p /etc/cryptsetup-keys.d -sudo openssl rsautl -encrypt -pubin -inkey pubkey.pem -in plaintext.bin -out /etc/cryptsetup-keys.d/mytest.key - -# Configure the LUKS decryption key on the LUKS device. We use very low pbkdf settings since the key already -# has quite a high quality (it comes directly from /dev/urandom after all), and thus we don't need to do much -# key derivation. Replace /dev/sdXn by the partition to use (e.g. sda1) -sudo cryptsetup luksAddKey /dev/sdXn plaintext.base64 --pbkdf=pbkdf2 --pbkdf-force-iterations=1000 - -# Now securely delete the plain text LUKS key, we don't need it anymore, and since it contains secret key -# material it should be removed from disk thoroughly. -shred -u plaintext.bin plaintext.base64 - -# We don't need the public key anymore either, let's remove it too. Since this one is not security -# sensitive we just do a regular "rm" here. +# We don't need the public key anymore, let's remove it. Since it is not +# security sensitive we just do a regular "rm" here. rm pubkey.pem -# Test: Let's run systemd-cryptsetup to test if this all worked. The option string should contain the full -# PKCS#11 URI we have in the clipboard; it tells the tool how to decipher the encrypted LUKS key. Note that -# systemd-cryptsetup automatically searches for the encrypted key in /etc/cryptsetup-keys.d/, hence we do -# not need to specify the key file path explicitly here. -sudo systemd-cryptsetup attach mytest /dev/sdXn - 'pkcs11-uri=pkcs11:…' +# Enroll the freshly initialized security token in the LUKS2 volume. Replace +# /dev/sdXn by the partition to use (e.g. /dev/sda1). +sudo systemd-cryptenroll --pkcs11-token-uri=auto /dev/sdXn -# If that worked, let's now add the same line persistently to /etc/crypttab, for the future. -sudo bash -c 'echo "mytest /dev/sdXn - \'pkcs11-uri=pkcs11:…\'" >> /etc/crypttab' +# Test: Let's run systemd-cryptsetup to test if this all worked. +sudo /usr/lib/systemd/systemd-cryptsetup attach mytest /dev/sdXn - pkcs11-uri=auto + +# If that worked, let's now add the same line persistently to /etc/crypttab, +# for the future. +sudo bash -c 'echo "mytest /dev/sdXn - pkcs11-uri=auto" >> /etc/crypttab' From 5e85016b1f7754c4cea88bb1a07e32c3c6b70b12 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 17 Dec 2020 14:16:15 +0100 Subject: [PATCH 29/30] mkosi: add TPM2 packages to debian/ubuntu/fedora mkosi files As suggested: https://github.com/systemd/systemd/pull/17741#issuecomment-743479834 --- .mkosi/mkosi.debian | 11 +++++++++-- .mkosi/mkosi.fedora | 2 ++ .mkosi/mkosi.ubuntu | 6 ++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.mkosi/mkosi.debian b/.mkosi/mkosi.debian index 0f47bb5ad1..69640fa81b 100644 --- a/.mkosi/mkosi.debian +++ b/.mkosi/mkosi.debian @@ -24,7 +24,6 @@ BuildPackages= git gnu-efi gperf - libiptc-dev libacl1-dev libaudit-dev libblkid-dev @@ -35,19 +34,23 @@ BuildPackages= libdbus-1-dev libdw-dev libfdisk-dev + libfido2-dev libgcrypt20-dev libgnutls28-dev libidn2-0-dev + libiptc-dev libkmod-dev - liblzma-dev liblz4-dev liblz4-tool + liblzma-dev libmicrohttpd-dev libmount-dev libpam0g-dev libqrencode-dev libseccomp-dev libsmartcols-dev + libssl-dev + libtss2-dev libxkbcommon-dev libzstd-dev m4 @@ -63,8 +66,12 @@ BuildPackages= Packages= gdb + libfdisk1 + libfido2-1 libidn2-0 libqrencode4 + # We pull in the -dev package here, since the binary ones appear to change names too often, and the -dev package pulls the right deps in automatically + libtss2-dev locales strace diff --git a/.mkosi/mkosi.fedora b/.mkosi/mkosi.fedora index e113fd92a8..680e6fb6a4 100644 --- a/.mkosi/mkosi.fedora +++ b/.mkosi/mkosi.fedora @@ -66,6 +66,7 @@ BuildPackages= python3-lxml qrencode-devel rpm + tpm2-tss-devel tree valgrind-devel xz-devel @@ -79,6 +80,7 @@ Packages= # procps-ng provides a set of useful utilies (ps, free, etc) procps-ng strace + tpm2-tss BuildDirectory=mkosi.builddir Cache=mkosi.cache diff --git a/.mkosi/mkosi.ubuntu b/.mkosi/mkosi.ubuntu index 47a2b9c3f8..1c22ee04ab 100644 --- a/.mkosi/mkosi.ubuntu +++ b/.mkosi/mkosi.ubuntu @@ -35,6 +35,7 @@ BuildPackages= libdbus-1-dev libdw-dev libfdisk-dev + libfido2-dev libgcrypt20-dev libgnutls28-dev libidn2-0-dev @@ -50,6 +51,8 @@ BuildPackages= libqrencode-dev libseccomp-dev libsmartcols-dev + libssl-dev + libtss2-dev libxkbcommon-dev libxtables-dev libzstd-dev @@ -67,8 +70,11 @@ BuildPackages= Packages= gdb + libfido2-1 libidn2-0 libqrencode4 + # We pull in the -dev package here, since the binary ones appear to change names too often, and the -dev package pulls the right deps in automatically + libtss2-dev locales strace From 80670e748de8cc2980ad6b5a5e172cfbb61d777a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 26 Nov 2020 14:42:23 +0100 Subject: [PATCH 30/30] update TODO --- TODO | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/TODO b/TODO index a666303571..00467b15f7 100644 --- a/TODO +++ b/TODO @@ -22,8 +22,32 @@ Features: * expose MS_NOSYMFOLLOW in various places +* Add concept for upgrading TPM2 enrollments, maybe a new switch + --pcrs=4: or so, i.e. select a PCR to include in the hash, and then + override its hash + +* homed: store PKCS#11 + FIDO2 token info in LUKS2 header, compatible with + systemd-cryptsetup, so that it can unlock homed volumes + +* cryptenroll: politely refuse enrolling new keys to homed volumes, since we + we cannot update identity info + +* TPM2: auto-reenroll in cryptsetup, as fallback for hosed firmware upgrades + and such + +* cryptsetup: if only recovery keys are registered and no regular passphrases, + ask user for "recovery key", not "passphrase" + +* cyptsetup: add option for automatically removing empty password slot on boot + * cryptsetup: optionally, when run during boot-up and password is never - entered, and we are on AC power (or so), power off machine again + entered, and we are on battery power (or so), power off machine again + +* cryptsetup: when FIDO2/PKCS#11/TPM2 token/chip didn't show up after some + time, abort the attempt, fallback to asking for pw + +* cryptsetup: when waiting for FIDO2/PKCS#11 token, tell plymouth that, and + allow plymouth to abort the waiting and enter pw instead * when configuring loopback netif, and it fails due to EPERM, eat up error if it happens to be set up alright already. @@ -200,9 +224,6 @@ Features: thus allows defining OS images which can be A/B updated and we default to the newest version automatically, both in nspawn and in sd-boot -* cryptsetup: support FIDO2 tokens for deriving keys (i.e. do what homed can do - also in plain cryptsetup) - * systemd-gpt-auto should probably set x-systemd.growfs on the mounts it creates @@ -241,12 +262,6 @@ Features: * add growvol and makevol options for /etc/crypttab, similar to x-systemd.growfs and x-systemd-makefs. -* hook up the TPM to /etc/crypttab, with a new option that is similar to the - new PKCS#11 option in crypttab, and allows unlocking a LUKS volume via a key - unsealed from the TPM. Optionally, if TPM is not available fall back to - TPM-less mode, and set up linear DM mapping instead (inspired by kpartx), so - that the device paths stay the same, regardless if crypto is used or not. - * systemd-repart: by default generate minimized partition tables (i.e. tables that only cover the space actually used, excluding any free space at the end), in order to maximize dd'ability. Requires libfdisk work, see