From 428d32afeaf9bb09909794ea5f9c047c2fb56a8f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 15 Apr 2020 18:45:16 +0200 Subject: [PATCH 01/20] locale-util: add support for touch emoji We can use this to highlight when users are supposed to touch their security tokens. --- src/basic/locale-util.c | 6 +++++- src/basic/locale-util.h | 3 ++- src/test/test-locale-util.c | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/basic/locale-util.c b/src/basic/locale-util.c index 01af759a7d..8e6a12b602 100644 --- a/src/basic/locale-util.c +++ b/src/basic/locale-util.c @@ -373,7 +373,8 @@ const char *special_glyph(SpecialGlyph code) { [SPECIAL_GLYPH_SLIGHTLY_UNHAPPY_SMILEY] = ":-(", [SPECIAL_GLYPH_UNHAPPY_SMILEY] = ":-{", [SPECIAL_GLYPH_DEPRESSED_SMILEY] = ":-[", - [SPECIAL_GLYPH_LOCK_AND_KEY] = "o-," + [SPECIAL_GLYPH_LOCK_AND_KEY] = "o-,", + [SPECIAL_GLYPH_TOUCH] = "O=", /* Yeah, not very convincing, can you do it better? */ }, /* UTF-8 */ @@ -415,6 +416,9 @@ const char *special_glyph(SpecialGlyph code) { /* This emoji is a single character cell glyph in Unicode, and three in ASCII */ [SPECIAL_GLYPH_LOCK_AND_KEY] = "\360\237\224\220", /* 🔐 (actually called: CLOSED LOCK WITH KEY) */ + + /* This emoji is a single character cell glyph in Unicode, and two in ASCII */ + [SPECIAL_GLYPH_TOUCH] = "\360\237\221\206", /* 👆 (actually called: BACKHAND INDEX POINTING UP */ }, }; diff --git a/src/basic/locale-util.h b/src/basic/locale-util.h index e4f9711b08..aa25e17f15 100644 --- a/src/basic/locale-util.h +++ b/src/basic/locale-util.h @@ -65,7 +65,8 @@ typedef enum { SPECIAL_GLYPH_UNHAPPY_SMILEY, SPECIAL_GLYPH_DEPRESSED_SMILEY, SPECIAL_GLYPH_LOCK_AND_KEY, - _SPECIAL_GLYPH_MAX + SPECIAL_GLYPH_TOUCH, + _SPECIAL_GLYPH_MAX, } SpecialGlyph; const char *special_glyph(SpecialGlyph code) _const_; diff --git a/src/test/test-locale-util.c b/src/test/test-locale-util.c index 45b6782145..347982dd52 100644 --- a/src/test/test-locale-util.c +++ b/src/test/test-locale-util.c @@ -89,7 +89,7 @@ static void test_keymaps(void) { #define dump_glyph(x) log_info(STRINGIFY(x) ": %s", special_glyph(x)) static void dump_special_glyphs(void) { - assert_cc(SPECIAL_GLYPH_LOCK_AND_KEY + 1 == _SPECIAL_GLYPH_MAX); + assert_cc(SPECIAL_GLYPH_TOUCH + 1 == _SPECIAL_GLYPH_MAX); log_info("/* %s */", __func__); @@ -116,6 +116,7 @@ static void dump_special_glyphs(void) { dump_glyph(SPECIAL_GLYPH_UNHAPPY_SMILEY); dump_glyph(SPECIAL_GLYPH_DEPRESSED_SMILEY); dump_glyph(SPECIAL_GLYPH_LOCK_AND_KEY); + dump_glyph(SPECIAL_GLYPH_TOUCH); } int main(int argc, char *argv[]) { From 1146b664e60cc16f9c3ab828029956739ae9552d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Apr 2020 18:52:24 +0200 Subject: [PATCH 02/20] macro: add new FOREACH_POINTER() macro magic This allows us to iterate through a series of specified pointers. It's a bit like FOREACH_STRING(), but for all kinds of pointers. --- src/basic/macro.h | 6 ++++ src/test/test-util.c | 80 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/src/basic/macro.h b/src/basic/macro.h index fd8772f377..ceea8176f5 100644 --- a/src/basic/macro.h +++ b/src/basic/macro.h @@ -538,6 +538,12 @@ static inline int __coverity_check_and_return__(int condition) { (y) = (_t); \ } while (false) +/* Iterates through a specified list of pointers. Accepts NULL pointers, but uses (void*) -1 as internal marker for EOL. */ +#define FOREACH_POINTER(p, x, ...) \ + for (typeof(p) *_l = (typeof(p)[]) { ({ p = x; }), ##__VA_ARGS__, (void*) -1 }; \ + p != (typeof(p)) (void*) -1; \ + p = *(++_l)) + /* Define C11 thread_local attribute even on older gcc compiler * version */ #ifndef thread_local diff --git a/src/test/test-util.c b/src/test/test-util.c index 76dd72a598..d4a6c8f5c3 100644 --- a/src/test/test-util.c +++ b/src/test/test-util.c @@ -410,6 +410,85 @@ static void test_system_tasks_max_scale(void) { assert_se(system_tasks_max_scale(UINT64_MAX/4, UINT64_MAX) == UINT64_MAX); } +static void test_foreach_pointer(void) { + int a, b, c, *i; + size_t k = 0; + + FOREACH_POINTER(i, &a, &b, &c) { + switch (k) { + + case 0: + assert_se(i == &a); + break; + + case 1: + assert_se(i == &b); + break; + + case 2: + assert_se(i == &c); + break; + + default: + assert_not_reached("unexpected index"); + break; + } + + k++; + } + + assert(k == 3); + + FOREACH_POINTER(i, &b) { + assert(k == 3); + assert(i == &b); + k = 4; + } + + assert(k == 4); + + FOREACH_POINTER(i, NULL, &c, NULL, &b, NULL, &a, NULL) { + switch (k) { + + case 4: + assert_se(i == NULL); + break; + + case 5: + assert_se(i == &c); + break; + + case 6: + assert_se(i == NULL); + break; + + case 7: + assert_se(i == &b); + break; + + case 8: + assert_se(i == NULL); + break; + + case 9: + assert_se(i == &a); + break; + + case 10: + assert_se(i == NULL); + break; + + default: + assert_not_reached("unexpected index"); + break; + } + + k++; + } + + assert(k == 11); +} + int main(int argc, char *argv[]) { test_setup_logging(LOG_INFO); @@ -428,6 +507,7 @@ int main(int argc, char *argv[]) { test_physical_memory_scale(); test_system_tasks_max(); test_system_tasks_max_scale(); + test_foreach_pointer(); return 0; } From c63ec11bf5c7f9ef68fa5af70b54df1b6fbc0f53 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Apr 2020 14:12:09 +0200 Subject: [PATCH 03/20] pkcs11-util: reduce scope of a variable --- src/shared/pkcs11-util.c | 41 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/shared/pkcs11-util.c b/src/shared/pkcs11-util.c index b4a7d86afe..b8863d2525 100644 --- a/src/shared/pkcs11-util.c +++ b/src/shared/pkcs11-util.c @@ -211,28 +211,8 @@ int pkcs11_token_login( for (unsigned tries = 0; tries < 3; tries++) { _cleanup_strv_free_erase_ char **passwords = NULL; - _cleanup_free_ char *text = NULL; char **i, *e; - if (FLAGS_SET(token_info->flags, CKF_USER_PIN_FINAL_TRY)) - r = asprintf(&text, - "Please enter correct PIN for security token '%s' in order to unlock %s (final try):", - token_label, friendly_name); - else if (FLAGS_SET(token_info->flags, CKF_USER_PIN_COUNT_LOW)) - r = asprintf(&text, - "PIN has been entered incorrectly previously, please enter correct PIN for security token '%s' in order to unlock %s:", - token_label, friendly_name); - else if (tries == 0) - r = asprintf(&text, - "Please enter PIN for security token '%s' in order to unlock %s:", - token_label, friendly_name); - else - r = asprintf(&text, - "Please enter PIN for security token '%s' in order to unlock %s (try #%u):", - token_label, friendly_name, tries+1); - if (r < 0) - return log_oom(); - e = getenv("PIN"); if (e) { passwords = strv_new(e); @@ -243,6 +223,27 @@ int pkcs11_token_login( if (unsetenv("PIN") < 0) return log_error_errno(errno, "Failed to unset $PIN: %m"); } else { + _cleanup_free_ char *text = NULL; + + if (FLAGS_SET(token_info->flags, CKF_USER_PIN_FINAL_TRY)) + r = asprintf(&text, + "Please enter correct PIN for security token '%s' in order to unlock %s (final try):", + token_label, friendly_name); + else if (FLAGS_SET(token_info->flags, CKF_USER_PIN_COUNT_LOW)) + r = asprintf(&text, + "PIN has been entered incorrectly previously, please enter correct PIN for security token '%s' in order to unlock %s:", + token_label, friendly_name); + else if (tries == 0) + r = asprintf(&text, + "Please enter PIN for security token '%s' in order to unlock %s:", + token_label, friendly_name); + else + r = asprintf(&text, + "Please enter PIN for security token '%s' in order to unlock %s (try #%u):", + token_label, friendly_name, tries+1); + if (r < 0) + return log_oom(); + /* We never cache PINs, simply because it's fatal if we use wrong PINs, since usually there are only 3 tries */ r = ask_password_auto(text, icon_name, id, keyname, until, 0, &passwords); if (r < 0) From 7b8d55b72c11ecaf8b5abef4a0aa9f0dca86c887 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 15 Apr 2020 19:31:33 +0200 Subject: [PATCH 04/20] homectl: add missing log messages when json_variant_format() fails --- src/home/homectl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/home/homectl.c b/src/home/homectl.c index 47a506f5e0..8b91f08edf 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -1423,7 +1423,7 @@ static int create_home(int argc, char *argv[], void *userdata) { r = json_variant_format(hr->json, 0, &formatted); if (r < 0) - return r; + return log_error_errno(r, "Failed to format user record: %m"); r = bus_message_new_method_call(bus, &m, bus_home_mgr, "CreateHome"); if (r < 0) @@ -1631,7 +1631,7 @@ static int update_home(int argc, char *argv[], void *userdata) { r = json_variant_format(hr->json, 0, &formatted); if (r < 0) - return r; + return log_error_errno(r, "Failed to format user record: %m"); (void) sd_bus_message_sensitive(m); From d00f318323177268872a1fa8bc140aa76979789d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Apr 2020 15:43:01 +0200 Subject: [PATCH 05/20] user-record: securely erase pkcs#11 when assigned NULL too --- src/shared/user-record.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/user-record.c b/src/shared/user-record.c index 83d86f69e7..f6f67eabc0 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -706,7 +706,7 @@ static int dispatch_pkcs11_key_data(const char *name, JsonVariant *variant, Json int r; if (json_variant_is_null(variant)) { - k->data = mfree(k->data); + k->data = erase_and_free(k->data); k->size = 0; return 0; } From c0bde0d2402b203207d1ec2f998e661ee0fe177c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Apr 2020 19:05:09 +0200 Subject: [PATCH 06/20] user-record: rename JSON field "pkcs11Pin" to "tokenPin" We'd like to use it for FIDO2 tokens too, and the concept is entirely generic, hence let's just reuse the field, but rename it. Read the old name for compatibility, and treat the old name and the new name as identical for most purposes. --- docs/USER_RECORD.md | 6 ++++-- src/home/homectl.c | 26 +++++++++++++------------- src/home/homework-pkcs11.c | 6 +++--- src/home/pam_systemd_home.c | 8 ++++---- src/home/user-record-util.c | 16 ++++++++-------- src/home/user-record-util.h | 2 +- src/shared/user-record.c | 5 +++-- src/shared/user-record.h | 2 +- 8 files changed, 37 insertions(+), 34 deletions(-) diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md index 7b6fe47665..2ed043734f 100644 --- a/docs/USER_RECORD.md +++ b/docs/USER_RECORD.md @@ -864,8 +864,10 @@ The `secret` field of the top-level user record contains the following fields: `password` → an array of strings, each containing a plain text password. -`pkcs11Pin` → an array of strings, each containing a plain text PIN, suitable -for unlocking PKCS#11 security tokens that require that. +`tokenPin` → an array of strings, each containing a plain text PIN, suitable +for unlocking security tokens that require that. (The field `pkcs11Pin` should +be considered a compatibility alias for this field, and merged with `tokenPin` +in case both are set.) `pkcs11ProtectedAuthenticationPathPermitted` → a boolean. If set to true allows the receiver to use the PKCS#11 "protected authentication path" (i.e. a diff --git a/src/home/homectl.c b/src/home/homectl.c index 8b91f08edf..e69fa63fa3 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -236,7 +236,7 @@ static int acquire_existing_password(const char *user_name, UserRecord *hr, bool return 0; } -static int acquire_pkcs11_pin(const char *user_name, UserRecord *hr) { +static int acquire_token_pin(const char *user_name, UserRecord *hr) { _cleanup_(strv_free_erasep) char **pin = NULL; _cleanup_free_ char *question = NULL; char *e; @@ -247,9 +247,9 @@ static int acquire_pkcs11_pin(const char *user_name, UserRecord *hr) { e = getenv("PIN"); if (e) { - r = user_record_set_pkcs11_pin(hr, STRV_MAKE(e), false); + r = user_record_set_token_pin(hr, STRV_MAKE(e), false); if (r < 0) - return log_error_errno(r, "Failed to store PKCS#11 PIN: %m"); + return log_error_errno(r, "Failed to store token PIN: %m"); string_erase(e); @@ -263,11 +263,11 @@ static int acquire_pkcs11_pin(const char *user_name, UserRecord *hr) { return log_oom(); /* We never cache or use cached PINs, since usually there are only very few attempts allowed before the PIN is blocked */ - r = ask_password_auto(question, "user-home", NULL, "pkcs11-pin", USEC_INFINITY, 0, &pin); + r = ask_password_auto(question, "user-home", NULL, "token-pin", USEC_INFINITY, 0, &pin); if (r < 0) return log_error_errno(r, "Failed to acquire security token PIN: %m"); - r = user_record_set_pkcs11_pin(hr, pin, false); + r = user_record_set_token_pin(hr, pin, false); if (r < 0) return log_error_errno(r, "Failed to store security token PIN: %m"); @@ -315,7 +315,7 @@ static int handle_generic_user_record_error( } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_NEEDED)) { - r = acquire_pkcs11_pin(user_name, hr); + r = acquire_token_pin(user_name, hr); if (r < 0) return r; @@ -334,7 +334,7 @@ static int handle_generic_user_record_error( log_notice("Security token PIN incorrect, please try again."); - r = acquire_pkcs11_pin(user_name, hr); + r = acquire_token_pin(user_name, hr); if (r < 0) return r; @@ -342,7 +342,7 @@ static int handle_generic_user_record_error( log_notice("Security token PIN incorrect, please try again (only a few tries left!)."); - r = acquire_pkcs11_pin(user_name, hr); + r = acquire_token_pin(user_name, hr); if (r < 0) return r; @@ -350,7 +350,7 @@ static int handle_generic_user_record_error( log_notice("Security token PIN incorrect, please try again (only one try left!)."); - r = acquire_pkcs11_pin(user_name, hr); + r = acquire_token_pin(user_name, hr); if (r < 0) return r; } else @@ -1005,7 +1005,7 @@ static int encrypt_bytes( return 0; } -static int add_pkcs11_pin(JsonVariant **v, const char *pin) { +static int add_token_pin(JsonVariant **v, const char *pin) { _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL; _cleanup_(strv_free_erasep) char **pins = NULL; int r; @@ -1016,7 +1016,7 @@ static int add_pkcs11_pin(JsonVariant **v, const char *pin) { return 0; w = json_variant_ref(json_variant_by_key(*v, "secret")); - l = json_variant_ref(json_variant_by_key(w, "pkcs11Pin")); + l = json_variant_ref(json_variant_by_key(w, "tokenPin")); r = json_variant_strv(l, &pins); if (r < 0) @@ -1039,7 +1039,7 @@ static int add_pkcs11_pin(JsonVariant **v, const char *pin) { json_variant_sensitive(l); - r = json_variant_set_field(&w, "pkcs11Pin", l); + r = json_variant_set_field(&w, "tokenPin", l); if (r < 0) return log_error_errno(r, "Failed to update PIN field: %m"); @@ -1212,7 +1212,7 @@ static int add_pkcs11_key_data(JsonVariant **v, const char *uri) { /* If we acquired the PIN also include it in the secret section of the record, so that systemd-homed * can use it if it needs to, given that it likely needs to decrypt the key again to pass to LUKS or * fscrypt. */ - r = add_pkcs11_pin(v, pin); + r = add_token_pin(v, pin); if (r < 0) return r; diff --git a/src/home/homework-pkcs11.c b/src/home/homework-pkcs11.c index 915bc0e57e..3a03fb7200 100644 --- a/src/home/homework-pkcs11.c +++ b/src/home/homework-pkcs11.c @@ -62,10 +62,10 @@ int pkcs11_callback( goto decrypt; } - if (strv_isempty(data->secret->pkcs11_pin)) - return log_error_errno(SYNTHETIC_ERRNO(ENOANO), "Security Token requires PIN."); + if (strv_isempty(data->secret->token_pin)) + return log_error_errno(SYNTHETIC_ERRNO(ENOANO), "Security token requires PIN."); - STRV_FOREACH(i, data->secret->pkcs11_pin) { + STRV_FOREACH(i, data->secret->token_pin) { rv = m->C_Login(session, CKU_USER, (CK_UTF8CHAR*) *i, strlen(*i)); if (rv == CKR_OK) { log_info("Successfully logged into security token '%s' with PIN.", token_label); diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c index 34dfd134fc..80797b4dd5 100644 --- a/src/home/pam_systemd_home.c +++ b/src/home/pam_systemd_home.c @@ -359,7 +359,7 @@ static int handle_generic_user_record_error( return PAM_AUTHTOK_ERR; } - r = user_record_set_pkcs11_pin(secret, STRV_MAKE(newp), false); + r = user_record_set_token_pin(secret, STRV_MAKE(newp), false); if (r < 0) { pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r)); return PAM_SERVICE_ERR; @@ -388,7 +388,7 @@ static int handle_generic_user_record_error( return PAM_AUTHTOK_ERR; } - r = user_record_set_pkcs11_pin(secret, STRV_MAKE(newp), false); + r = user_record_set_token_pin(secret, STRV_MAKE(newp), false); if (r < 0) { pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r)); return PAM_SERVICE_ERR; @@ -407,7 +407,7 @@ static int handle_generic_user_record_error( return PAM_AUTHTOK_ERR; } - r = user_record_set_pkcs11_pin(secret, STRV_MAKE(newp), false); + r = user_record_set_token_pin(secret, STRV_MAKE(newp), false); if (r < 0) { pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r)); return PAM_SERVICE_ERR; @@ -426,7 +426,7 @@ static int handle_generic_user_record_error( return PAM_AUTHTOK_ERR; } - r = user_record_set_pkcs11_pin(secret, STRV_MAKE(newp), false); + r = user_record_set_token_pin(secret, STRV_MAKE(newp), false); if (r < 0) { pam_syslog(handle, LOG_ERR, "Failed to store PIN: %s", strerror_safe(r)); return PAM_SERVICE_ERR; diff --git a/src/home/user-record-util.c b/src/home/user-record-util.c index 8f51f8d6e8..f58f9e0709 100644 --- a/src/home/user-record-util.c +++ b/src/home/user-record-util.c @@ -887,7 +887,7 @@ int user_record_set_password(UserRecord *h, char **password, bool prepend) { return 0; } -int user_record_set_pkcs11_pin(UserRecord *h, char **pin, bool prepend) { +int user_record_set_token_pin(UserRecord *h, char **pin, bool prepend) { _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; _cleanup_(strv_free_erasep) char **e = NULL; int r; @@ -899,17 +899,17 @@ int user_record_set_pkcs11_pin(UserRecord *h, char **pin, bool prepend) { if (!e) return -ENOMEM; - r = strv_extend_strv(&e, h->pkcs11_pin, true); + r = strv_extend_strv(&e, h->token_pin, true); if (r < 0) return r; strv_uniq(e); - if (strv_equal(h->pkcs11_pin, e)) + if (strv_equal(h->token_pin, e)) return 0; } else { - if (strv_equal(h->pkcs11_pin, pin)) + if (strv_equal(h->token_pin, pin)) return 0; e = strv_copy(pin); @@ -922,7 +922,7 @@ int user_record_set_pkcs11_pin(UserRecord *h, char **pin, bool prepend) { w = json_variant_ref(json_variant_by_key(h->json, "secret")); if (strv_isempty(e)) - r = json_variant_filter(&w, STRV_MAKE("pkcs11Pin")); + r = json_variant_filter(&w, STRV_MAKE("tokenPin")); else { _cleanup_(json_variant_unrefp) JsonVariant *l = NULL; @@ -932,7 +932,7 @@ int user_record_set_pkcs11_pin(UserRecord *h, char **pin, bool prepend) { json_variant_sensitive(l); - r = json_variant_set_field(&w, "pkcs11Pin", l); + r = json_variant_set_field(&w, "tokenPin", l); } if (r < 0) return r; @@ -943,7 +943,7 @@ int user_record_set_pkcs11_pin(UserRecord *h, char **pin, bool prepend) { if (r < 0) return r; - strv_free_and_replace(h->pkcs11_pin, e); + strv_free_and_replace(h->token_pin, e); SET_FLAG(h->mask, USER_RECORD_SECRET, !json_variant_is_blank_object(w)); return 0; @@ -1062,7 +1062,7 @@ int user_record_merge_secret(UserRecord *h, UserRecord *secret) { if (r < 0) return r; - r = user_record_set_pkcs11_pin(h, secret->pkcs11_pin, true); + r = user_record_set_token_pin(h, secret->token_pin, true); if (r < 0) return r; diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h index 6afc8df19a..c20018fcba 100644 --- a/src/home/user-record-util.h +++ b/src/home/user-record-util.h @@ -47,7 +47,7 @@ int user_record_set_disk_size(UserRecord *h, uint64_t disk_size); int user_record_set_password(UserRecord *h, char **password, bool prepend); int user_record_make_hashed_password(UserRecord *h, char **password, bool extend); int user_record_set_hashed_password(UserRecord *h, char **hashed_password); -int user_record_set_pkcs11_pin(UserRecord *h, char **pin, bool prepend); +int user_record_set_token_pin(UserRecord *h, char **pin, bool prepend); int user_record_set_pkcs11_protected_authentication_path_permitted(UserRecord *h, int b); int user_record_set_password_change_now(UserRecord *h, int b); int user_record_merge_secret(UserRecord *h, UserRecord *secret); diff --git a/src/shared/user-record.c b/src/shared/user-record.c index f6f67eabc0..d4dbecaae9 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -120,7 +120,7 @@ static UserRecord* user_record_free(UserRecord *h) { strv_free_erase(h->hashed_password); strv_free_erase(h->ssh_authorized_keys); strv_free_erase(h->password); - strv_free_erase(h->pkcs11_pin); + strv_free_erase(h->token_pin); free(h->cifs_service); free(h->cifs_user_name); @@ -620,7 +620,8 @@ static int dispatch_secret(const char *name, JsonVariant *variant, JsonDispatchF static const JsonDispatch secret_dispatch_table[] = { { "password", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, password), 0 }, - { "pkcs11Pin", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, pkcs11_pin), 0 }, + { "tokenPin", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, token_pin), 0 }, + { "pkcs11Pin", /* legacy alias */ _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, token_pin), 0 }, { "pkcs11ProtectedAuthenticationPathPermitted", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, pkcs11_protected_authentication_path_permitted), 0 }, {}, }; diff --git a/src/shared/user-record.h b/src/shared/user-record.h index 9fd10610d9..9cc849c083 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -239,7 +239,7 @@ typedef struct UserRecord { char **hashed_password; char **ssh_authorized_keys; char **password; - char **pkcs11_pin; + char **token_pin; char *cifs_domain; char *cifs_user_name; From af4fbd463fa9c0f37a91a8f7b380655c6dcdba3f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Apr 2020 15:54:52 +0200 Subject: [PATCH 07/20] build-sys: add libfido2 as optional dependency --- meson.build | 12 ++++++++++++ meson_options.txt | 2 ++ 2 files changed, 14 insertions(+) diff --git a/meson.build b/meson.build index 08f322117f..e2ccb658c8 100644 --- a/meson.build +++ b/meson.build @@ -1153,6 +1153,17 @@ else endif conf.set10('HAVE_P11KIT', have) +want_libfido2 = get_option('libfido2') +if want_libfido2 != 'false' and not skip_deps + libfido2 = dependency('libfido2', + required : want_libfido2 == 'true') + have = libfido2.found() +else + have = false + libfido2 = [] +endif +conf.set10('HAVE_LIBFIDO2', have) + want_elfutils = get_option('elfutils') if want_elfutils != 'false' and not skip_deps libdw = dependency('libdw', @@ -3575,6 +3586,7 @@ foreach tuple : [ ['pwquality'], ['libfdisk'], ['p11kit'], + ['libfido2'], ['AUDIT'], ['IMA'], ['AppArmor'], diff --git a/meson_options.txt b/meson_options.txt index e13bfb0c4b..fd73d5e681 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -312,6 +312,8 @@ option('openssl', type : 'combo', choices : ['auto', 'true', 'false'], description : 'openssl support') option('p11kit', type : 'combo', choices : ['auto', 'true', 'false'], description : 'p11kit support') +option('libfido2', type : 'combo', choices : ['auto', 'true', 'false'], + description : 'FIDO2 support') option('elfutils', type : 'combo', choices : ['auto', 'true', 'false'], description : 'elfutils support') option('zlib', type : 'combo', choices : ['auto', 'true', 'false'], From 5e4fa456faacd36fbbd731d37803615e2e9594b7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Apr 2020 15:53:05 +0200 Subject: [PATCH 08/20] user-record: add fields for FIDO2 HMAC authentication options --- src/shared/user-record-show.c | 5 +- src/shared/user-record.c | 426 +++++++++++++++++++++++----------- src/shared/user-record.h | 22 ++ 3 files changed, 316 insertions(+), 137 deletions(-) diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c index de04859a2f..84ededd86e 100644 --- a/src/shared/user-record-show.c +++ b/src/shared/user-record-show.c @@ -472,10 +472,13 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) { STRV_FOREACH(i, hr->pkcs11_token_uri) printf(i == hr->pkcs11_token_uri ? - " Sec. Token: %s\n" : + "PKCS11 Token: %s\n" : " %s\n", *i); } + if (hr->n_fido2_hmac_credential > 0) + printf(" FIDO2 Token: %zu\n", hr->n_fido2_hmac_credential); + k = strv_length(hr->hashed_password); if (k == 0) printf(" Passwords: %snone%s\n", diff --git a/src/shared/user-record.c b/src/shared/user-record.c index d4dbecaae9..9d9b9689e1 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -95,6 +95,22 @@ static void pkcs11_encrypted_key_done(Pkcs11EncryptedKey *k) { erase_and_free(k->hashed_password); } +static void fido2_hmac_credential_done(Fido2HmacCredential *c) { + if (!c) + return; + + free(c->id); +} + +static void fido2_hmac_salt_done(Fido2HmacSalt *s) { + if (!s) + return; + + fido2_hmac_credential_done(&s->credential); + erase_and_free(s->salt); + erase_and_free(s->hashed_password); +} + static UserRecord* user_record_free(UserRecord *h) { if (!h) return NULL; @@ -147,6 +163,11 @@ static UserRecord* user_record_free(UserRecord *h) { pkcs11_encrypted_key_done(h->pkcs11_encrypted_key + i); free(h->pkcs11_encrypted_key); + for (size_t i = 0; i < h->n_fido2_hmac_credential; i++) + fido2_hmac_credential_done(h->fido2_hmac_credential + i); + for (size_t i = 0; i < h->n_fido2_hmac_salt; i++) + fido2_hmac_salt_done(h->fido2_hmac_salt + i); + json_variant_unref(h->json); return mfree(h); @@ -767,13 +788,141 @@ static int dispatch_pkcs11_key(const char *name, JsonVariant *variant, JsonDispa return 0; } +static int dispatch_fido2_hmac_credential(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + Fido2HmacCredential *k = userdata; + size_t l; + void *b; + int r; + + if (json_variant_is_null(variant)) { + k->id = mfree(k->id); + k->size = 0; + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + r = unbase64mem(json_variant_string(variant), (size_t) -1, &b, &l); + if (r < 0) + return json_log(variant, flags, r, "Failed to decode FIDO2 credential ID: %m"); + + free_and_replace(k->id, b); + k->size = l; + + return 0; +} + +static int dispatch_fido2_hmac_credential_array(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + UserRecord *h = userdata; + JsonVariant *e; + int r; + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of strings.", strna(name)); + + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + Fido2HmacCredential *array; + size_t l; + void *b; + + if (!json_variant_is_string(e)) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a string."); + + array = reallocarray(h->fido2_hmac_credential, h->n_fido2_hmac_credential + 1, sizeof(Fido2HmacCredential)); + if (!array) + return log_oom(); + + r = unbase64mem(json_variant_string(e), (size_t) -1, &b, &l); + if (r < 0) + return json_log(variant, flags, r, "Failed to decode FIDO2 credential ID: %m"); + + h->fido2_hmac_credential = array; + + h->fido2_hmac_credential[h->n_fido2_hmac_credential++] = (Fido2HmacCredential) { + .id = b, + .size = l, + }; + } + + return 0; +} + +static int dispatch_fido2_hmac_salt_value(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + Fido2HmacSalt *k = userdata; + size_t l; + void *b; + int r; + + if (json_variant_is_null(variant)) { + k->salt = erase_and_free(k->salt); + k->salt_size = 0; + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + r = unbase64mem(json_variant_string(variant), (size_t) -1, &b, &l); + if (r < 0) + return json_log(variant, flags, r, "Failed to decode FIDO2 salt: %m"); + + erase_and_free(k->salt); + k->salt = b; + k->salt_size = l; + + return 0; +} + +static int dispatch_fido2_hmac_salt(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + UserRecord *h = userdata; + JsonVariant *e; + int r; + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of objects.", strna(name)); + + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + Fido2HmacSalt *array, *k; + + static const JsonDispatch fido2_hmac_salt_dispatch_table[] = { + { "credential", JSON_VARIANT_STRING, dispatch_fido2_hmac_credential, offsetof(Fido2HmacSalt, credential), JSON_MANDATORY }, + { "salt", JSON_VARIANT_STRING, dispatch_fido2_hmac_salt_value, 0, JSON_MANDATORY }, + { "hashedPassword", JSON_VARIANT_STRING, json_dispatch_string, offsetof(Fido2HmacSalt, hashed_password), JSON_MANDATORY }, + {}, + }; + + if (!json_variant_is_object(e)) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not an object."); + + array = reallocarray(h->fido2_hmac_salt, h->n_fido2_hmac_salt + 1, sizeof(Fido2HmacSalt)); + if (!array) + return log_oom(); + + h->fido2_hmac_salt = array; + k = h->fido2_hmac_salt + h->n_fido2_hmac_salt; + *k = (Fido2HmacSalt) {}; + + r = json_dispatch(e, fido2_hmac_salt_dispatch_table, NULL, flags, k); + if (r < 0) { + fido2_hmac_salt_done(k); + return r; + } + + h->n_fido2_hmac_salt++; + } + + return 0; +} + static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { static const JsonDispatch privileged_dispatch_table[] = { - { "passwordHint", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, password_hint), 0 }, - { "hashedPassword", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, hashed_password), JSON_SAFE }, - { "sshAuthorizedKeys", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, ssh_authorized_keys), 0 }, - { "pkcs11EncryptedKey", JSON_VARIANT_ARRAY, dispatch_pkcs11_key, 0, 0 }, + { "passwordHint", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, password_hint), 0 }, + { "hashedPassword", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, hashed_password), JSON_SAFE }, + { "sshAuthorizedKeys", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, ssh_authorized_keys), 0 }, + { "pkcs11EncryptedKey", JSON_VARIANT_ARRAY, dispatch_pkcs11_key, 0, 0 }, + { "fido2HmacSalt", JSON_VARIANT_ARRAY, dispatch_fido2_hmac_salt, 0, 0 }, {}, }; @@ -907,66 +1056,67 @@ int per_machine_hostname_match(JsonVariant *hns, JsonDispatchFlags flags) { static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { static const JsonDispatch per_machine_dispatch_table[] = { - { "matchMachineId", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, - { "matchHostname", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, - { "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE }, - { "location", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, location), 0 }, - { "shell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 }, - { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 }, - { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 }, - { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE }, - { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE }, - { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 }, - { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 }, - { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 }, - { "notBeforeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_before_usec), 0 }, - { "notAfterUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_after_usec), 0 }, - { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 }, - { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_disk_size, offsetof(UserRecord, disk_size), 0 }, - { "diskSizeRelative", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 }, - { "skeletonDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), 0 }, - { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, - { "tasksMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 }, - { "memoryHigh", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 }, - { "memoryMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 }, - { "cpuWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, cpu_weight), 0 }, - { "ioWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, io_weight), 0 }, - { "mountNoDevices", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nodev), 0 }, - { "mountNoSuid", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nosuid), 0 }, - { "mountNoExecute", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, noexec), 0 }, - { "cifsDomain", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_domain), JSON_SAFE }, - { "cifsUserName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_user_name), JSON_SAFE }, - { "cifsService", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_service), JSON_SAFE }, - { "imagePath", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, image_path), 0 }, - { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 }, - { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 }, - { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), JSON_RELAX}, - { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE }, - { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 }, - { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, - { "fileSystemUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 }, - { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0, }, - { "luksOfflineDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_offline_discard), 0, }, - { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE }, - { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE }, - { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 }, - { "luksPbkdfHashAlgorithm", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_hash_algorithm), JSON_SAFE }, - { "luksPbkdfType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_type), JSON_SAFE }, - { "luksPbkdfTimeCostUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_time_cost_usec), 0 }, - { "luksPbkdfMemoryCost", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_memory_cost), 0 }, - { "luksPbkdfParallelThreads", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_parallel_threads), 0 }, - { "rateLimitIntervalUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_interval_usec), 0 }, - { "rateLimitBurst", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_burst), 0 }, - { "enforcePasswordPolicy", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, enforce_password_policy), 0 }, - { "autoLogin", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, auto_login), 0 }, - { "stopDelayUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, stop_delay_usec), 0 }, - { "killProcesses", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, kill_processes), 0 }, - { "passwordChangeMinUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_min_usec), 0 }, - { "passwordChangeMaxUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_max_usec), 0 }, - { "passwordChangeWarnUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_warn_usec), 0 }, - { "passwordChangeInactiveUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_inactive_usec), 0 }, - { "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 }, - { "pkcs11TokenUri", JSON_VARIANT_ARRAY, dispatch_pkcs11_uri_array, offsetof(UserRecord, pkcs11_token_uri), 0 }, + { "matchMachineId", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "matchHostname", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE }, + { "location", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, location), 0 }, + { "shell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 }, + { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 }, + { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 }, + { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE }, + { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE }, + { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 }, + { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 }, + { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 }, + { "notBeforeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_before_usec), 0 }, + { "notAfterUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_after_usec), 0 }, + { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 }, + { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_disk_size, offsetof(UserRecord, disk_size), 0 }, + { "diskSizeRelative", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 }, + { "skeletonDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), 0 }, + { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, + { "tasksMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 }, + { "memoryHigh", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 }, + { "memoryMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 }, + { "cpuWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, cpu_weight), 0 }, + { "ioWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, io_weight), 0 }, + { "mountNoDevices", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nodev), 0 }, + { "mountNoSuid", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nosuid), 0 }, + { "mountNoExecute", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, noexec), 0 }, + { "cifsDomain", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_domain), JSON_SAFE }, + { "cifsUserName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_user_name), JSON_SAFE }, + { "cifsService", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_service), JSON_SAFE }, + { "imagePath", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, image_path), 0 }, + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 }, + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 }, + { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), JSON_RELAX}, + { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE }, + { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 }, + { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, + { "fileSystemUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 }, + { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0, }, + { "luksOfflineDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_offline_discard), 0, }, + { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE }, + { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE }, + { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 }, + { "luksPbkdfHashAlgorithm", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_hash_algorithm), JSON_SAFE }, + { "luksPbkdfType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_type), JSON_SAFE }, + { "luksPbkdfTimeCostUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_time_cost_usec), 0 }, + { "luksPbkdfMemoryCost", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_memory_cost), 0 }, + { "luksPbkdfParallelThreads", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_parallel_threads), 0 }, + { "rateLimitIntervalUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_interval_usec), 0 }, + { "rateLimitBurst", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_burst), 0 }, + { "enforcePasswordPolicy", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, enforce_password_policy), 0 }, + { "autoLogin", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, auto_login), 0 }, + { "stopDelayUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, stop_delay_usec), 0 }, + { "killProcesses", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, kill_processes), 0 }, + { "passwordChangeMinUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_min_usec), 0 }, + { "passwordChangeMaxUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_max_usec), 0 }, + { "passwordChangeWarnUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_warn_usec), 0 }, + { "passwordChangeInactiveUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_inactive_usec), 0 }, + { "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 }, + { "pkcs11TokenUri", JSON_VARIANT_ARRAY, dispatch_pkcs11_uri_array, offsetof(UserRecord, pkcs11_token_uri), 0 }, + { "fido2HmacCredential", JSON_VARIANT_ARRAY, dispatch_fido2_hmac_credential_array, 0, 0 }, {}, }; @@ -1248,84 +1398,85 @@ int user_group_record_mangle( int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_flags) { static const JsonDispatch user_dispatch_table[] = { - { "userName", JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(UserRecord, user_name), JSON_RELAX}, - { "realm", JSON_VARIANT_STRING, json_dispatch_realm, offsetof(UserRecord, realm), 0 }, - { "realName", JSON_VARIANT_STRING, json_dispatch_gecos, offsetof(UserRecord, real_name), 0 }, - { "emailAddress", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, email_address), JSON_SAFE }, - { "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE }, - { "location", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, location), 0 }, - { "disposition", JSON_VARIANT_STRING, json_dispatch_user_disposition, offsetof(UserRecord, disposition), 0 }, - { "lastChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_change_usec), 0 }, - { "lastPasswordChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_password_change_usec), 0 }, - { "shell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 }, - { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 }, - { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 }, - { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE }, - { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE }, - { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 }, - { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 }, - { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 }, - { "notBeforeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_before_usec), 0 }, - { "notAfterUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_after_usec), 0 }, - { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 }, - { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_disk_size, offsetof(UserRecord, disk_size), 0 }, - { "diskSizeRelative", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 }, - { "skeletonDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), 0 }, - { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, - { "tasksMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 }, - { "memoryHigh", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 }, - { "memoryMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 }, - { "cpuWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, cpu_weight), 0 }, - { "ioWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, io_weight), 0 }, - { "mountNoDevices", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nodev), 0 }, - { "mountNoSuid", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nosuid), 0 }, - { "mountNoExecute", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, noexec), 0 }, - { "cifsDomain", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_domain), JSON_SAFE }, - { "cifsUserName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_user_name), JSON_SAFE }, - { "cifsService", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_service), JSON_SAFE }, - { "imagePath", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, image_path), 0 }, - { "homeDirectory", JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, home_directory), 0 }, - { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 }, - { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 }, - { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), JSON_RELAX}, - { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE }, - { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 }, - { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, - { "fileSystemUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 }, - { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0 }, + { "userName", JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(UserRecord, user_name), JSON_RELAX}, + { "realm", JSON_VARIANT_STRING, json_dispatch_realm, offsetof(UserRecord, realm), 0 }, + { "realName", JSON_VARIANT_STRING, json_dispatch_gecos, offsetof(UserRecord, real_name), 0 }, + { "emailAddress", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, email_address), JSON_SAFE }, + { "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE }, + { "location", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, location), 0 }, + { "disposition", JSON_VARIANT_STRING, json_dispatch_user_disposition, offsetof(UserRecord, disposition), 0 }, + { "lastChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_change_usec), 0 }, + { "lastPasswordChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_password_change_usec), 0 }, + { "shell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 }, + { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 }, + { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 }, + { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE }, + { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE }, + { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 }, + { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 }, + { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 }, + { "notBeforeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_before_usec), 0 }, + { "notAfterUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_after_usec), 0 }, + { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 }, + { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_disk_size, offsetof(UserRecord, disk_size), 0 }, + { "diskSizeRelative", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 }, + { "skeletonDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), 0 }, + { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, + { "tasksMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 }, + { "memoryHigh", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 }, + { "memoryMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 }, + { "cpuWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, cpu_weight), 0 }, + { "ioWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, io_weight), 0 }, + { "mountNoDevices", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nodev), 0 }, + { "mountNoSuid", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nosuid), 0 }, + { "mountNoExecute", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, noexec), 0 }, + { "cifsDomain", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_domain), JSON_SAFE }, + { "cifsUserName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_user_name), JSON_SAFE }, + { "cifsService", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_service), JSON_SAFE }, + { "imagePath", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, image_path), 0 }, + { "homeDirectory", JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, home_directory), 0 }, + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 }, + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 }, + { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), JSON_RELAX}, + { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE }, + { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 }, + { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, + { "fileSystemUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 }, + { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0 }, { "luksOfflineDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_offline_discard), 0 }, - { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE }, - { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE }, - { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 }, - { "luksPbkdfHashAlgorithm", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_hash_algorithm), JSON_SAFE }, - { "luksPbkdfType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_type), JSON_SAFE }, - { "luksPbkdfTimeCostUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_time_cost_usec), 0 }, - { "luksPbkdfMemoryCost", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_memory_cost), 0 }, - { "luksPbkdfParallelThreads", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_parallel_threads), 0 }, - { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE }, - { "rateLimitIntervalUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_interval_usec), 0 }, - { "rateLimitBurst", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_burst), 0 }, - { "enforcePasswordPolicy", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, enforce_password_policy), 0 }, - { "autoLogin", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, auto_login), 0 }, - { "stopDelayUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, stop_delay_usec), 0 }, - { "killProcesses", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, kill_processes), 0 }, - { "passwordChangeMinUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_min_usec), 0 }, - { "passwordChangeMaxUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_max_usec), 0 }, - { "passwordChangeWarnUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_warn_usec), 0 }, - { "passwordChangeInactiveUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_inactive_usec), 0 }, - { "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 }, - { "pkcs11TokenUri", JSON_VARIANT_ARRAY, dispatch_pkcs11_uri_array, offsetof(UserRecord, pkcs11_token_uri), 0 }, + { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE }, + { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE }, + { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 }, + { "luksPbkdfHashAlgorithm", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_hash_algorithm), JSON_SAFE }, + { "luksPbkdfType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_type), JSON_SAFE }, + { "luksPbkdfTimeCostUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_time_cost_usec), 0 }, + { "luksPbkdfMemoryCost", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_memory_cost), 0 }, + { "luksPbkdfParallelThreads", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_parallel_threads), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE }, + { "rateLimitIntervalUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_interval_usec), 0 }, + { "rateLimitBurst", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_burst), 0 }, + { "enforcePasswordPolicy", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, enforce_password_policy), 0 }, + { "autoLogin", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, auto_login), 0 }, + { "stopDelayUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, stop_delay_usec), 0 }, + { "killProcesses", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, kill_processes), 0 }, + { "passwordChangeMinUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_min_usec), 0 }, + { "passwordChangeMaxUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_max_usec), 0 }, + { "passwordChangeWarnUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_warn_usec), 0 }, + { "passwordChangeInactiveUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_inactive_usec), 0 }, + { "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 }, + { "pkcs11TokenUri", JSON_VARIANT_ARRAY, dispatch_pkcs11_uri_array, offsetof(UserRecord, pkcs11_token_uri), 0 }, + { "fido2HmacCredential", JSON_VARIANT_ARRAY, dispatch_fido2_hmac_credential_array, 0, 0 }, - { "secret", JSON_VARIANT_OBJECT, dispatch_secret, 0, 0 }, - { "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 }, + { "secret", JSON_VARIANT_OBJECT, dispatch_secret, 0, 0 }, + { "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 }, /* Ignore the perMachine, binding, status stuff here, and process it later, so that it overrides whatever is set above */ - { "perMachine", JSON_VARIANT_ARRAY, NULL, 0, 0 }, - { "binding", JSON_VARIANT_OBJECT, NULL, 0, 0 }, - { "status", JSON_VARIANT_OBJECT, NULL, 0, 0 }, + { "perMachine", JSON_VARIANT_ARRAY, NULL, 0, 0 }, + { "binding", JSON_VARIANT_OBJECT, NULL, 0, 0 }, + { "status", JSON_VARIANT_OBJECT, NULL, 0, 0 }, /* Ignore 'signature', we check it with explicit accessors instead */ - { "signature", JSON_VARIANT_ARRAY, NULL, 0, 0 }, + { "signature", JSON_VARIANT_ARRAY, NULL, 0, 0 }, {}, }; @@ -1685,6 +1836,9 @@ bool user_record_can_authenticate(UserRecord *h) { if (h->n_pkcs11_encrypted_key > 0) return true; + if (h->n_fido2_hmac_salt > 0) + return true; + return !strv_isempty(h->hashed_password); } diff --git a/src/shared/user-record.h b/src/shared/user-record.h index 9cc849c083..9e036c562b 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -189,6 +189,23 @@ typedef struct Pkcs11EncryptedKey { char *hashed_password; } Pkcs11EncryptedKey; +typedef struct Fido2HmacCredential { + void *id; + size_t size; +} Fido2HmacCredential; + +typedef struct Fido2HmacSalt { + /* The FIDO2 Cridential ID to use */ + Fido2HmacCredential credential; + + /* The FIDO2 salt value */ + void *salt; + size_t salt_size; + + /* What to test the hashed salt value against, usualy UNIX password hash here. */ + char *hashed_password; +} Fido2HmacSalt; + typedef struct UserRecord { /* The following three fields are not part of the JSON record */ unsigned n_ref; @@ -309,6 +326,11 @@ typedef struct UserRecord { size_t n_pkcs11_encrypted_key; int pkcs11_protected_authentication_path_permitted; + Fido2HmacCredential *fido2_hmac_credential; + size_t n_fido2_hmac_credential; + Fido2HmacSalt *fido2_hmac_salt; + size_t n_fido2_hmac_salt; + JsonVariant *json; } UserRecord; From 1c0c4a43c6118aa4057222789e4b777b61e4bb27 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Apr 2020 15:54:12 +0200 Subject: [PATCH 09/20] homectl: add support for enrolling FIDO2 HMAC-SECRET tokens --- meson.build | 1 + src/home/homectl-fido2.c | 531 +++++++++++++++++++++++++++++++++++++++ src/home/homectl-fido2.h | 10 + src/home/homectl.c | 63 ++++- src/home/meson.build | 2 + 5 files changed, 604 insertions(+), 3 deletions(-) create mode 100644 src/home/homectl-fido2.c create mode 100644 src/home/homectl-fido2.h diff --git a/meson.build b/meson.build index e2ccb658c8..4422d73a29 100644 --- a/meson.build +++ b/meson.build @@ -2184,6 +2184,7 @@ if conf.get('ENABLE_HOMED') == 1 libcrypt, libopenssl, libp11kit, + libfido2, libpwquality], install_rpath : rootlibexecdir, install : true, diff --git a/src/home/homectl-fido2.c b/src/home/homectl-fido2.c new file mode 100644 index 0000000000..c78645fa3b --- /dev/null +++ b/src/home/homectl-fido2.c @@ -0,0 +1,531 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#if HAVE_LIBFIDO2 +#include +#endif + +#include "ask-password-api.h" +#include "errno-util.h" +#include "format-table.h" +#include "hexdecoct.h" +#include "homectl-fido2.h" +#include "libcrypt-util.h" +#include "locale-util.h" +#include "memory-util.h" +#include "random-util.h" +#include "strv.h" + +#if HAVE_LIBFIDO2 +static int add_fido2_credential_id( + JsonVariant **v, + const void *cid, + size_t cid_size) { + + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + _cleanup_strv_free_ char **l = NULL; + _cleanup_free_ char *escaped = NULL; + int r; + + assert(v); + assert(cid); + + r = base64mem(cid, cid_size, &escaped); + if (r < 0) + return log_error_errno(r, "Failed to base64 encode FIDO2 credential ID: %m"); + + w = json_variant_ref(json_variant_by_key(*v, "fido2HmacCredential")); + if (w) { + r = json_variant_strv(w, &l); + if (r < 0) + return log_error_errno(r, "Failed to parse FIDO2 credential ID list: %m"); + + if (strv_contains(l, escaped)) + return 0; + } + + r = strv_extend(&l, escaped); + if (r < 0) + return log_oom(); + + w = json_variant_unref(w); + r = json_variant_new_array_strv(&w, l); + if (r < 0) + return log_error_errno(r, "Failed to create FIDO2 credential ID JSON: %m"); + + r = json_variant_set_field(v, "fido2HmacCredential", w); + if (r < 0) + return log_error_errno(r, "Failed to update FIDO2 credential ID: %m"); + + return 0; +} + +static int add_fido2_salt( + JsonVariant **v, + const void *cid, + size_t cid_size, + const void *fido2_salt, + size_t fido2_salt_size, + const void *secret, + size_t secret_size) { + + _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL; + _cleanup_(erase_and_freep) char *base64_encoded = NULL; + _cleanup_free_ char *unix_salt = NULL; + struct crypt_data cd = {}; + char *k; + int r; + + r = make_salt(&unix_salt); + if (r < 0) + return log_error_errno(r, "Failed to generate salt: %m"); + + /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends + * expect a NUL terminated string, and we use a binary key */ + r = base64mem(secret, secret_size, &base64_encoded); + if (r < 0) + return log_error_errno(r, "Failed to base64 encode secret key: %m"); + + errno = 0; + k = crypt_r(base64_encoded, unix_salt, &cd); + if (!k) + return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m"); + + r = json_build(&e, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("credential", JSON_BUILD_BASE64(cid, cid_size)), + JSON_BUILD_PAIR("salt", JSON_BUILD_BASE64(fido2_salt, fido2_salt_size)), + JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(k)))); + if (r < 0) + return log_error_errno(r, "Failed to build FIDO2 salt JSON key object: %m"); + + w = json_variant_ref(json_variant_by_key(*v, "privileged")); + l = json_variant_ref(json_variant_by_key(w, "fido2HmacSalt")); + + r = json_variant_append_array(&l, e); + if (r < 0) + return log_error_errno(r, "Failed append FIDO2 salt: %m"); + + r = json_variant_set_field(&w, "fido2HmacSalt", l); + if (r < 0) + return log_error_errno(r, "Failed to set FDO2 salt: %m"); + + r = json_variant_set_field(v, "privileged", w); + if (r < 0) + return log_error_errno(r, "Failed to update privileged field: %m"); + + return 0; +} +#endif + +#define FIDO2_SALT_SIZE 32 + +int identity_add_fido2_parameters( + JsonVariant **v, + 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_(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; + const void *cid, *secret; + 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); + + 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 = fido_dev_new(); + if (!d) + return log_oom(); + + r = 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)); + + if (!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(); + if (!di) + return log_oom(); + + r = 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)); + + e = fido_cbor_info_extensions_ptr(di); + n = 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 = fido_cred_new(); + if (!c) + return log_oom(); + + r = 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)); + + r = 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)); + + r = 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)); + + un = json_variant_by_key(*v, "userName"); + if (!un) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "userName field of user record is missing"); + if (!json_variant_is_string(un)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "userName field of user record is not a string"); + + realm = json_variant_by_key(*v, "realm"); + if (realm) { + if (!json_variant_is_string(realm)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "realm field of user record is not a string"); + + fido_un = strjoina(json_variant_string(un), json_variant_string(realm)); + } else + fido_un = json_variant_string(un); + + rn = json_variant_by_key(*v, "realName"); + if (rn && !json_variant_is_string(rn)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "realName field of user record is not a string"); + + r = 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)); + + r = 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)); + + r = 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)); + + r = 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)); + + 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 = 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 = 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", fido_strerr(r)); + + cid = 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); + + a = fido_assert_new(); + if (!a) + return log_oom(); + + r = 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)); + + r = 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)); + + r = 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)); + + r = 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)); + + r = 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)); + + r = 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)); + + log_info("Generating secret key on FIDO2 security token."); + + r = fido_dev_get_assert(d, a, used_pin); + if (r == FIDO_ERR_UP_REQUIRED) { + r = 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)); + + 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); + } + 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)); + + secret = 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); + + r = add_fido2_credential_id(v, cid, cid_size); + if (r < 0) + return r; + + r = add_fido2_salt(v, + cid, + cid_size, + salt, + FIDO2_SALT_SIZE, + secret, + secret_size); + if (r < 0) + return r; + + return 0; +#else + return log_error_errno(EOPNOTSUPP, "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; + + di = fido_dev_info_new(allocated); + if (!di) + return log_oom(); + + r = 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", 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 = 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, fido_dev_info_path(entry), + TABLE_STRING, fido_dev_info_manufacturer_string(entry), + TABLE_STRING, 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: + fido_dev_info_free(&di, allocated); + return r; +#else + return log_error_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; + + di = fido_dev_info_new(di_size); + if (!di) + return log_oom(); + + r = 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)); + goto finish; + } + if (found > 1) { + r = log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "More than one FIDO2 device found."); + goto finish; + } + + entry = 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); + 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: + fido_dev_info_free(&di, di_size); + return r; +#else + return log_error_errno(EOPNOTSUPP, "FIDO2 tokens not supported on this build."); +#endif +} diff --git a/src/home/homectl-fido2.h b/src/home/homectl-fido2.h new file mode 100644 index 0000000000..0d9faefa81 --- /dev/null +++ b/src/home/homectl-fido2.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#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 e69fa63fa3..a7b548aa61 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -19,6 +19,7 @@ #include "fs-util.h" #include "hexdecoct.h" #include "home-util.h" +#include "homectl-fido2.h" #include "libcrypt-util.h" #include "locale-util.h" #include "main-func.h" @@ -56,6 +57,7 @@ static char **arg_identity_filter_rlimits = NULL; static uint64_t arg_disk_size = UINT64_MAX; static uint64_t arg_disk_size_relative = UINT64_MAX; static char **arg_pkcs11_token_uri = NULL; +static char **arg_fido2_device = NULL; static bool arg_json = false; static JsonFormatFlags arg_json_format_flags = 0; static bool arg_and_resize = false; @@ -73,6 +75,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_rlimits, json_variant_unrefp); STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_identity_filter_rlimits, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, strv_freep); static bool identity_properties_specified(void) { return @@ -83,7 +86,8 @@ static bool identity_properties_specified(void) { !json_variant_is_blank_object(arg_identity_extra_rlimits) || !strv_isempty(arg_identity_filter) || !strv_isempty(arg_identity_filter_rlimits) || - !strv_isempty(arg_pkcs11_token_uri); + !strv_isempty(arg_pkcs11_token_uri) || + !strv_isempty(arg_fido2_device); } static int acquire_bus(sd_bus **bus) { @@ -1251,6 +1255,12 @@ static int acquire_new_home_record(UserRecord **ret) { return r; } + STRV_FOREACH(i, arg_fido2_device) { + r = identity_add_fido2_parameters(&v, *i); + if (r < 0) + return r; + } + r = update_last_change(&v, true, false); if (r < 0) return r; @@ -1571,9 +1581,15 @@ static int acquire_updated_home_record( return r; } + STRV_FOREACH(i, arg_fido2_device) { + r = identity_add_fido2_parameters(&json, *i); + if (r < 0) + return r; + } + /* If the user supplied a full record, then add in lastChange, but do not override. Otherwise always * override. */ - r = update_last_change(&json, !!arg_pkcs11_token_uri, !arg_identity); + r = update_last_change(&json, arg_pkcs11_token_uri || arg_fido2_device, !arg_identity); if (r < 0) return r; @@ -1732,6 +1748,8 @@ static int passwd_home(int argc, char *argv[], void *userdata) { if (arg_pkcs11_token_uri) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "To change the PKCS#11 security token use 'homectl update --pkcs11-token-uri=…'."); + if (arg_fido2_device) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "To change the FIDO2 security token use 'homectl update --fido2-device=…'."); if (identity_properties_specified()) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The 'passwd' verb does not permit changing other record properties at the same time."); @@ -2182,6 +2200,8 @@ static int help(int argc, char *argv[], void *userdata) { " Specify SSH public keys\n" " --pkcs11-token-uri=URI URI to PKCS#11 security token containing\n" " private key and matching X.509 certificate\n" + " --fido2-device=PATH Path to FIDO2 hidraw device with hmac-secret\n" + " extension\n" "\n%4$sAccount Management User Record Properties:%5$s\n" " --locked=BOOL Set locked account state\n" " --not-before=TIMESTAMP Do not allow logins before\n" @@ -2328,6 +2348,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_EXPORT_FORMAT, ARG_AUTO_LOGIN, ARG_PKCS11_TOKEN_URI, + ARG_FIDO2_DEVICE, ARG_AND_RESIZE, ARG_AND_CHANGE_PASSWORD, }; @@ -2405,6 +2426,7 @@ static int parse_argv(int argc, char *argv[]) { { "json", required_argument, NULL, ARG_JSON }, { "export-format", required_argument, NULL, ARG_EXPORT_FORMAT }, { "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI }, + { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE }, { "and-resize", required_argument, NULL, ARG_AND_RESIZE }, { "and-change-password", required_argument, NULL, ARG_AND_CHANGE_PASSWORD }, {} @@ -3388,6 +3410,41 @@ static int parse_argv(int argc, char *argv[]) { break; } + case ARG_FIDO2_DEVICE: { + const char *p; + + if (streq(optarg, "list")) + return list_fido2_devices(); + + FOREACH_STRING(p, "fido2HmacCredential", "fido2HmacSalt") { + r = drop_from_identity(p); + if (r < 0) + return r; + } + + if (isempty(optarg)) { + arg_fido2_device = strv_free(arg_fido2_device); + break; + } + + if (streq(optarg, "auto")) { + _cleanup_free_ char *found = NULL; + + r = find_fido2_auto(&found); + if (r < 0) + return r; + + r = strv_consume(&arg_fido2_device, TAKE_PTR(found)); + } else + r = strv_extend(&arg_fido2_device, optarg); + + if (r < 0) + return r; + + strv_uniq(arg_fido2_device); + break; + } + case 'j': arg_json = true; arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO; @@ -3458,7 +3515,7 @@ static int parse_argv(int argc, char *argv[]) { } } - if (!strv_isempty(arg_pkcs11_token_uri)) + if (!strv_isempty(arg_pkcs11_token_uri) || !strv_isempty(arg_fido2_device)) arg_and_change_password = true; if (arg_disk_size != UINT64_MAX || arg_disk_size_relative != UINT64_MAX) diff --git a/src/home/meson.build b/src/home/meson.build index 2c5664aae1..3fa6bc08c9 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -65,6 +65,8 @@ systemd_homed_sources += [homed_gperf_c] homectl_sources = files(''' home-util.c home-util.h + homectl-fido2.c + homectl-fido2.h homectl.c pwquality-util.c pwquality-util.h From 7b78db28e544f3007b22b612da229773987fdf74 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Apr 2020 19:18:09 +0200 Subject: [PATCH 10/20] homed: add support for authenticating with fido2 hmac-secret tokens --- meson.build | 3 +- src/home/homectl.c | 12 +- src/home/homed-home.c | 12 +- src/home/homework-cifs.c | 4 +- src/home/homework-cifs.h | 2 +- src/home/homework-directory.c | 12 +- src/home/homework-directory.h | 4 +- src/home/homework-fido2.c | 197 +++++++++++++++++ src/home/homework-fido2.h | 6 + src/home/homework-fscrypt.c | 25 ++- src/home/homework-fscrypt.h | 4 +- src/home/homework-luks.c | 100 +++++---- src/home/homework-luks.h | 12 +- src/home/homework.c | 250 ++++++++++++++++------ src/home/homework.h | 16 +- src/home/meson.build | 4 + src/home/pam_systemd_home.c | 10 + src/home/user-record-util.c | 40 +++- src/home/user-record-util.h | 1 + src/libsystemd/sd-bus/bus-common-errors.c | 2 + src/libsystemd/sd-bus/bus-common-errors.h | 2 + src/shared/user-record.c | 2 + src/shared/user-record.h | 1 + 23 files changed, 568 insertions(+), 153 deletions(-) create mode 100644 src/home/homework-fido2.c create mode 100644 src/home/homework-fido2.h diff --git a/meson.build b/meson.build index 4422d73a29..fc75fddf6b 100644 --- a/meson.build +++ b/meson.build @@ -2157,7 +2157,8 @@ if conf.get('ENABLE_HOMED') == 1 libcrypt, libopenssl, libfdisk, - libp11kit], + libp11kit, + libfido2], install_rpath : rootlibexecdir, install : true, install_dir : rootlibexecdir) diff --git a/src/home/homectl.c b/src/home/homectl.c index a7b548aa61..886069fb49 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -331,8 +331,18 @@ static int handle_generic_user_record_error( if (r < 0) return log_error_errno(r, "Failed to set PKCS#11 protected authentication path permitted flag: %m"); + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_USER_PRESENCE_NEEDED)) { + + log_notice("%s%sAuthentication requires presence verification on security token.", + emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", + emoji_enabled() ? " " : ""); + + r = user_record_set_fido2_user_presence_permitted(hr, true); + if (r < 0) + return log_error_errno(r, "Failed to set FIDO2 user presence permitted flag: %m"); + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_LOCKED)) - return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Security token PIN is locked, please unlock security token PIN first."); + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Security token PIN is locked, please unlock it first. (Hint: Removal and re-insertion might suffice.)"); else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN)) { diff --git a/src/home/homed-home.c b/src/home/homed-home.c index 47ee7d2328..f0c157cb7d 100644 --- a/src/home/homed-home.c +++ b/src/home/homed-home.c @@ -457,6 +457,10 @@ static int convert_worker_errno(Home *h, int e, sd_bus_error *error) { return sd_bus_error_setf(error, BUS_ERROR_TOKEN_PIN_NEEDED, "PIN for security token required."); case -ERFKILL: return sd_bus_error_setf(error, BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED, "Security token requires protected authentication path."); + case -EMEDIUMTYPE: + return sd_bus_error_setf(error, BUS_ERROR_TOKEN_USER_PRESENCE_NEEDED, "Security token requires user presence."); + case -ENOSTR: + return sd_bus_error_setf(error, BUS_ERROR_TOKEN_ACTION_TIMEOUT, "Token action timeout. (User was supposed to verify presence or similar, by interacting with the token, and didn't do that in time.)"); case -EOWNERDEAD: return sd_bus_error_setf(error, BUS_ERROR_TOKEN_PIN_LOCKED, "PIN of security token locked."); case -ENOLCK: @@ -1357,7 +1361,13 @@ static int user_record_extend_with_binding(UserRecord *hr, UserRecord *with_bind return 0; } -static int home_update_internal(Home *h, const char *verb, UserRecord *hr, UserRecord *secret, sd_bus_error *error) { +static int home_update_internal( + Home *h, + const char *verb, + UserRecord *hr, + UserRecord *secret, + sd_bus_error *error) { + _cleanup_(user_record_unrefp) UserRecord *new_hr = NULL, *saved_secret = NULL, *signed_hr = NULL; int r, c; diff --git a/src/home/homework-cifs.c b/src/home/homework-cifs.c index 27c92e16e7..cfceaed742 100644 --- a/src/home/homework-cifs.c +++ b/src/home/homework-cifs.c @@ -98,7 +98,7 @@ int home_prepare_cifs( int home_activate_cifs( UserRecord *h, - char ***pkcs11_decrypted_passwords, + PasswordCache *cache, UserRecord **ret_home) { _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; @@ -120,7 +120,7 @@ int home_activate_cifs( if (r < 0) return r; - r = home_refresh(h, &setup, NULL, pkcs11_decrypted_passwords, NULL, &new_home); + r = home_refresh(h, &setup, NULL, cache, NULL, &new_home); if (r < 0) return r; diff --git a/src/home/homework-cifs.h b/src/home/homework-cifs.h index 346be8826e..ee799e2a4b 100644 --- a/src/home/homework-cifs.h +++ b/src/home/homework-cifs.h @@ -6,6 +6,6 @@ int home_prepare_cifs(UserRecord *h, bool already_activated, HomeSetup *setup); -int home_activate_cifs(UserRecord *h, char ***pkcs11_decrypted_passwords, UserRecord **ret_home); +int home_activate_cifs(UserRecord *h, PasswordCache *cache, UserRecord **ret_home); int home_create_cifs(UserRecord *h, UserRecord **ret_home); diff --git a/src/home/homework-directory.c b/src/home/homework-directory.c index 8a4cb1732a..7d00da214a 100644 --- a/src/home/homework-directory.c +++ b/src/home/homework-directory.c @@ -26,7 +26,7 @@ int home_prepare_directory(UserRecord *h, bool already_activated, HomeSetup *set int home_activate_directory( UserRecord *h, - char ***pkcs11_decrypted_passwords, + PasswordCache *cache, UserRecord **ret_home) { _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL; @@ -44,11 +44,11 @@ int home_activate_directory( assert_se(hdo = user_record_home_directory(h)); hd = strdupa(hdo); - r = home_prepare(h, false, pkcs11_decrypted_passwords, &setup, &header_home); + r = home_prepare(h, false, cache, &setup, &header_home); if (r < 0) return r; - r = home_refresh(h, &setup, header_home, pkcs11_decrypted_passwords, NULL, &new_home); + r = home_refresh(h, &setup, header_home, cache, NULL, &new_home); if (r < 0) return r; @@ -193,7 +193,7 @@ int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home) { int home_resize_directory( UserRecord *h, bool already_activated, - char ***pkcs11_decrypted_passwords, + PasswordCache *cache, HomeSetup *setup, UserRecord **ret_home) { @@ -205,11 +205,11 @@ int home_resize_directory( assert(ret_home); assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)); - r = home_prepare(h, already_activated, pkcs11_decrypted_passwords, setup, NULL); + r = home_prepare(h, already_activated, cache, setup, NULL); if (r < 0) return r; - r = home_load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, pkcs11_decrypted_passwords, &embedded_home, &new_home); + r = home_load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home); if (r < 0) return r; diff --git a/src/home/homework-directory.h b/src/home/homework-directory.h index 047c3a70a0..717837f348 100644 --- a/src/home/homework-directory.h +++ b/src/home/homework-directory.h @@ -5,6 +5,6 @@ #include "user-record.h" int home_prepare_directory(UserRecord *h, bool already_activated, HomeSetup *setup); -int home_activate_directory(UserRecord *h, char ***pkcs11_decrypted_passwords, UserRecord **ret_home); +int home_activate_directory(UserRecord *h, PasswordCache *cache, UserRecord **ret_home); int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home); -int home_resize_directory(UserRecord *h, bool already_activated, char ***pkcs11_decrypted_passwords, HomeSetup *setup, UserRecord **ret_home); +int home_resize_directory(UserRecord *h, bool already_activated, PasswordCache *cache, HomeSetup *setup, UserRecord **ret_home); diff --git a/src/home/homework-fido2.c b/src/home/homework-fido2.c new file mode 100644 index 0000000000..36fe059ab3 --- /dev/null +++ b/src/home/homework-fido2.c @@ -0,0 +1,197 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "hexdecoct.h" +#include "homework-fido2.h" +#include "strv.h" + +static int fido2_use_specific_token( + const char *path, + UserRecord *h, + UserRecord *secret, + 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; + bool found_extension = false; + size_t n, hmac_size; + const void *hmac; + char **e; + int r; + + d = fido_dev_new(); + if (!d) + return log_oom(); + + r = 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)); + + if (!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(); + if (!di) + return log_oom(); + + r = 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)); + + e = fido_cbor_info_extensions_ptr(di); + n = 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 = fido_assert_new(); + if (!a) + return log_oom(); + + r = 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)); + + r = 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)); + + r = 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)); + + r = 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)); + + r = 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)); + + r = 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)); + + log_info("Asking FIDO2 token for authentication."); + + r = 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); + 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", fido_strerr(r)); + } + + hmac = 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); + + r = base64mem(hmac, hmac_size, ret); + if (r < 0) + return log_error_errno(r, "Failed to base64 encode HMAC secret: %m"); + + 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; + + di = fido_dev_info_new(allocated); + if (!di) + return log_oom(); + + r = 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)); + goto finish; + } + + for (size_t i = 0; i < found; i++) { + const fido_dev_info_t *entry; + const char *path; + + entry = 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); + 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: + fido_dev_info_free(&di, allocated); + return r; +} diff --git a/src/home/homework-fido2.h b/src/home/homework-fido2.h new file mode 100644 index 0000000000..d3b142a923 --- /dev/null +++ b/src/home/homework-fido2.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "user-record.h" + +int fido2_use_token(UserRecord *h, UserRecord *secret, const Fido2HmacSalt *salt, char **ret); diff --git a/src/home/homework-fscrypt.c b/src/home/homework-fscrypt.c index 696e265397..da9bb64b71 100644 --- a/src/home/homework-fscrypt.c +++ b/src/home/homework-fscrypt.c @@ -208,7 +208,7 @@ static int fscrypt_slot_try_many( } static int fscrypt_setup( - char **pkcs11_decrypted_passwords, + const PasswordCache *cache, char **password, HomeSetup *setup, void **ret_volume_key, @@ -230,6 +230,7 @@ static int fscrypt_setup( _cleanup_free_ char *value = NULL; size_t salt_size, encrypted_size; const char *nr, *e; + char **list; int n; /* Check if this xattr has the format 'trusted.fscrypt_slot' where '' is a 32bit unsigned integer */ @@ -256,19 +257,17 @@ static int fscrypt_setup( if (r < 0) return log_error_errno(r, "Failed to decode encrypted key of %s: %m", xa); - r = fscrypt_slot_try_many( - pkcs11_decrypted_passwords, - salt, salt_size, - encrypted, encrypted_size, - setup->fscrypt_key_descriptor, - ret_volume_key, ret_volume_key_size); - if (r == -ENOANO) + r = -ENOANO; + FOREACH_POINTER(list, cache->pkcs11_passwords, cache->fido2_passwords, password) { r = fscrypt_slot_try_many( - password, + list, salt, salt_size, encrypted, encrypted_size, setup->fscrypt_key_descriptor, ret_volume_key, ret_volume_key_size); + if (r != -ENOANO) + break; + } if (r < 0) { if (r != -ENOANO) return r; @@ -282,7 +281,7 @@ static int fscrypt_setup( int home_prepare_fscrypt( UserRecord *h, bool already_activated, - char ***pkcs11_decrypted_passwords, + PasswordCache *cache, HomeSetup *setup) { _cleanup_(erase_and_freep) void *volume_key = NULL; @@ -314,7 +313,7 @@ int home_prepare_fscrypt( memcpy(setup->fscrypt_key_descriptor, policy.master_key_descriptor, FS_KEY_DESCRIPTOR_SIZE); r = fscrypt_setup( - pkcs11_decrypted_passwords ? *pkcs11_decrypted_passwords : NULL, + cache, h->password, setup, &volume_key, @@ -584,7 +583,7 @@ int home_create_fscrypt( int home_passwd_fscrypt( UserRecord *h, HomeSetup *setup, - char **pkcs11_decrypted_passwords, /* the passwords acquired via PKCS#11 security tokens */ + PasswordCache *cache, /* the passwords acquired via PKCS#11/FIDO2 security tokens */ char **effective_passwords /* new passwords */) { _cleanup_(erase_and_freep) void *volume_key = NULL; @@ -600,7 +599,7 @@ int home_passwd_fscrypt( assert(setup); r = fscrypt_setup( - pkcs11_decrypted_passwords, + cache, h->password, setup, &volume_key, diff --git a/src/home/homework-fscrypt.h b/src/home/homework-fscrypt.h index aa3bcd3a69..e5cf7baaaa 100644 --- a/src/home/homework-fscrypt.h +++ b/src/home/homework-fscrypt.h @@ -4,7 +4,7 @@ #include "homework.h" #include "user-record.h" -int home_prepare_fscrypt(UserRecord *h, bool already_activated, char ***pkcs11_decrypted_passwords, HomeSetup *setup); +int home_prepare_fscrypt(UserRecord *h, bool already_activated, PasswordCache *cache, HomeSetup *setup); int home_create_fscrypt(UserRecord *h, char **effective_passwords, UserRecord **ret_home); -int home_passwd_fscrypt(UserRecord *h, HomeSetup *setup, char **pkcs11_decrypted_passwords, char **effective_passwords); +int home_passwd_fscrypt(UserRecord *h, HomeSetup *setup, PasswordCache *cache, char **effective_passwords); diff --git a/src/home/homework-luks.c b/src/home/homework-luks.c index 2a782e34bc..99cab0929e 100644 --- a/src/home/homework-luks.c +++ b/src/home/homework-luks.c @@ -216,7 +216,7 @@ static int luks_setup( const char *cipher_mode, uint64_t volume_key_size, char **passwords, - char **pkcs11_decrypted_passwords, + const PasswordCache *cache, bool discard, struct crypt_device **ret, sd_id128_t *ret_found_uuid, @@ -227,6 +227,7 @@ static int luks_setup( _cleanup_(erase_and_freep) void *vk = NULL; sd_id128_t p; size_t vks; + char **list; int r; assert(node); @@ -278,12 +279,14 @@ static int luks_setup( if (!vk) return log_oom(); - r = luks_try_passwords(cd, pkcs11_decrypted_passwords, vk, &vks); - if (r == -ENOKEY) { - r = luks_try_passwords(cd, passwords, vk, &vks); - if (r == -ENOKEY) - return log_error_errno(r, "No valid password for LUKS superblock."); + r = -ENOKEY; + FOREACH_POINTER(list, cache->pkcs11_passwords, cache->fido2_passwords, passwords) { + r = luks_try_passwords(cd, list, vk, &vks); + if (r != -ENOKEY) + break; } + if (r == -ENOKEY) + return log_error_errno(r, "No valid password for LUKS superblock."); if (r < 0) return log_error_errno(r, "Failed to unlocks LUKS superblock: %m"); @@ -312,7 +315,7 @@ static int luks_setup( static int luks_open( const char *dm_name, char **passwords, - char **pkcs11_decrypted_passwords, + PasswordCache *cache, struct crypt_device **ret, sd_id128_t *ret_found_uuid, void **ret_volume_key, @@ -321,6 +324,7 @@ static int luks_open( _cleanup_(crypt_freep) struct crypt_device *cd = NULL; _cleanup_(erase_and_freep) void *vk = NULL; sd_id128_t p; + char **list; size_t vks; int r; @@ -361,12 +365,14 @@ static int luks_open( if (!vk) return log_oom(); - r = luks_try_passwords(cd, pkcs11_decrypted_passwords, vk, &vks); - if (r == -ENOKEY) { - r = luks_try_passwords(cd, passwords, vk, &vks); - if (r == -ENOKEY) - return log_error_errno(r, "No valid password for LUKS superblock."); + r = -ENOKEY; + FOREACH_POINTER(list, cache->pkcs11_passwords, cache->fido2_passwords, passwords) { + r = luks_try_passwords(cd, list, vk, &vks); + if (r != -ENOKEY) + break; } + if (r == -ENOKEY) + return log_error_errno(r, "No valid password for LUKS superblock."); if (r < 0) return log_error_errno(r, "Failed to unlocks LUKS superblock: %m"); @@ -622,7 +628,7 @@ static int luks_validate_home_record( struct crypt_device *cd, UserRecord *h, const void *volume_key, - char ***pkcs11_decrypted_passwords, + PasswordCache *cache, UserRecord **ret_luks_home_record) { int r, token; @@ -727,7 +733,7 @@ static int luks_validate_home_record( if (!user_record_compatible(h, lhr)) return log_error_errno(SYNTHETIC_ERRNO(EREMCHG), "LUKS home record not compatible with host record, refusing."); - r = user_record_authenticate(lhr, h, pkcs11_decrypted_passwords, /* strict_verify= */ true); + r = user_record_authenticate(lhr, h, cache, /* strict_verify= */ true); if (r < 0) return r; assert(r > 0); /* Insist that a password was verified */ @@ -982,7 +988,7 @@ int home_prepare_luks( UserRecord *h, bool already_activated, const char *force_image_path, - char ***pkcs11_decrypted_passwords, + PasswordCache *cache, HomeSetup *setup, UserRecord **ret_luks_home) { @@ -1010,7 +1016,7 @@ int home_prepare_luks( r = luks_open(setup->dm_name, h->password, - pkcs11_decrypted_passwords ? *pkcs11_decrypted_passwords : NULL, + cache, &cd, &found_luks_uuid, &volume_key, @@ -1018,7 +1024,7 @@ int home_prepare_luks( if (r < 0) return r; - r = luks_validate_home_record(cd, h, volume_key, pkcs11_decrypted_passwords, &luks_home); + r = luks_validate_home_record(cd, h, volume_key, cache, &luks_home); if (r < 0) return r; @@ -1133,7 +1139,7 @@ int home_prepare_luks( h->luks_cipher_mode, h->luks_volume_key_size, h->password, - pkcs11_decrypted_passwords ? *pkcs11_decrypted_passwords : NULL, + cache, user_record_luks_discard(h) || user_record_luks_offline_discard(h), &cd, &found_luks_uuid, @@ -1144,7 +1150,7 @@ int home_prepare_luks( dm_activated = true; - r = luks_validate_home_record(cd, h, volume_key, pkcs11_decrypted_passwords, &luks_home); + r = luks_validate_home_record(cd, h, volume_key, cache, &luks_home); if (r < 0) goto fail; @@ -1218,7 +1224,7 @@ static void print_size_summary(uint64_t host_size, uint64_t encrypted_size, stru int home_activate_luks( UserRecord *h, - char ***pkcs11_decrypted_passwords, + PasswordCache *cache, UserRecord **ret_home) { _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *luks_home_record = NULL; @@ -1250,7 +1256,7 @@ int home_activate_luks( h, false, NULL, - pkcs11_decrypted_passwords, + cache, &setup, &luks_home_record); if (r < 0) @@ -1268,7 +1274,7 @@ int home_activate_luks( h, &setup, luks_home_record, - pkcs11_decrypted_passwords, + cache, &sfs, &new_home); if (r < 0) @@ -1464,7 +1470,7 @@ static int luks_format( const char *dm_name, sd_id128_t uuid, const char *label, - char **pkcs11_decrypted_passwords, + const PasswordCache *cache, char **effective_passwords, bool discard, UserRecord *hr, @@ -1533,7 +1539,8 @@ static int luks_format( STRV_FOREACH(pp, effective_passwords) { - if (strv_contains(pkcs11_decrypted_passwords, *pp)) { + if (strv_contains(cache->pkcs11_passwords, *pp) || + strv_contains(cache->fido2_passwords, *pp)) { log_debug("Using minimal PBKDF for slot %i", slot); r = crypt_set_pbkdf_type(cd, &minimal_pbkdf); } else { @@ -1858,7 +1865,7 @@ static int home_truncate( int home_create_luks( UserRecord *h, - char **pkcs11_decrypted_passwords, + PasswordCache *cache, char **effective_passwords, UserRecord **ret_home) { @@ -2055,7 +2062,7 @@ int home_create_luks( dm_name, luks_uuid, user_record_user_name_and_realm(h), - pkcs11_decrypted_passwords, + cache, effective_passwords, user_record_luks_discard(h) || user_record_luks_offline_discard(h), h, @@ -2561,7 +2568,7 @@ static int apply_resize_partition(int fd, sd_id128_t disk_uuids, struct fdisk_ta int home_resize_luks( UserRecord *h, bool already_activated, - char ***pkcs11_decrypted_passwords, + PasswordCache *cache, HomeSetup *setup, UserRecord **ret_home) { @@ -2647,11 +2654,11 @@ int home_resize_luks( } } - r = home_prepare_luks(h, already_activated, whole_disk, pkcs11_decrypted_passwords, setup, &header_home); + r = home_prepare_luks(h, already_activated, whole_disk, cache, setup, &header_home); if (r < 0) return r; - r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, pkcs11_decrypted_passwords, &embedded_home, &new_home); + r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home); if (r < 0) return r; @@ -2855,13 +2862,14 @@ int home_resize_luks( int home_passwd_luks( UserRecord *h, HomeSetup *setup, - char **pkcs11_decrypted_passwords, /* the passwords acquired via PKCS#11 security tokens */ - char **effective_passwords /* new passwords */) { + PasswordCache *cache, /* the passwords acquired via PKCS#11/FIDO2 security tokens */ + char **effective_passwords /* new passwords */) { size_t volume_key_size, i, max_key_slots, n_effective; _cleanup_(erase_and_freep) void *volume_key = NULL; struct crypt_pbkdf_type good_pbkdf, minimal_pbkdf; const char *type; + char **list; int r; assert(h); @@ -2886,12 +2894,14 @@ int home_passwd_luks( if (!volume_key) return log_oom(); - r = luks_try_passwords(setup->crypt_device, pkcs11_decrypted_passwords, volume_key, &volume_key_size); - if (r == -ENOKEY) { - r = luks_try_passwords(setup->crypt_device, h->password, volume_key, &volume_key_size); - if (r == -ENOKEY) - return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to unlock LUKS superblock with supplied passwords."); + r = -ENOKEY; + FOREACH_POINTER(list, cache->pkcs11_passwords, cache->fido2_passwords, h->password) { + r = luks_try_passwords(setup->crypt_device, list, volume_key, &volume_key_size); + if (r != -ENOKEY) + break; } + if (r == -ENOKEY) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to unlock LUKS superblock with supplied passwords."); if (r < 0) return log_error_errno(r, "Failed to unlocks LUKS superblock: %m"); @@ -2911,7 +2921,8 @@ int home_passwd_luks( continue; } - if (strv_find(pkcs11_decrypted_passwords, effective_passwords[i])) { + if (strv_contains(cache->pkcs11_passwords, effective_passwords[i]) || + strv_contains(cache->fido2_passwords, effective_passwords[i])) { log_debug("Using minimal PBKDF for slot %zu", i); r = crypt_set_pbkdf_type(setup->crypt_device, &minimal_pbkdf); } else { @@ -3008,9 +3019,10 @@ static int luks_try_resume( return -ENOKEY; } -int home_unlock_luks(UserRecord *h, char ***pkcs11_decrypted_passwords) { +int home_unlock_luks(UserRecord *h, PasswordCache *cache) { _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + char **list; int r; assert(h); @@ -3026,12 +3038,14 @@ int home_unlock_luks(UserRecord *h, char ***pkcs11_decrypted_passwords) { log_info("Discovered used LUKS device %s.", dm_node); crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); - r = luks_try_resume(cd, dm_name, pkcs11_decrypted_passwords ? *pkcs11_decrypted_passwords : NULL); - if (r == -ENOKEY) { - r = luks_try_resume(cd, dm_name, h->password); - if (r == -ENOKEY) - return log_error_errno(r, "No valid password for LUKS superblock."); + r = -ENOKEY; + FOREACH_POINTER(list, cache->pkcs11_passwords, cache->fido2_passwords, h->password) { + r = luks_try_resume(cd, dm_name, list); + if (r != -ENOKEY) + break; } + if (r == -ENOKEY) + return log_error_errno(r, "No valid password for LUKS superblock."); if (r < 0) return log_error_errno(r, "Failed to resume LUKS superblock: %m"); diff --git a/src/home/homework-luks.h b/src/home/homework-luks.h index bd51f5da50..b51f1ad7a0 100644 --- a/src/home/homework-luks.h +++ b/src/home/homework-luks.h @@ -5,24 +5,24 @@ #include "homework.h" #include "user-record.h" -int home_prepare_luks(UserRecord *h, bool already_activated, const char *force_image_path, char ***pkcs11_decrypted_passwords, HomeSetup *setup, UserRecord **ret_luks_home); +int home_prepare_luks(UserRecord *h, bool already_activated, const char *force_image_path, PasswordCache *cache, HomeSetup *setup, UserRecord **ret_luks_home); -int home_activate_luks(UserRecord *h, char ***pkcs11_decrypted_passwords, UserRecord **ret_home); +int home_activate_luks(UserRecord *h, PasswordCache *cache, UserRecord **ret_home); int home_deactivate_luks(UserRecord *h); int home_trim_luks(UserRecord *h); int home_store_header_identity_luks(UserRecord *h, HomeSetup *setup, UserRecord *old_home); -int home_create_luks(UserRecord *h, char **pkcs11_decrypted_passwords, char **effective_passwords, UserRecord **ret_home); +int home_create_luks(UserRecord *h, PasswordCache *cache, char **effective_passwords, UserRecord **ret_home); int home_validate_update_luks(UserRecord *h, HomeSetup *setup); -int home_resize_luks(UserRecord *h, bool already_activated, char ***pkcs11_decrypted_passwords, HomeSetup *setup, UserRecord **ret_home); +int home_resize_luks(UserRecord *h, bool already_activated, PasswordCache *cache, HomeSetup *setup, UserRecord **ret_home); -int home_passwd_luks(UserRecord *h, HomeSetup *setup, char **pkcs11_decrypted_passwords, char **effective_passwords); +int home_passwd_luks(UserRecord *h, HomeSetup *setup, PasswordCache *cache, char **effective_passwords); int home_lock_luks(UserRecord *h); -int home_unlock_luks(UserRecord *h, char ***pkcs11_decrypted_passwords); +int home_unlock_luks(UserRecord *h, PasswordCache *cache); static inline uint64_t luks_volume_key_size_convert(struct crypt_device *cd) { int k; diff --git a/src/home/homework.c b/src/home/homework.c index 316933cf4e..83bd875d2d 100644 --- a/src/home/homework.c +++ b/src/home/homework.c @@ -11,6 +11,7 @@ #include "home-util.h" #include "homework-cifs.h" #include "homework-directory.h" +#include "homework-fido2.h" #include "homework-fscrypt.h" #include "homework-luks.h" #include "homework-mount.h" @@ -21,7 +22,6 @@ #include "missing_magic.h" #include "mount-util.h" #include "path-util.h" -#include "pkcs11-util.h" #include "rm-rf.h" #include "stat-util.h" #include "strv.h" @@ -32,14 +32,22 @@ /* Make sure a bad password always results in a 3s delay, no matter what */ #define BAD_PASSWORD_DELAY_USEC (3 * USEC_PER_SEC) +void password_cache_free(PasswordCache *cache) { + if (!cache) + return; + + cache->pkcs11_passwords = strv_free_erase(cache->pkcs11_passwords); + cache->fido2_passwords = strv_free_erase(cache->fido2_passwords); +} + int user_record_authenticate( UserRecord *h, UserRecord *secret, - char ***pkcs11_decrypted_passwords, + PasswordCache *cache, bool strict_verify) { - bool need_password = false, need_token = false, need_pin = false, need_protected_authentication_path_permitted = false, - pin_locked = false, pin_incorrect = false, pin_incorrect_few_tries_left = false, pin_incorrect_one_try_left = false; + bool need_password = false, need_token = false, need_pin = false, need_protected_authentication_path_permitted = false, need_user_presence_permitted = false, + pin_locked = false, pin_incorrect = false, pin_incorrect_few_tries_left = false, pin_incorrect_one_try_left = false, token_action_timeout = false; int r; assert(h); @@ -47,14 +55,14 @@ int user_record_authenticate( /* Tries to authenticate a user record with the supplied secrets. i.e. checks whether at least one * supplied plaintext passwords matches a hashed password field of the user record. Or if a - * configured PKCS#11 token is around and can unlock the record. + * configured PKCS#11 or FIDO2 token is around and can unlock the record. * - * Note that the pkcs11_decrypted_passwords parameter is both an input and and output parameter: it - * is a list of configured, decrypted PKCS#11 passwords. We typically have to call this function - * multiple times over the course of an operation (think: on login we authenticate the host user - * record, the record embedded in the LUKS record and the one embedded in $HOME). Hence we keep a - * list of passwords we already decrypted, so that we don't have to do the (slow an potentially - * interactive) PKCS#11 dance for the relevant token again and again. */ + * Note that the 'cache' parameter is both an input and output parameter: it contains lists of + * configured, decrypted PKCS#11/FIDO2 passwords. We typically have to call this function multiple + * times over the course of an operation (think: on login we authenticate the host user record, the + * record embedded in the LUKS record and the one embedded in $HOME). Hence we keep a list of + * passwords we already decrypted, so that we don't have to do the (slow and potentially interactive) + * PKCS#11/FIDO2 dance for the relevant token again and again. */ /* First, let's see if the supplied plain-text passwords work? */ r = user_record_test_secret(h, secret); @@ -70,19 +78,12 @@ int user_record_authenticate( return 1; } - /* Second, let's see if any of the PKCS#11 security tokens are plugged in and help us */ + /* Second, test cached PKCS#11 passwords */ for (size_t n = 0; n < h->n_pkcs11_encrypted_key; n++) { -#if HAVE_P11KIT - _cleanup_(pkcs11_callback_data_release) struct pkcs11_callback_data data = { - .user_record = h, - .secret = secret, - .encrypted_key = h->pkcs11_encrypted_key + n, - }; char **pp; - /* See if any of the previously calculated passwords work */ - STRV_FOREACH(pp, *pkcs11_decrypted_passwords) { - r = test_password_one(data.encrypted_key->hashed_password, *pp); + STRV_FOREACH(pp, cache->pkcs11_passwords) { + r = test_password_one(h->pkcs11_encrypted_key[n].hashed_password, *pp); if (r < 0) return log_error_errno(r, "Failed to check supplied PKCS#11 password: %m"); if (r > 0) { @@ -90,6 +91,32 @@ int user_record_authenticate( return 1; } } + } + + /* Third, test cached FIDO2 passwords */ + for (size_t n = 0; n < h->n_fido2_hmac_salt; n++) { + char **pp; + + /* See if any of the previously calculated passwords work */ + STRV_FOREACH(pp, cache->fido2_passwords) { + r = test_password_one(h->fido2_hmac_salt[n].hashed_password, *pp); + if (r < 0) + return log_error_errno(r, "Failed to check supplied FIDO2 password: %m"); + if (r > 0) { + log_info("Previously acquired FIDO2 password unlocks user record."); + return 0; + } + } + } + + /* Fourth, let's see if any of the PKCS#11 security tokens are plugged in and help us */ + for (size_t n = 0; n < h->n_pkcs11_encrypted_key; n++) { +#if HAVE_P11KIT + _cleanup_(pkcs11_callback_data_release) struct pkcs11_callback_data data = { + .user_record = h, + .secret = secret, + .encrypted_key = h->pkcs11_encrypted_key + n, + }; r = pkcs11_find_token(data.encrypted_key->uri, pkcs11_callback, &data); switch (r) { @@ -126,7 +153,56 @@ int user_record_authenticate( log_info("Decrypted password from PKCS#11 security token %s unlocks user record.", data.encrypted_key->uri); - r = strv_extend(pkcs11_decrypted_passwords, data.decrypted_password); + r = strv_extend(&cache->pkcs11_passwords, data.decrypted_password); + if (r < 0) + return log_oom(); + + return 0; + } +#else + need_token = true; + break; +#endif + } + + /* Fifth, let's see if any of the FIDO2 security tokens are plugged in and help us */ + for (size_t n = 0; n < h->n_fido2_hmac_salt; n++) { +#if HAVE_LIBFIDO2 + _cleanup_(erase_and_freep) char *decrypted_password = NULL; + + r = fido2_use_token(h, secret, h->fido2_hmac_salt + n, &decrypted_password); + switch (r) { + case -EAGAIN: + need_token = true; + break; + case -ENOANO: + need_pin = true; + break; + case -EOWNERDEAD: + pin_locked = true; + break; + case -ENOLCK: + pin_incorrect = true; + break; + case -EMEDIUMTYPE: + need_user_presence_permitted = true; + break; + case -ENOSTR: + token_action_timeout = true; + break; + default: + if (r < 0) + return r; + + r = test_password_one(h->fido2_hmac_salt[n].hashed_password, decrypted_password); + if (r < 0) + return log_error_errno(r, "Failed to test FIDO2 password: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Configured FIDO2 security token does not decrypt encrypted key correctly."); + + log_info("Decrypted password from FIDO2 security token unlocks user record."); + + r = strv_extend(&cache->fido2_passwords, decrypted_password); if (r < 0) return log_oom(); @@ -147,8 +223,12 @@ int user_record_authenticate( return -ENOLCK; if (pin_locked) return -EOWNERDEAD; + if (token_action_timeout) + return -ENOSTR; if (need_protected_authentication_path_permitted) return -ERFKILL; + if (need_user_presence_permitted) + return -EMEDIUMTYPE; if (need_pin) return -ENOANO; if (need_token) @@ -156,10 +236,11 @@ int user_record_authenticate( if (need_password) return -ENOKEY; - /* Hmm, this means neither PCKS#11 nor classic hashed passwords were supplied, we cannot authenticate this reasonably */ + /* Hmm, this means neither PCKS#11/FIDO2 nor classic hashed passwords were supplied, we cannot + * authenticate this reasonably */ if (strict_verify) return log_debug_errno(SYNTHETIC_ERRNO(EKEYREVOKED), - "No hashed passwords and no PKCS#11 tokens defined, cannot authenticate user record, refusing."); + "No hashed passwords and no PKCS#11/FIDO2 tokens defined, cannot authenticate user record, refusing."); /* If strict verification is off this means we are possibly in the case where we encountered an * unfixated record, i.e. a synthetic one that accordingly lacks any authentication data. In this @@ -230,7 +311,7 @@ int home_setup_undo(HomeSetup *setup) { int home_prepare( UserRecord *h, bool already_activated, - char ***pkcs11_decrypted_passwords, + PasswordCache *cache, HomeSetup *setup, UserRecord **ret_header_home) { @@ -249,7 +330,7 @@ int home_prepare( switch (user_record_storage(h)) { case USER_LUKS: - return home_prepare_luks(h, already_activated, NULL, pkcs11_decrypted_passwords, setup, ret_header_home); + return home_prepare_luks(h, already_activated, NULL, cache, setup, ret_header_home); case USER_SUBVOLUME: case USER_DIRECTORY: @@ -257,7 +338,7 @@ int home_prepare( break; case USER_FSCRYPT: - r = home_prepare_fscrypt(h, already_activated, pkcs11_decrypted_passwords, setup); + r = home_prepare_fscrypt(h, already_activated, cache, setup); break; case USER_CIFS: @@ -387,7 +468,7 @@ int home_load_embedded_identity( int root_fd, UserRecord *header_home, UserReconcileMode mode, - char ***pkcs11_decrypted_passwords, + PasswordCache *cache, UserRecord **ret_embedded_home, UserRecord **ret_new_home) { @@ -414,7 +495,7 @@ int home_load_embedded_identity( return log_error_errno(SYNTHETIC_ERRNO(EREMCHG), "Embedded home record not compatible with host record, refusing."); /* Insist that credentials the user supplies also unlocks any embedded records. */ - r = user_record_authenticate(embedded_home, h, pkcs11_decrypted_passwords, /* strict_verify= */ true); + r = user_record_authenticate(embedded_home, h, cache, /* strict_verify= */ true); if (r < 0) return r; assert(r > 0); /* Insist that a password was verified */ @@ -576,7 +657,7 @@ int home_refresh( UserRecord *h, HomeSetup *setup, UserRecord *header_home, - char ***pkcs11_decrypted_passwords, + PasswordCache *cache, struct statfs *ret_statfs, UserRecord **ret_new_home) { @@ -590,7 +671,7 @@ int home_refresh( /* When activating a home directory, does the identity work: loads the identity from the $HOME * directory, reconciles it with our idea, chown()s everything. */ - r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_ANY, pkcs11_decrypted_passwords, &embedded_home, &new_home); + r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_ANY, cache, &embedded_home, &new_home); if (r < 0) return r; @@ -615,7 +696,7 @@ int home_refresh( } static int home_activate(UserRecord *h, UserRecord **ret_home) { - _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL; + _cleanup_(password_cache_free) PasswordCache cache = {}; _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; int r; @@ -628,7 +709,7 @@ static int home_activate(UserRecord *h, UserRecord **ret_home) { if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Activating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); - r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords, /* strict_verify= */ false); + r = user_record_authenticate(h, h, &cache, /* strict_verify= */ false); if (r < 0) return r; @@ -647,7 +728,7 @@ static int home_activate(UserRecord *h, UserRecord **ret_home) { switch (user_record_storage(h)) { case USER_LUKS: - r = home_activate_luks(h, &pkcs11_decrypted_passwords, &new_home); + r = home_activate_luks(h, &cache, &new_home); if (r < 0) return r; @@ -656,14 +737,14 @@ static int home_activate(UserRecord *h, UserRecord **ret_home) { case USER_SUBVOLUME: case USER_DIRECTORY: case USER_FSCRYPT: - r = home_activate_directory(h, &pkcs11_decrypted_passwords, &new_home); + r = home_activate_directory(h, &cache, &new_home); if (r < 0) return r; break; case USER_CIFS: - r = home_activate_cifs(h, &pkcs11_decrypted_passwords, &new_home); + r = home_activate_cifs(h, &cache, &new_home); if (r < 0) return r; @@ -783,15 +864,16 @@ int home_populate(UserRecord *h, int dir_fd) { static int user_record_compile_effective_passwords( UserRecord *h, - char ***ret_effective_passwords, - char ***ret_pkcs11_decrypted_passwords) { + PasswordCache *cache, + char ***ret_effective_passwords) { - _cleanup_(strv_free_erasep) char **effective = NULL, **pkcs11_passwords = NULL; + _cleanup_(strv_free_erasep) char **effective = NULL; size_t n; char **i; int r; assert(h); + assert(cache); /* We insist on at least one classic hashed password to be defined in addition to any PKCS#11 one, as * a safe fallback, but also to simplify the password changing algorithm: there we require providing @@ -858,11 +940,37 @@ static int user_record_compile_effective_passwords( return log_oom(); } - if (ret_pkcs11_decrypted_passwords) { - r = strv_extend(&pkcs11_passwords, data.decrypted_password); + r = strv_extend(&cache->pkcs11_passwords, data.decrypted_password); + if (r < 0) + return log_oom(); +#else + return -EBADSLT; +#endif + } + + for (n = 0; n < h->n_fido2_hmac_salt; n++) { +#if HAVE_LIBFIDO2 + _cleanup_(erase_and_freep) char *decrypted_password = NULL; + + r = fido2_use_token(h, h, h->fido2_hmac_salt + n, &decrypted_password); + if (r < 0) + return r; + + r = test_password_one(h->fido2_hmac_salt[n].hashed_password, decrypted_password); + if (r < 0) + return log_error_errno(r, "Failed to test FIDO2 password: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Decrypted password from token is not correct, refusing."); + + if (ret_effective_passwords) { + r = strv_extend(&effective, decrypted_password); if (r < 0) return log_oom(); } + + r = strv_extend(&cache->fido2_passwords, decrypted_password); + if (r < 0) + return log_oom(); #else return -EBADSLT; #endif @@ -870,8 +978,6 @@ static int user_record_compile_effective_passwords( if (ret_effective_passwords) *ret_effective_passwords = TAKE_PTR(effective); - if (ret_pkcs11_decrypted_passwords) - *ret_pkcs11_decrypted_passwords = TAKE_PTR(pkcs11_passwords); return 0; } @@ -934,8 +1040,9 @@ static int determine_default_storage(UserStorage *ret) { } static int home_create(UserRecord *h, UserRecord **ret_home) { - _cleanup_(strv_free_erasep) char **effective_passwords = NULL, **pkcs11_decrypted_passwords = NULL; + _cleanup_(strv_free_erasep) char **effective_passwords = NULL; _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + _cleanup_(password_cache_free) PasswordCache cache = {}; UserStorage new_storage = _USER_STORAGE_INVALID; const char *new_fs = NULL; int r; @@ -947,7 +1054,7 @@ static int home_create(UserRecord *h, UserRecord **ret_home) { if (!uid_is_valid(h->uid)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing."); - r = user_record_compile_effective_passwords(h, &effective_passwords, &pkcs11_decrypted_passwords); + r = user_record_compile_effective_passwords(h, &cache, &effective_passwords); if (r < 0) return r; @@ -996,7 +1103,7 @@ static int home_create(UserRecord *h, UserRecord **ret_home) { switch (user_record_storage(h)) { case USER_LUKS: - r = home_create_luks(h, pkcs11_decrypted_passwords, effective_passwords, &new_home); + r = home_create_luks(h, &cache, effective_passwords, &new_home); break; case USER_DIRECTORY: @@ -1182,15 +1289,15 @@ static int home_validate_update(UserRecord *h, HomeSetup *setup) { static int home_update(UserRecord *h, UserRecord **ret) { _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL, *embedded_home = NULL; - _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL; _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_(password_cache_free) PasswordCache cache = {}; bool already_activated = false; int r; assert(h); assert(ret); - r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords, /* strict_verify= */ true); + r = user_record_authenticate(h, h, &cache, /* strict_verify= */ true); if (r < 0) return r; assert(r > 0); /* Insist that a password was verified */ @@ -1201,11 +1308,11 @@ static int home_update(UserRecord *h, UserRecord **ret) { already_activated = r > 0; - r = home_prepare(h, already_activated, &pkcs11_decrypted_passwords, &setup, &header_home); + r = home_prepare(h, already_activated, &cache, &setup, &header_home); if (r < 0) return r; - r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER, &pkcs11_decrypted_passwords, &embedded_home, &new_home); + r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER, &cache, &embedded_home, &new_home); if (r < 0) return r; @@ -1237,7 +1344,7 @@ static int home_update(UserRecord *h, UserRecord **ret) { static int home_resize(UserRecord *h, UserRecord **ret) { _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; - _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL; + _cleanup_(password_cache_free) PasswordCache cache = {}; bool already_activated = false; int r; @@ -1247,7 +1354,7 @@ static int home_resize(UserRecord *h, UserRecord **ret) { if (h->disk_size == UINT64_MAX) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No target size specified, refusing."); - r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords, /* strict_verify= */ true); + r = user_record_authenticate(h, h, &cache, /* strict_verify= */ true); if (r < 0) return r; assert(r > 0); /* Insist that a password was verified */ @@ -1261,12 +1368,12 @@ static int home_resize(UserRecord *h, UserRecord **ret) { switch (user_record_storage(h)) { case USER_LUKS: - return home_resize_luks(h, already_activated, &pkcs11_decrypted_passwords, &setup, ret); + return home_resize_luks(h, already_activated, &cache, &setup, ret); case USER_DIRECTORY: case USER_SUBVOLUME: case USER_FSCRYPT: - return home_resize_directory(h, already_activated, &pkcs11_decrypted_passwords, &setup, ret); + return home_resize_directory(h, already_activated, &cache, &setup, ret); default: return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Resizing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); @@ -1275,8 +1382,9 @@ static int home_resize(UserRecord *h, UserRecord **ret) { static int home_passwd(UserRecord *h, UserRecord **ret_home) { _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL; - _cleanup_(strv_free_erasep) char **effective_passwords = NULL, **pkcs11_decrypted_passwords = NULL; + _cleanup_(strv_free_erasep) char **effective_passwords = NULL; _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_(password_cache_free) PasswordCache cache = {}; bool already_activated = false; int r; @@ -1286,7 +1394,7 @@ static int home_passwd(UserRecord *h, UserRecord **ret_home) { if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)) return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Changing password of home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); - r = user_record_compile_effective_passwords(h, &effective_passwords, &pkcs11_decrypted_passwords); + r = user_record_compile_effective_passwords(h, &cache, &effective_passwords); if (r < 0) return r; @@ -1296,24 +1404,24 @@ static int home_passwd(UserRecord *h, UserRecord **ret_home) { already_activated = r > 0; - r = home_prepare(h, already_activated, &pkcs11_decrypted_passwords, &setup, &header_home); + r = home_prepare(h, already_activated, &cache, &setup, &header_home); if (r < 0) return r; - r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &pkcs11_decrypted_passwords, &embedded_home, &new_home); + r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &cache, &embedded_home, &new_home); if (r < 0) return r; switch (user_record_storage(h)) { case USER_LUKS: - r = home_passwd_luks(h, &setup, pkcs11_decrypted_passwords, effective_passwords); + r = home_passwd_luks(h, &setup, &cache, effective_passwords); if (r < 0) return r; break; case USER_FSCRYPT: - r = home_passwd_fscrypt(h, &setup, pkcs11_decrypted_passwords, effective_passwords); + r = home_passwd_fscrypt(h, &setup, &cache, effective_passwords); if (r < 0) return r; break; @@ -1351,14 +1459,14 @@ static int home_passwd(UserRecord *h, UserRecord **ret_home) { static int home_inspect(UserRecord *h, UserRecord **ret_home) { _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *new_home = NULL; _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; - _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL; + _cleanup_(password_cache_free) PasswordCache cache = {}; bool already_activated = false; int r; assert(h); assert(ret_home); - r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords, /* strict_verify= */ false); + r = user_record_authenticate(h, h, &cache, /* strict_verify= */ false); if (r < 0) return r; @@ -1368,11 +1476,11 @@ static int home_inspect(UserRecord *h, UserRecord **ret_home) { already_activated = r > 0; - r = home_prepare(h, already_activated, &pkcs11_decrypted_passwords, &setup, &header_home); + r = home_prepare(h, already_activated, &cache, &setup, &header_home); if (r < 0) return r; - r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_ANY, &pkcs11_decrypted_passwords, NULL, &new_home); + r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_ANY, &cache, NULL, &new_home); if (r < 0) return r; @@ -1415,7 +1523,7 @@ static int home_lock(UserRecord *h) { } static int home_unlock(UserRecord *h) { - _cleanup_(strv_free_erasep) char **pkcs11_decrypted_passwords = NULL; + _cleanup_(password_cache_free) PasswordCache cache = {}; int r; assert(h); @@ -1428,11 +1536,11 @@ static int home_unlock(UserRecord *h) { /* Note that we don't check if $HOME is actually mounted, since we want to avoid disk accesses on * that mount until we have resumed the device. */ - r = user_record_authenticate(h, h, &pkcs11_decrypted_passwords, /* strict_verify= */ false); + r = user_record_authenticate(h, h, &cache, /* strict_verify= */ false); if (r < 0) return r; - r = home_unlock_luks(h, &pkcs11_decrypted_passwords); + r = home_unlock_luks(h, &cache); if (r < 0) return r; @@ -1495,10 +1603,12 @@ static int run(int argc, char *argv[]) { * ESOCKTNOSUPPORT → operation not support on this file system * ENOKEY → password incorrect (or not sufficient, or not supplied) * EBADSLT → similar, but PKCS#11 device is defined and might be able to provide password, if it was plugged in which it is not - * ENOANO → suitable PKCS#11 device found, but PIN is missing to unlock it + * ENOANO → suitable PKCS#11/FIDO2 device found, but PIN is missing to unlock it * ERFKILL → suitable PKCS#11 device found, but OK to ask for on-device interactive authentication not given - * EOWNERDEAD → suitable PKCS#11 device found, but its PIN is locked - * ENOLCK → suitable PKCS#11 device found, but PIN incorrect + * EMEDIUMTYPE → suitable FIDO2 device found, but OK to ask for user presence not given + * ENOSTR → suitable FIDO2 device found, but user didn't react to action request on token quickly enough + * EOWNERDEAD → suitable PKCS#11/FIDO2 device found, but its PIN is locked + * ENOLCK → suitable PKCS#11/FIDO2 device found, but PIN incorrect * ETOOMANYREFS → suitable PKCS#11 device found, but PIN incorrect, and only few tries left * EUCLEAN → suitable PKCS#11 device found, but PIN incorrect, and only one try left * EBUSY → file system is currently active diff --git a/src/home/homework.h b/src/home/homework.h index 46641172a4..ce8f2a461f 100644 --- a/src/home/homework.h +++ b/src/home/homework.h @@ -36,6 +36,14 @@ typedef struct HomeSetup { uint64_t partition_size; } HomeSetup; +typedef struct PasswordCache { + /* Decoding passwords from security tokens is expensive and typically requires user interaction, hence cache any we already figured out. */ + char **pkcs11_passwords; + char **fido2_passwords; +} PasswordCache; + +void password_cache_free(PasswordCache *cache); + #define HOME_SETUP_INIT \ { \ .root_fd = -1, \ @@ -46,16 +54,16 @@ typedef struct HomeSetup { int home_setup_undo(HomeSetup *setup); -int home_prepare(UserRecord *h, bool already_activated, char ***pkcs11_decrypted_passwords, HomeSetup *setup, UserRecord **ret_header_home); +int home_prepare(UserRecord *h, bool already_activated, PasswordCache *cache, HomeSetup *setup, UserRecord **ret_header_home); -int home_refresh(UserRecord *h, HomeSetup *setup, UserRecord *header_home, char ***pkcs11_decrypted_passwords, struct statfs *ret_statfs, UserRecord **ret_new_home); +int home_refresh(UserRecord *h, HomeSetup *setup, UserRecord *header_home, PasswordCache *cache, struct statfs *ret_statfs, UserRecord **ret_new_home); int home_populate(UserRecord *h, int dir_fd); -int home_load_embedded_identity(UserRecord *h, int root_fd, UserRecord *header_home, UserReconcileMode mode, char ***pkcs11_decrypted_passwords, UserRecord **ret_embedded_home, UserRecord **ret_new_home); +int home_load_embedded_identity(UserRecord *h, int root_fd, UserRecord *header_home, UserReconcileMode mode, PasswordCache *cache, UserRecord **ret_embedded_home, UserRecord **ret_new_home); int home_store_embedded_identity(UserRecord *h, int root_fd, uid_t uid, UserRecord *old_home); int home_extend_embedded_identity(UserRecord *h, UserRecord *used, HomeSetup *setup); -int user_record_authenticate(UserRecord *h, UserRecord *secret, char ***pkcs11_decrypted_passwords, bool strict_verify); +int user_record_authenticate(UserRecord *h, UserRecord *secret, PasswordCache *cache, bool strict_verify); int home_sync_and_statfs(int root_fd, struct statfs *ret); diff --git a/src/home/meson.build b/src/home/meson.build index 3fa6bc08c9..7dee08408a 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -14,6 +14,7 @@ systemd_homework_sources = files(''' homework-mount.c homework-mount.h homework-pkcs11.h + homework-fido2.h homework-quota.c homework-quota.h homework.c @@ -25,6 +26,9 @@ systemd_homework_sources = files(''' if conf.get('HAVE_P11KIT') == 1 systemd_homework_sources += files('homework-pkcs11.c') endif +if conf.get('HAVE_LIBFIDO2') == 1 + systemd_homework_sources += files('homework-fido2.c') +endif systemd_homed_sources = files(''' home-util.c diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c index 80797b4dd5..87bf1620d5 100644 --- a/src/home/pam_systemd_home.c +++ b/src/home/pam_systemd_home.c @@ -375,6 +375,16 @@ static int handle_generic_user_record_error( return PAM_SERVICE_ERR; } + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_USER_PRESENCE_NEEDED)) { + + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Please verify presence on security token of user %s.", user_name); + + r = user_record_set_fido2_user_presence_permitted(secret, true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to set FIDO2 user presence permitted flag: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN)) { _cleanup_(erase_and_freep) char *newp = NULL; diff --git a/src/home/user-record-util.c b/src/home/user-record-util.c index f58f9e0709..cf8e988521 100644 --- a/src/home/user-record-util.c +++ b/src/home/user-record-util.c @@ -980,6 +980,34 @@ int user_record_set_pkcs11_protected_authentication_path_permitted(UserRecord *h return 0; } +int user_record_set_fido2_user_presence_permitted(UserRecord *h, int b) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + int r; + + assert(h); + + w = json_variant_ref(json_variant_by_key(h->json, "secret")); + + if (b < 0) + r = json_variant_filter(&w, STRV_MAKE("fido2UserPresencePermitted")); + else + r = json_variant_set_field_boolean(&w, "fido2UserPresencePermitted", b); + if (r < 0) + return r; + + if (json_variant_is_blank_object(w)) + r = json_variant_filter(&h->json, STRV_MAKE("secret")); + else + r = json_variant_set_field(&h->json, "secret", w); + if (r < 0) + return r; + + h->fido2_user_presence_permitted = b; + + SET_FLAG(h->mask, USER_RECORD_SECRET, !json_variant_is_blank_object(w)); + return 0; +} + static bool per_machine_entry_empty(JsonVariant *v) { const char *k; _unused_ JsonVariant *e; @@ -1067,7 +1095,17 @@ int user_record_merge_secret(UserRecord *h, UserRecord *secret) { return r; if (secret->pkcs11_protected_authentication_path_permitted >= 0) { - r = user_record_set_pkcs11_protected_authentication_path_permitted(h, secret->pkcs11_protected_authentication_path_permitted); + r = user_record_set_pkcs11_protected_authentication_path_permitted( + h, + secret->pkcs11_protected_authentication_path_permitted); + if (r < 0) + return r; + } + + if (secret->fido2_user_presence_permitted >= 0) { + r = user_record_set_fido2_user_presence_permitted( + h, + secret->fido2_user_presence_permitted); if (r < 0) return r; } diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h index c20018fcba..2458298825 100644 --- a/src/home/user-record-util.h +++ b/src/home/user-record-util.h @@ -49,6 +49,7 @@ int user_record_make_hashed_password(UserRecord *h, char **password, bool extend int user_record_set_hashed_password(UserRecord *h, char **hashed_password); int user_record_set_token_pin(UserRecord *h, char **pin, bool prepend); int user_record_set_pkcs11_protected_authentication_path_permitted(UserRecord *h, int b); +int user_record_set_fido2_user_presence_permitted(UserRecord *h, int b); int user_record_set_password_change_now(UserRecord *h, int b); int user_record_merge_secret(UserRecord *h, UserRecord *secret); int user_record_good_authentication(UserRecord *h); diff --git a/src/libsystemd/sd-bus/bus-common-errors.c b/src/libsystemd/sd-bus/bus-common-errors.c index 28f98cebce..dc9a2fdc3a 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.c +++ b/src/libsystemd/sd-bus/bus-common-errors.c @@ -120,6 +120,8 @@ BUS_ERROR_MAP_ELF_REGISTER const sd_bus_error_map bus_common_errors[] = { SD_BUS_ERROR_MAP(BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN, EBADSLT), SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_PIN_NEEDED, ENOANO), SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED, ERFKILL), + SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_USER_PRESENCE_NEEDED, EMEDIUMTYPE), + SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_ACTION_TIMEOUT, ENOSTR), SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_PIN_LOCKED, EOWNERDEAD), SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_BAD_PIN, ENOLCK), SD_BUS_ERROR_MAP(BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT, ETOOMANYREFS), diff --git a/src/libsystemd/sd-bus/bus-common-errors.h b/src/libsystemd/sd-bus/bus-common-errors.h index 68ecbd65dd..ae80543880 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.h +++ b/src/libsystemd/sd-bus/bus-common-errors.h @@ -100,6 +100,8 @@ #define BUS_ERROR_BAD_PASSWORD_AND_NO_TOKEN "org.freedesktop.home1.BadPasswordAndNoToken" #define BUS_ERROR_TOKEN_PIN_NEEDED "org.freedesktop.home1.TokenPinNeeded" #define BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED "org.freedesktop.home1.TokenProtectedAuthenticationPathNeeded" +#define BUS_ERROR_TOKEN_USER_PRESENCE_NEEDED "org.freedesktop.home1.TokenUserPresenceNeeded" +#define BUS_ERROR_TOKEN_ACTION_TIMEOUT "org.freedesktop.home1.TokenActionTimeout" #define BUS_ERROR_TOKEN_PIN_LOCKED "org.freedesktop.home1.TokenPinLocked" #define BUS_ERROR_TOKEN_BAD_PIN "org.freedesktop.home1.BadPin" #define BUS_ERROR_TOKEN_BAD_PIN_FEW_TRIES_LEFT "org.freedesktop.home1.BadPinFewTriesLeft" diff --git a/src/shared/user-record.c b/src/shared/user-record.c index 9d9b9689e1..16edaa45fa 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -81,6 +81,7 @@ UserRecord* user_record_new(void) { .password_change_inactive_usec = UINT64_MAX, .password_change_now = -1, .pkcs11_protected_authentication_path_permitted = -1, + .fido2_user_presence_permitted = -1, }; return h; @@ -644,6 +645,7 @@ static int dispatch_secret(const char *name, JsonVariant *variant, JsonDispatchF { "tokenPin", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, token_pin), 0 }, { "pkcs11Pin", /* legacy alias */ _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, token_pin), 0 }, { "pkcs11ProtectedAuthenticationPathPermitted", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, pkcs11_protected_authentication_path_permitted), 0 }, + { "fido2UserPresencePermitted", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, fido2_user_presence_permitted), 0 }, {}, }; diff --git a/src/shared/user-record.h b/src/shared/user-record.h index 9e036c562b..e75f0ff00b 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -330,6 +330,7 @@ typedef struct UserRecord { size_t n_fido2_hmac_credential; Fido2HmacSalt *fido2_hmac_salt; size_t n_fido2_hmac_salt; + int fido2_user_presence_permitted; JsonVariant *json; } UserRecord; From 85b1294488d2de3a17dbd0f82771f03fd121e036 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 14 Apr 2020 19:26:48 +0200 Subject: [PATCH 11/20] pam-systemd-home: print helpful message when token's PIN is locked --- src/home/pam_systemd_home.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c index 87bf1620d5..2d2732a440 100644 --- a/src/home/pam_systemd_home.c +++ b/src/home/pam_systemd_home.c @@ -385,6 +385,11 @@ static int handle_generic_user_record_error( return PAM_SERVICE_ERR; } + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_LOCKED)) { + + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Security token PIN is locked, please unlock it first. (Hint: Removal and re-insertion might suffice.)"); + return PAM_SERVICE_ERR; + } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN)) { _cleanup_(erase_and_freep) char *newp = NULL; From f737186ab1570842b81dabf5054a138427238bd1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 15 Apr 2020 19:34:41 +0200 Subject: [PATCH 12/20] homectl: show touch emoji when asking for PKCS#11 protected auth path --- src/home/homectl.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/home/homectl.c b/src/home/homectl.c index 886069fb49..41531735de 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -325,7 +325,9 @@ static int handle_generic_user_record_error( } else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PROTECTED_AUTHENTICATION_PATH_NEEDED)) { - log_notice("Please authenticate physically on security token."); + log_notice("%s%sPlease authenticate physically on security token.", + emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", + emoji_enabled() ? " " : ""); r = user_record_set_pkcs11_protected_authentication_path_permitted(hr, true); if (r < 0) From 8e62dfb12aff85cc894ff7eef39fb21623335566 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 15 Apr 2020 19:35:07 +0200 Subject: [PATCH 13/20] homectl: do generic error handling/retry also when creating a home directory After all, when creating we might need interaction with the security token too, and our initial attempt to create the user will fail, since we do not allow interactive auth on the security token, so that we then can print a log message and retry with interactive auth then enabled. --- src/home/homectl.c | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/home/homectl.c b/src/home/homectl.c index 41531735de..943f526676 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -1459,25 +1459,28 @@ static int create_home(int argc, char *argv[], void *userdata) { r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); if (r < 0) { - if (!sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) - return log_error_errno(r, "Failed to create user home: %s", bus_error_message(&error, r)); + if (sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) { + log_error_errno(r, "%s", bus_error_message(&error, r)); + log_info("(Use --enforce-password-policy=no to turn off password quality checks for this account.)"); - log_error_errno(r, "%s", bus_error_message(&error, r)); - log_info("(Use --enforce-password-policy=no to turn off password quality checks for this account.)"); + r = user_record_set_hashed_password(hr, original_hashed_passwords); + if (r < 0) + return r; + + r = acquire_new_password(hr->user_name, hr, /* suggest = */ false); + if (r < 0) + return r; + + r = user_record_make_hashed_password(hr, hr->password, /* extend = */ true); + if (r < 0) + return log_error_errno(r, "Failed to hash passwords: %m"); + } else { + r = handle_generic_user_record_error(hr->user_name, hr, &error, r, false); + if (r < 0) + return r; + } } else break; /* done */ - - r = user_record_set_hashed_password(hr, original_hashed_passwords); - if (r < 0) - return r; - - r = acquire_new_password(hr->user_name, hr, /* suggest = */ false); - if (r < 0) - return r; - - r = user_record_make_hashed_password(hr, hr->password, /* extend = */ true); - if (r < 0) - return log_error_errno(r, "Failed to hash passwords: %m"); } return 0; From c98811d837c5b00370e25737766325b2ae6307fc Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 15 Apr 2020 19:35:55 +0200 Subject: [PATCH 14/20] homectl: rework how we log when doing a home directory update When updating a home directory we might update the record first, then resize the image and finally synchronize the passwords to the storage layers. These are three individually authenticated operations. Since each might require touching a FIDO2 or PKCS#11 key we should say what we are doing. Hence do so. Usually we are pretty quiet with what we do, and let's stick to that. Hence show this information only if we actually do more than one thing. If we only update (and do not resize/sync passwords) then let's be quiet as usual, as the command line then sufficiently clarifies what we are doing. --- src/home/homectl.c | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/home/homectl.c b/src/home/homectl.c index 943f526676..d47a9a6589 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -1623,6 +1623,26 @@ static int acquire_updated_home_record( return 0; } +static int home_record_reset_human_interaction_permission(UserRecord *hr) { + int r; + + assert(hr); + + /* When we execute multiple operations one after the other, let's reset the permission to ask the + * user each time, so that if interaction is necessary we will be told so again and thus can print a + * nice message to the user, telling the user so. */ + + r = user_record_set_pkcs11_protected_authentication_path_permitted(hr, -1); + if (r < 0) + return log_error_errno(r, "Failed to reset PKCS#11 protected authentication path permission flag: %m"); + + r = user_record_set_fido2_user_presence_permitted(hr, -1); + if (r < 0) + return log_error_errno(r, "Failed to reset FIDO2 user presence permission flag: %m"); + + return 0; +} + static int update_home(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; _cleanup_(user_record_unrefp) UserRecord *hr = NULL; @@ -1651,6 +1671,12 @@ static int update_home(int argc, char *argv[], void *userdata) { if (r < 0) return r; + /* If we do multiple operations, let's output things more verbosely, since otherwise the repeated + * authentication might be confusing. */ + + if (arg_and_resize || arg_and_change_password) + log_info("Updating home directory."); + for (;;) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; @@ -1686,13 +1712,16 @@ static int update_home(int argc, char *argv[], void *userdata) { break; } + if (arg_and_resize) + log_info("Resizing home."); + + (void) home_record_reset_human_interaction_permission(hr); + /* Also sync down disk size to underlying LUKS/fscrypt/quota */ while (arg_and_resize) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; - log_debug("Resizing"); - r = bus_message_new_method_call(bus, &m, bus_home_mgr, "ResizeHome"); if (r < 0) return bus_log_create_error(r); @@ -1719,13 +1748,16 @@ static int update_home(int argc, char *argv[], void *userdata) { break; } + if (arg_and_change_password) + log_info("Synchronizing passwords and encryption keys."); + + (void) home_record_reset_human_interaction_permission(hr); + /* Also sync down passwords to underlying LUKS/fscrypt */ while (arg_and_change_password) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; - log_debug("Propagating password"); - r = bus_message_new_method_call(bus, &m, bus_home_mgr, "ChangePasswordHome"); if (r < 0) return bus_log_create_error(r); From 93295a250179d30320b757a423f5697e91c87a26 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 15 Apr 2020 21:53:03 +0200 Subject: [PATCH 15/20] homectl: split out pkcs#11 related code bits into own .c/.h file There's some highly specific PKCS#11 code in homectl.c. Let's split that out, since it is easily isolatable, to make homectl.c a bit more readable. No funcional changes, just some moving around and renaming two functions to make them more suitably named when exported. --- src/home/homectl-pkcs11.c | 341 +++++++++++++++++++++++++++++++++++++ src/home/homectl-pkcs11.h | 8 + src/home/homectl.c | 342 +------------------------------------- src/home/meson.build | 2 + 4 files changed, 354 insertions(+), 339 deletions(-) create mode 100644 src/home/homectl-pkcs11.c create mode 100644 src/home/homectl-pkcs11.h diff --git a/src/home/homectl-pkcs11.c b/src/home/homectl-pkcs11.c new file mode 100644 index 0000000000..6762d44e7e --- /dev/null +++ b/src/home/homectl-pkcs11.c @@ -0,0 +1,341 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "errno-util.h" +#include "hexdecoct.h" +#include "homectl-pkcs11.h" +#include "libcrypt-util.h" +#include "memory-util.h" +#include "openssl-util.h" +#include "pkcs11-util.h" +#include "random-util.h" +#include "strv.h" + +struct pkcs11_callback_data { + char *pin_used; + X509 *cert; +}; + +static void pkcs11_callback_data_release(struct pkcs11_callback_data *data) { + erase_and_free(data->pin_used); + X509_free(data->cert); +} + +#if HAVE_P11KIT +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(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(EOPNOTSUPP, "PKCS#11 tokens not supported on this build."); +#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, + const void *encrypted_key, size_t encrypted_key_size, + const void *decrypted_key, size_t decrypted_key_size) { + + _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL; + _cleanup_(erase_and_freep) char *base64_encoded = NULL; + _cleanup_free_ char *salt = NULL; + struct crypt_data cd = {}; + char *k; + int r; + + assert(v); + assert(uri); + assert(encrypted_key); + assert(encrypted_key_size > 0); + assert(decrypted_key); + assert(decrypted_key_size > 0); + + r = make_salt(&salt); + if (r < 0) + return log_error_errno(r, "Failed to generate salt: %m"); + + /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends + * expect a NUL terminated string, and we use a binary key */ + r = base64mem(decrypted_key, decrypted_key_size, &base64_encoded); + if (r < 0) + return log_error_errno(r, "Failed to base64 encode secret key: %m"); + + errno = 0; + k = crypt_r(base64_encoded, salt, &cd); + if (!k) + return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m"); + + r = json_build(&e, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("uri", JSON_BUILD_STRING(uri)), + JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(encrypted_key, encrypted_key_size)), + JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(k)))); + if (r < 0) + return log_error_errno(r, "Failed to build encrypted JSON key object: %m"); + + w = json_variant_ref(json_variant_by_key(*v, "privileged")); + l = json_variant_ref(json_variant_by_key(w, "pkcs11EncryptedKey")); + + r = json_variant_append_array(&l, e); + if (r < 0) + return log_error_errno(r, "Failed append PKCS#11 encrypted key: %m"); + + r = json_variant_set_field(&w, "pkcs11EncryptedKey", l); + if (r < 0) + return log_error_errno(r, "Failed to set PKCS#11 encrypted key: %m"); + + r = json_variant_set_field(v, "privileged", w); + if (r < 0) + return log_error_errno(r, "Failed to update privileged field: %m"); + + return 0; +} + +static int add_pkcs11_token_uri(JsonVariant **v, const char *uri) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + _cleanup_strv_free_ char **l = NULL; + int r; + + assert(v); + assert(uri); + + w = json_variant_ref(json_variant_by_key(*v, "pkcs11TokenUri")); + if (w) { + r = json_variant_strv(w, &l); + if (r < 0) + return log_error_errno(r, "Failed to parse PKCS#11 token list: %m"); + + if (strv_contains(l, uri)) + return 0; + } + + r = strv_extend(&l, uri); + if (r < 0) + return log_oom(); + + w = json_variant_unref(w); + r = json_variant_new_array_strv(&w, l); + if (r < 0) + return log_error_errno(r, "Failed to create PKCS#11 token URI JSON: %m"); + + r = json_variant_set_field(v, "pkcs11TokenUri", w); + if (r < 0) + return log_error_errno(r, "Failed to update PKCS#11 token URI list: %m"); + + return 0; +} + +int identity_add_token_pin(JsonVariant **v, const char *pin) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL; + _cleanup_(strv_free_erasep) char **pins = NULL; + int r; + + assert(v); + + if (isempty(pin)) + return 0; + + w = json_variant_ref(json_variant_by_key(*v, "secret")); + l = json_variant_ref(json_variant_by_key(w, "tokenPin")); + + r = json_variant_strv(l, &pins); + if (r < 0) + return log_error_errno(r, "Failed to convert PIN array: %m"); + + if (strv_find(pins, pin)) + return 0; + + r = strv_extend(&pins, pin); + if (r < 0) + return log_oom(); + + strv_uniq(pins); + + l = json_variant_unref(l); + + r = json_variant_new_array_strv(&l, pins); + if (r < 0) + return log_error_errno(r, "Failed to allocate new PIN array JSON: %m"); + + json_variant_sensitive(l); + + r = json_variant_set_field(&w, "tokenPin", l); + if (r < 0) + return log_error_errno(r, "Failed to update PIN field: %m"); + + r = json_variant_set_field(v, "secret", w); + if (r < 0) + return log_error_errno(r, "Failed to update secret object: %m"); + + return 1; +} + +int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) { + _cleanup_(erase_and_freep) void *decrypted_key = NULL, *encrypted_key = NULL; + _cleanup_(erase_and_freep) char *pin = NULL; + size_t decrypted_key_size, encrypted_key_size; + _cleanup_(X509_freep) X509 *cert = NULL; + EVP_PKEY *pkey; + RSA *rsa; + int bits; + int r; + + assert(v); + + r = acquire_pkcs11_certificate(uri, &cert, &pin); + 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."); + + 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?"); + + 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 = 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"); + + /* Add the token URI to the public part of the record. */ + r = add_pkcs11_token_uri(v, uri); + if (r < 0) + return r; + + /* Include the encrypted version of the random key we just generated in the privileged part of the record */ + r = add_pkcs11_encrypted_key( + v, + uri, + encrypted_key, encrypted_key_size, + decrypted_key, decrypted_key_size); + if (r < 0) + return r; + + /* If we acquired the PIN also include it in the secret section of the record, so that systemd-homed + * can use it if it needs to, given that it likely needs to decrypt the key again to pass to LUKS or + * fscrypt. */ + r = identity_add_token_pin(v, pin); + if (r < 0) + return r; + + return 0; +} diff --git a/src/home/homectl-pkcs11.h b/src/home/homectl-pkcs11.h new file mode 100644 index 0000000000..efe1a2f223 --- /dev/null +++ b/src/home/homectl-pkcs11.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "json.h" + +int identity_add_token_pin(JsonVariant **v, const char *pin); + +int identity_add_pkcs11_key_data(JsonVariant **v, const char *token_uri); diff --git a/src/home/homectl.c b/src/home/homectl.c index d47a9a6589..95fcead038 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -4,7 +4,6 @@ #include "sd-bus.h" -#include "alloc-util.h" #include "ask-password-api.h" #include "bus-common-errors.h" #include "bus-error.h" @@ -15,16 +14,12 @@ #include "fd-util.h" #include "fileio.h" #include "format-table.h" -#include "format-util.h" -#include "fs-util.h" -#include "hexdecoct.h" #include "home-util.h" #include "homectl-fido2.h" -#include "libcrypt-util.h" +#include "homectl-pkcs11.h" #include "locale-util.h" #include "main-func.h" #include "memory-util.h" -#include "openssl-util.h" #include "pager.h" #include "parse-util.h" #include "path-util.h" @@ -32,7 +27,6 @@ #include "pretty-print.h" #include "process-util.h" #include "pwquality-util.h" -#include "random-util.h" #include "rlimit-util.h" #include "spawn-polkit-agent.h" #include "terminal-util.h" @@ -905,336 +899,6 @@ static int add_disposition(JsonVariant **v) { return 1; } -struct pkcs11_callback_data { - char *pin_used; - X509 *cert; -}; - -static void pkcs11_callback_data_release(struct pkcs11_callback_data *data) { - erase_and_free(data->pin_used); - X509_free(data->cert); -} - -#if HAVE_P11KIT -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(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(EOPNOTSUPP, "PKCS#11 tokens not supported on this build."); -#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_token_pin(JsonVariant **v, const char *pin) { - _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL; - _cleanup_(strv_free_erasep) char **pins = NULL; - int r; - - assert(v); - - if (isempty(pin)) - return 0; - - w = json_variant_ref(json_variant_by_key(*v, "secret")); - l = json_variant_ref(json_variant_by_key(w, "tokenPin")); - - r = json_variant_strv(l, &pins); - if (r < 0) - return log_error_errno(r, "Failed to convert PIN array: %m"); - - if (strv_find(pins, pin)) - return 0; - - r = strv_extend(&pins, pin); - if (r < 0) - return log_oom(); - - strv_uniq(pins); - - l = json_variant_unref(l); - - r = json_variant_new_array_strv(&l, pins); - if (r < 0) - return log_error_errno(r, "Failed to allocate new PIN array JSON: %m"); - - json_variant_sensitive(l); - - r = json_variant_set_field(&w, "tokenPin", l); - if (r < 0) - return log_error_errno(r, "Failed to update PIN field: %m"); - - r = json_variant_set_field(v, "secret", w); - if (r < 0) - return log_error_errno(r, "Failed to update secret object: %m"); - - return 1; -} - -static int add_pkcs11_encrypted_key( - JsonVariant **v, - const char *uri, - const void *encrypted_key, size_t encrypted_key_size, - const void *decrypted_key, size_t decrypted_key_size) { - - _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL; - _cleanup_(erase_and_freep) char *base64_encoded = NULL; - _cleanup_free_ char *salt = NULL; - struct crypt_data cd = {}; - char *k; - int r; - - assert(v); - assert(uri); - assert(encrypted_key); - assert(encrypted_key_size > 0); - assert(decrypted_key); - assert(decrypted_key_size > 0); - - r = make_salt(&salt); - if (r < 0) - return log_error_errno(r, "Failed to generate salt: %m"); - - /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends - * expect a NUL terminated string, and we use a binary key */ - r = base64mem(decrypted_key, decrypted_key_size, &base64_encoded); - if (r < 0) - return log_error_errno(r, "Failed to base64 encode secret key: %m"); - - errno = 0; - k = crypt_r(base64_encoded, salt, &cd); - if (!k) - return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m"); - - r = json_build(&e, JSON_BUILD_OBJECT( - JSON_BUILD_PAIR("uri", JSON_BUILD_STRING(uri)), - JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(encrypted_key, encrypted_key_size)), - JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(k)))); - if (r < 0) - return log_error_errno(r, "Failed to build encrypted JSON key object: %m"); - - w = json_variant_ref(json_variant_by_key(*v, "privileged")); - l = json_variant_ref(json_variant_by_key(w, "pkcs11EncryptedKey")); - - r = json_variant_append_array(&l, e); - if (r < 0) - return log_error_errno(r, "Failed append PKCS#11 encrypted key: %m"); - - r = json_variant_set_field(&w, "pkcs11EncryptedKey", l); - if (r < 0) - return log_error_errno(r, "Failed to set PKCS#11 encrypted key: %m"); - - r = json_variant_set_field(v, "privileged", w); - if (r < 0) - return log_error_errno(r, "Failed to update privileged field: %m"); - - return 0; -} - -static int add_pkcs11_token_uri(JsonVariant **v, const char *uri) { - _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; - _cleanup_strv_free_ char **l = NULL; - int r; - - assert(v); - assert(uri); - - w = json_variant_ref(json_variant_by_key(*v, "pkcs11TokenUri")); - if (w) { - r = json_variant_strv(w, &l); - if (r < 0) - return log_error_errno(r, "Failed to parse PKCS#11 token list: %m"); - - if (strv_contains(l, uri)) - return 0; - } - - r = strv_extend(&l, uri); - if (r < 0) - return log_oom(); - - w = json_variant_unref(w); - r = json_variant_new_array_strv(&w, l); - if (r < 0) - return log_error_errno(r, "Failed to create PKCS#11 token URI JSON: %m"); - - r = json_variant_set_field(v, "pkcs11TokenUri", w); - if (r < 0) - return log_error_errno(r, "Failed to update PKCS#11 token URI list: %m"); - - return 0; -} - -static int add_pkcs11_key_data(JsonVariant **v, const char *uri) { - _cleanup_(erase_and_freep) void *decrypted_key = NULL, *encrypted_key = NULL; - _cleanup_(erase_and_freep) char *pin = NULL; - size_t decrypted_key_size, encrypted_key_size; - _cleanup_(X509_freep) X509 *cert = NULL; - EVP_PKEY *pkey; - RSA *rsa; - int bits; - int r; - - assert(v); - - r = acquire_pkcs11_certificate(uri, &cert, &pin); - 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."); - - 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?"); - - 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 = 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"); - - /* Add the token URI to the public part of the record. */ - r = add_pkcs11_token_uri(v, uri); - if (r < 0) - return r; - - /* Include the encrypted version of the random key we just generated in the privileged part of the record */ - r = add_pkcs11_encrypted_key( - v, - uri, - encrypted_key, encrypted_key_size, - decrypted_key, decrypted_key_size); - if (r < 0) - return r; - - /* If we acquired the PIN also include it in the secret section of the record, so that systemd-homed - * can use it if it needs to, given that it likely needs to decrypt the key again to pass to LUKS or - * fscrypt. */ - r = add_token_pin(v, pin); - if (r < 0) - return r; - - return 0; -} - static int acquire_new_home_record(UserRecord **ret) { _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; _cleanup_(user_record_unrefp) UserRecord *hr = NULL; @@ -1262,7 +926,7 @@ static int acquire_new_home_record(UserRecord **ret) { return r; STRV_FOREACH(i, arg_pkcs11_token_uri) { - r = add_pkcs11_key_data(&v, *i); + r = identity_add_pkcs11_key_data(&v, *i); if (r < 0) return r; } @@ -1591,7 +1255,7 @@ static int acquire_updated_home_record( return r; STRV_FOREACH(i, arg_pkcs11_token_uri) { - r = add_pkcs11_key_data(&json, *i); + r = identity_add_pkcs11_key_data(&json, *i); if (r < 0) return r; } diff --git a/src/home/meson.build b/src/home/meson.build index 7dee08408a..797f3a3c6d 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -71,6 +71,8 @@ homectl_sources = files(''' home-util.h homectl-fido2.c homectl-fido2.h + homectl-pkcs11.c + homectl-pkcs11.h homectl.c pwquality-util.c pwquality-util.h From 2af3966af33b961f7bb8239287037dce7f41af5a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 15 Apr 2020 21:56:31 +0200 Subject: [PATCH 16/20] homectl: add acquired fido2 PIN to user record If we successfully acquired the PIN for the fido2 key, let's add it to our user record, so that we can pass it to homed, which will need it too. --- src/home/homectl-fido2.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/home/homectl-fido2.c b/src/home/homectl-fido2.c index c78645fa3b..b7b2c1a3b5 100644 --- a/src/home/homectl-fido2.c +++ b/src/home/homectl-fido2.c @@ -9,6 +9,7 @@ #include "format-table.h" #include "hexdecoct.h" #include "homectl-fido2.h" +#include "homectl-pkcs11.h" #include "libcrypt-util.h" #include "locale-util.h" #include "memory-util.h" @@ -399,6 +400,13 @@ int identity_add_fido2_parameters( if (r < 0) return r; + /* If we acquired the PIN also include it in the secret section of the record, so that systemd-homed + * can use it if it needs to, given that it likely needs to decrypt the key again to pass to LUKS or + * fscrypt. */ + r = identity_add_token_pin(v, used_pin); + if (r < 0) + return r; + return 0; #else return log_error_errno(EOPNOTSUPP, "FIDO2 tokens not supported on this build."); From 0eb3be464446ff98dba63cac6769467514403e10 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Apr 2020 09:44:55 +0200 Subject: [PATCH 17/20] homectl: add --pkcs11-uri=auto and --pkcs-11-uri=list support We have the same for FIDO2 devices, for listing suitable devices, or picking the right one automatically, let's add that for PKCS11 too. --- src/home/homectl-pkcs11.c | 139 ++++++++++++++++++++++++++++++++++++++ src/home/homectl-pkcs11.h | 3 + src/home/homectl.c | 18 ++++- src/shared/pkcs11-util.c | 40 ++++++++--- src/shared/pkcs11-util.h | 2 + 5 files changed, 188 insertions(+), 14 deletions(-) diff --git a/src/home/homectl-pkcs11.c b/src/home/homectl-pkcs11.c index 6762d44e7e..830aafaab1 100644 --- a/src/home/homectl-pkcs11.c +++ b/src/home/homectl-pkcs11.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #include "errno-util.h" +#include "format-table.h" #include "hexdecoct.h" #include "homectl-pkcs11.h" #include "libcrypt-util.h" @@ -339,3 +340,141 @@ 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(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(EOPNOTSUPP, "PKCS#11 tokens not supported on this build."); +#endif +} diff --git a/src/home/homectl-pkcs11.h b/src/home/homectl-pkcs11.h index efe1a2f223..0403c73ea1 100644 --- a/src/home/homectl-pkcs11.h +++ b/src/home/homectl-pkcs11.h @@ -6,3 +6,6 @@ int identity_add_token_pin(JsonVariant **v, const char *pin); int identity_add_pkcs11_key_data(JsonVariant **v, const char *token_uri); + +int list_pkcs11_tokens(void); +int find_pkcs11_token_auto(char **ret); diff --git a/src/home/homectl.c b/src/home/homectl.c index 95fcead038..74c967eb26 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -3098,6 +3098,9 @@ static int parse_argv(int argc, char *argv[]) { case ARG_PKCS11_TOKEN_URI: { const char *p; + if (streq(optarg, "list")) + return list_pkcs11_tokens(); + /* If --pkcs11-token-uri= is specified we always drop everything old */ FOREACH_STRING(p, "pkcs11TokenUri", "pkcs11EncryptedKey") { r = drop_from_identity(p); @@ -3110,10 +3113,19 @@ static int parse_argv(int argc, char *argv[]) { break; } - if (!pkcs11_uri_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid PKCS#11 URI: %s", optarg); + if (streq(optarg, "auto")) { + _cleanup_free_ char *found = NULL; - r = strv_extend(&arg_pkcs11_token_uri, optarg); + r = find_pkcs11_token_auto(&found); + if (r < 0) + return r; + r = strv_consume(&arg_pkcs11_token_uri, TAKE_PTR(found)); + } else { + if (!pkcs11_uri_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid PKCS#11 URI: %s", optarg); + + r = strv_extend(&arg_pkcs11_token_uri, optarg); + } if (r < 0) return r; diff --git a/src/shared/pkcs11-util.c b/src/shared/pkcs11-util.c index b8863d2525..632964df44 100644 --- a/src/shared/pkcs11-util.c +++ b/src/shared/pkcs11-util.c @@ -151,6 +151,28 @@ char *pkcs11_token_label(const CK_TOKEN_INFO *token_info) { return t; } +char *pkcs11_token_manufacturer_id(const CK_TOKEN_INFO *token_info) { + char *t; + + t = strndup((char*) token_info->manufacturerID, sizeof(token_info->manufacturerID)); + if (!t) + return NULL; + + strstrip(t); + return t; +} + +char *pkcs11_token_model(const CK_TOKEN_INFO *token_info) { + char *t; + + t = strndup((char*) token_info->model, sizeof(token_info->model)); + if (!t) + return NULL; + + strstrip(t); + return t; +} + int pkcs11_token_login( CK_FUNCTION_LIST *m, CK_SESSION_HANDLE session, @@ -165,9 +187,8 @@ int pkcs11_token_login( _cleanup_free_ char *token_uri_string = NULL, *token_uri_escaped = NULL, *id = NULL, *token_label = NULL; _cleanup_(p11_kit_uri_freep) P11KitUri *token_uri = NULL; CK_TOKEN_INFO updated_token_info; - int uri_result; + int uri_result, r; CK_RV rv; - int r; assert(m); assert(token_info); @@ -703,7 +724,6 @@ static int token_process( assert(m); assert(slot_info); assert(token_info); - assert(search_uri); token_label = pkcs11_token_label(token_info); if (!token_label) @@ -741,7 +761,6 @@ static int slot_process( CK_RV rv; assert(m); - assert(search_uri); /* We return -EAGAIN for all failures we can attribute to a specific slot in some way, so that the * caller might try other slots before giving up. */ @@ -787,7 +806,7 @@ static int slot_process( return -EAGAIN; } - if (!p11_kit_uri_match_token_info(search_uri, &token_info)) { + if (search_uri && !p11_kit_uri_match_token_info(search_uri, &token_info)) { log_debug("Found non-matching token with URI %s.", token_uri_string); return -EAGAIN; } @@ -821,7 +840,6 @@ static int module_process( int r; assert(m); - assert(search_uri); /* We ignore most errors from modules here, in order to skip over faulty modules: one faulty module * should not have the effect that we don't try the others anymore. We indicate such per-module @@ -884,14 +902,14 @@ int pkcs11_find_token( _cleanup_(p11_kit_uri_freep) P11KitUri *search_uri = NULL; int r; - assert(pkcs11_uri); - /* Execute the specified callback for each matching token found. If nothing is found returns * -EAGAIN. Logs about all errors, except for EAGAIN, which the caller has to log about. */ - r = uri_from_string(pkcs11_uri, &search_uri); - if (r < 0) - return log_error_errno(r, "Failed to parse PKCS#11 URI '%s': %m", pkcs11_uri); + if (pkcs11_uri) { + r = uri_from_string(pkcs11_uri, &search_uri); + if (r < 0) + return log_error_errno(r, "Failed to parse PKCS#11 URI '%s': %m", pkcs11_uri); + } modules = p11_kit_modules_load_and_initialize(0); if (!modules) diff --git a/src/shared/pkcs11-util.h b/src/shared/pkcs11-util.h index 46791eb23b..959e7c3e0d 100644 --- a/src/shared/pkcs11-util.h +++ b/src/shared/pkcs11-util.h @@ -27,6 +27,8 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(CK_FUNCTION_LIST**, p11_kit_modules_finalize_and_rel CK_RV pkcs11_get_slot_list_malloc(CK_FUNCTION_LIST *m, CK_SLOT_ID **ret_slotids, CK_ULONG *ret_n_slotids); char *pkcs11_token_label(const CK_TOKEN_INFO *token_info); +char *pkcs11_token_manufacturer_id(const CK_TOKEN_INFO *token_info); +char *pkcs11_token_model(const CK_TOKEN_INFO *token_info); int pkcs11_token_login(CK_FUNCTION_LIST *m, CK_SESSION_HANDLE session, CK_SLOT_ID slotid, const CK_TOKEN_INFO *token_info, const char *friendly_name, const char *icon_name, const char *keyname, usec_t until, char **ret_used_pin); From fe2520fbb56c04fab06b959bbd12662bb8b745fd Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Apr 2020 10:03:44 +0200 Subject: [PATCH 18/20] docs: document new FIDO2 user record fields --- docs/USER_RECORD.md | 48 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md index 2ed043734f..514a941605 100644 --- a/docs/USER_RECORD.md +++ b/docs/USER_RECORD.md @@ -546,6 +546,11 @@ below). It's undefined how precise the URI is: during log-in it is tested against all plugged in security tokens and if there's exactly one matching private key found with it it is used. +`fido2HmacCredential` → An array of strings, each with a Base64-encoded FIDO2 +credential ID that shell be used for authentication with FIDO2 devices that +implement the `hmac-secret` extension. The salt to pass to the FIDO2 device is +found in `fido2HmacSalt`. + `privileged` → An object, which contains the fields of the `privileged` section of the user record, see below. @@ -594,7 +599,7 @@ as the lines in the traditional `~/.ssh/authorized_key` file. `pkcs11EncryptedKey` → An array of objects. Each element of the array should be an object consisting of three string fields: `uri` shall contain a PKCS#11 -security token URI, `data` shall contain a Base64 encoded encrypted key and +security token URI, `data` shall contain a Base64-encoded encrypted key and `hashedPassword` shall contain a UNIX password hash to test the key against. Authenticating with a security token against this account shall work as follows: the encrypted secret key is converted from its Base64 @@ -602,13 +607,29 @@ representation into binary, then decrypted with the PKCS#11 `C_Decrypt()` function of the PKCS#11 module referenced by the specified URI, using the private key found on the same token. The resulting decrypted key is then Base64-encoded and tested against the specified UNIX hashed password. The -Base64-enceded decrypted key may also be used to unlock further resources +Base64-encoded decrypted key may also be used to unlock further resources during log-in, for example the LUKS or `fscrypt` storage backend. It is generally recommended that for each entry in `pkcs11EncryptedKey` there's also a matching one in `pkcs11TokenUri` and vice versa, with the same URI, appearing in the same order, but this should not be required by applications processing user records. +`fido2HmacSalt` → An array of objects, implementing authentication support with +FIDO2 devices that implement the `hmac-secret` extension. Each element of the +array should be an object consisting of three string fields: `credential`, +`salt`, `hashedPassword`. The first two shall contain Base64-encoded binary +data: the FIDO2 credential ID and the salt value to pass to the FIDO2 +device. During authentication this salt along with the credential ID is sent to +the FIDO2 token, which will HMAC hash the salt with its internal secret key and +return the result. This resulting binary key should then be Base64-encoded and +used as string password for the further layers of the stack. The +`hashedPassword` field of the `fido2HmacSalt` field shall be a UNIX password +hash to test this derived secret key against for authentication. It is +generally recommended that for each entry in `fido2HmacSalt` there's also a +matching one in `fido2HmacCredential`, and vice versa, with the same credential +ID, appearing in the same order, but this should not be required by +applications processing user recrods. + ## Fields in the `perMachine` section As mentioned, the `perMachine` section contains settings that shall apply to @@ -652,13 +673,13 @@ that may be used in this section are identical to the equally named ones in the `mountNoDevices`, `mountNoSuid`, `mountNoExecute`, `cifsDomain`, `cifsUserName`, `cifsService`, `imagePath`, `uid`, `gid`, `memberOf`, `fileSystemType`, `partitionUuid`, `luksUuid`, `fileSystemUuid`, `luksDiscard`, -`luksOfflineDiscard`, `luksOfflineDiscard`, `luksCipher`, `luksCipherMode`, -`luksVolumeKeySize`, `luksPbkdfHashAlgorithm`, `luksPbkdfType`, -`luksPbkdfTimeCostUSec`, `luksPbkdfMemoryCost`, `luksPbkdfParallelThreads`, -`rateLimitIntervalUSec`, `rateLimitBurst`, `enforcePasswordPolicy`, -`autoLogin`, `stopDelayUSec`, `killProcesses`, `passwordChangeMinUSec`, -`passwordChangeMaxUSec`, `passwordChangeWarnUSec`, -`passwordChangeInactiveUSec`, `passwordChangeNow`, `pkcs11TokenUri`. +`luksOfflineDiscard`, `luksCipher`, `luksCipherMode`, `luksVolumeKeySize`, +`luksPbkdfHashAlgorithm`, `luksPbkdfType`, `luksPbkdfTimeCostUSec`, +`luksPbkdfMemoryCost`, `luksPbkdfParallelThreads`, `rateLimitIntervalUSec`, +`rateLimitBurst`, `enforcePasswordPolicy`, `autoLogin`, `stopDelayUSec`, +`killProcesses`, `passwordChangeMinUSec`, `passwordChangeMaxUSec`, +`passwordChangeWarnUSec`, `passwordChangeInactiveUSec`, `passwordChangeNow`, +`pkcs11TokenUri`, `fido2HmacCredential`. ## Fields in the `binding` section @@ -810,7 +831,7 @@ public key. The `signature` field in the top-level user record object is an array of objects. Each object encapsulates one signature and has two fields: `data` and `key` (both are strings). The `data` field contains the actual signature, -encoded in base64, the `key` field contains a copy of the public key whose +encoded in Base64, the `key` field contains a copy of the public key whose private key was used to make the signature, in PEM format. Currently only signatures with Ed25519 keys are defined. @@ -872,7 +893,12 @@ in case both are set.) `pkcs11ProtectedAuthenticationPathPermitted` → a boolean. If set to true allows the receiver to use the PKCS#11 "protected authentication path" (i.e. a physical button/touch element on the security token) for authenticating the -user. If false or unset authentication this way shall not be attempted. +user. If false or unset, authentication this way shall not be attempted. + +`fido2UserPresencePermitted` → a boolean. If set to true allows the receiver to +use the FIDO2 "user presence" flag. This is similar to the concept of +`pkcs11ProtectedAuthenticationPathPermitted`, but exposes the FIDO2 concept +behind it. If false or unset authentication this way shall not be attempted. ## Mapping to `struct passwd` and `struct spwd` From 4442c26942c6355371be529d1c7caac434336d7d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Apr 2020 10:51:54 +0200 Subject: [PATCH 19/20] man: update homectl man page with documentation for new features --- man/homectl.xml | 62 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/man/homectl.xml b/man/homectl.xml index 3a4c6e5f5b..f25782308b 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -332,7 +332,49 @@ then generated, encrypted with the public key of the X.509 certificate, and stored as part of the user record. At login time it is decrypted with the PKCS#11 module and then used to unlock the account and associated resources. See below for an example how to set up authentication with security - token. + token. + + Instead of a valid PKCS#11 URI, the special strings list and + auto may be specified. If list is passed, a brief table of + suitable, currently plugged in PKCS#11 hardware tokens is shown, along with their URIs. If + auto is passed, a suitable PKCS#11 hardware token is automatically selected (this + operation will fail if there isn't exactly one suitable token discovered). The latter is a useful + shortcut for the most common case where a single PKCS#11 hardware token is plugged in. + + Note that many hardware security tokens implement both PKCS#11/PIV and FIDO2 with the + hmac-secret extension (for example: the YubiKey 5 series), as supported with the + option below. Both mechanisms are similarly powerful, though FIDO2 + is the more modern technology. PKCS#11/PIV tokens have the benefit of being recognizable before + authentication and hence can be used for implying the user identity to use for logging in, which + FIDO2 does not allow. PKCS#11/PIV devices generally require initialization (i.e. storing a + private/public key pair on them, see example below) before they can be used; FIDO2 security tokens + generally do not required that, and work out of the box. + + + + PATH + + Takes a path to a Linux hidraw device + (e.g. /dev/hidraw1), referring to a FIDO2 security token implementing the + hmac-secret extension, that shall be able to unlock the user account. If used, a + random salt value is generated on the host, which is passed to the FIDO2 device, which calculates a + HMAC hash of it, keyed by its internal secret key. The result is then used as key for unlocking the + user account. The random salt is included in the user record, so that whenever authentication is + needed it can be passed again to the FIDO2 token, to retrieve the actual key. + + Instead of a valid path to a FIDO2 hidraw device the special strings + list and auto may be specified. If list is + passed, a brief table of suitable discovered FIDO2 devices is shown. If auto is + passed, a suitable FIDO2 token is automatically selected, if exactly one is discovered. The latter is + a useful shortcut for the most common case where a single FIDO2 hardware token is plugged in. + + Note that FIDO2 devices suitable for this option must implement the + hmac-secret extension. Most current devices (such as the YubiKey 5 series) do. If + the extension is not implemented the device cannot be used for unlocking home directories. + + Note that many hardware security tokens implement both FIDO2 and PKCS#11/PIV (and thus may be + used with either or ), for a + discussion see above. @@ -810,7 +852,7 @@ - Set up authentication with a YubiKey security token: + Set up authentication with a YubiKey security token using PKCS#11/PIV: # Clear the Yubikey from any old keys (careful!) ykman piv reset @@ -821,16 +863,18 @@ ykman piv generate-key -a RSA2048 9d pubkey.pem # Create a self-signed certificate from this public key, and store it on the device. ykman piv generate-certificate --subject "Knobelei" 9d pubkey.pem -# We don't need the publibc key on disk anymore +# We don't need the public key on disk anymore rm 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 +# Allow the security token to unlock the account of user 'lafcadio'. +homectl update lafcadio --pkcs11-token-uri=auto + -# Allow the security token referenced by the determined PKCS#11 URI to unlock the account of user -# 'lafcadio'. (Replace the '…' by the URI from the clipboard.) -homectl update lafcadio --pkcs11-token-uri=… + + Set up authentication with a FIDO2 security token: + + # Allow a FIDO2 security token to unlock the account of user 'nihilbaxter'. +homectl update nihilbaxter --fido2-device=auto From c8fe23d45c59d0dd1dc299b4ba6eb90d7ab4edec Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Apr 2020 11:12:39 +0200 Subject: [PATCH 20/20] update TODO --- TODO | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TODO b/TODO index 6751bd96aa..2ef8035099 100644 --- a/TODO +++ b/TODO @@ -150,8 +150,8 @@ 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/homed: also support FIDO2 HMAC password logic for unlocking - devices. (see: https://github.com/mjec/fido2-hmac-secret) +* 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