/* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "libfido2-util.h" #if HAVE_LIBFIDO2 #include "alloc-util.h" #include "ask-password-api.h" #include "dlfcn-util.h" #include "format-table.h" #include "glyph-util.h" #include "log.h" #include "memory-util.h" #include "random-util.h" #include "strv.h" #include "unistd.h" static void *libfido2_dl = NULL; int (*sym_fido_assert_allow_cred)(fido_assert_t *, const unsigned char *, size_t) = NULL; void (*sym_fido_assert_free)(fido_assert_t **) = NULL; size_t (*sym_fido_assert_hmac_secret_len)(const fido_assert_t *, size_t) = NULL; const unsigned char* (*sym_fido_assert_hmac_secret_ptr)(const fido_assert_t *, size_t) = NULL; fido_assert_t* (*sym_fido_assert_new)(void) = NULL; int (*sym_fido_assert_set_clientdata_hash)(fido_assert_t *, const unsigned char *, size_t) = NULL; int (*sym_fido_assert_set_extensions)(fido_assert_t *, int) = NULL; int (*sym_fido_assert_set_hmac_salt)(fido_assert_t *, const unsigned char *, size_t) = NULL; int (*sym_fido_assert_set_rp)(fido_assert_t *, const char *) = NULL; int (*sym_fido_assert_set_up)(fido_assert_t *, fido_opt_t) = NULL; int (*sym_fido_assert_set_uv)(fido_assert_t *, fido_opt_t) = NULL; size_t (*sym_fido_cbor_info_extensions_len)(const fido_cbor_info_t *) = NULL; char **(*sym_fido_cbor_info_extensions_ptr)(const fido_cbor_info_t *) = NULL; void (*sym_fido_cbor_info_free)(fido_cbor_info_t **) = NULL; fido_cbor_info_t* (*sym_fido_cbor_info_new)(void) = NULL; size_t (*sym_fido_cbor_info_options_len)(const fido_cbor_info_t *) = NULL; char** (*sym_fido_cbor_info_options_name_ptr)(const fido_cbor_info_t *) = NULL; const bool* (*sym_fido_cbor_info_options_value_ptr)(const fido_cbor_info_t *) = NULL; void (*sym_fido_cred_free)(fido_cred_t **) = NULL; size_t (*sym_fido_cred_id_len)(const fido_cred_t *) = NULL; const unsigned char* (*sym_fido_cred_id_ptr)(const fido_cred_t *) = NULL; fido_cred_t* (*sym_fido_cred_new)(void) = NULL; int (*sym_fido_cred_set_clientdata_hash)(fido_cred_t *, const unsigned char *, size_t) = NULL; int (*sym_fido_cred_set_extensions)(fido_cred_t *, int) = NULL; int (*sym_fido_cred_set_rk)(fido_cred_t *, fido_opt_t) = NULL; int (*sym_fido_cred_set_rp)(fido_cred_t *, const char *, const char *) = NULL; int (*sym_fido_cred_set_type)(fido_cred_t *, int) = NULL; int (*sym_fido_cred_set_user)(fido_cred_t *, const unsigned char *, size_t, const char *, const char *, const char *) = NULL; int (*sym_fido_cred_set_uv)(fido_cred_t *, fido_opt_t) = NULL; void (*sym_fido_dev_free)(fido_dev_t **) = NULL; int (*sym_fido_dev_get_assert)(fido_dev_t *, fido_assert_t *, const char *) = NULL; int (*sym_fido_dev_get_cbor_info)(fido_dev_t *, fido_cbor_info_t *) = NULL; void (*sym_fido_dev_info_free)(fido_dev_info_t **, size_t) = NULL; int (*sym_fido_dev_info_manifest)(fido_dev_info_t *, size_t, size_t *) = NULL; const char* (*sym_fido_dev_info_manufacturer_string)(const fido_dev_info_t *) = NULL; const char* (*sym_fido_dev_info_product_string)(const fido_dev_info_t *) = NULL; fido_dev_info_t* (*sym_fido_dev_info_new)(size_t) = NULL; const char* (*sym_fido_dev_info_path)(const fido_dev_info_t *) = NULL; const fido_dev_info_t* (*sym_fido_dev_info_ptr)(const fido_dev_info_t *, size_t) = NULL; bool (*sym_fido_dev_is_fido2)(const fido_dev_t *) = NULL; int (*sym_fido_dev_make_cred)(fido_dev_t *, fido_cred_t *, const char *) = NULL; fido_dev_t* (*sym_fido_dev_new)(void) = NULL; int (*sym_fido_dev_open)(fido_dev_t *, const char *) = NULL; int (*sym_fido_dev_close)(fido_dev_t *) = NULL; const char* (*sym_fido_strerr)(int) = NULL; int dlopen_libfido2(void) { return dlopen_many_sym_or_warn( &libfido2_dl, "libfido2.so.1", LOG_DEBUG, DLSYM_ARG(fido_assert_allow_cred), DLSYM_ARG(fido_assert_free), DLSYM_ARG(fido_assert_hmac_secret_len), DLSYM_ARG(fido_assert_hmac_secret_ptr), DLSYM_ARG(fido_assert_new), DLSYM_ARG(fido_assert_set_clientdata_hash), DLSYM_ARG(fido_assert_set_extensions), DLSYM_ARG(fido_assert_set_hmac_salt), DLSYM_ARG(fido_assert_set_rp), DLSYM_ARG(fido_assert_set_up), DLSYM_ARG(fido_assert_set_uv), DLSYM_ARG(fido_cbor_info_extensions_len), DLSYM_ARG(fido_cbor_info_extensions_ptr), DLSYM_ARG(fido_cbor_info_free), DLSYM_ARG(fido_cbor_info_new), DLSYM_ARG(fido_cbor_info_options_len), DLSYM_ARG(fido_cbor_info_options_name_ptr), DLSYM_ARG(fido_cbor_info_options_value_ptr), DLSYM_ARG(fido_cred_free), DLSYM_ARG(fido_cred_id_len), DLSYM_ARG(fido_cred_id_ptr), DLSYM_ARG(fido_cred_new), DLSYM_ARG(fido_cred_set_clientdata_hash), DLSYM_ARG(fido_cred_set_extensions), DLSYM_ARG(fido_cred_set_rk), DLSYM_ARG(fido_cred_set_rp), DLSYM_ARG(fido_cred_set_type), DLSYM_ARG(fido_cred_set_user), DLSYM_ARG(fido_cred_set_uv), DLSYM_ARG(fido_dev_free), DLSYM_ARG(fido_dev_get_assert), DLSYM_ARG(fido_dev_get_cbor_info), DLSYM_ARG(fido_dev_info_free), DLSYM_ARG(fido_dev_info_manifest), DLSYM_ARG(fido_dev_info_manufacturer_string), DLSYM_ARG(fido_dev_info_new), DLSYM_ARG(fido_dev_info_path), DLSYM_ARG(fido_dev_info_product_string), DLSYM_ARG(fido_dev_info_ptr), DLSYM_ARG(fido_dev_is_fido2), DLSYM_ARG(fido_dev_make_cred), DLSYM_ARG(fido_dev_new), DLSYM_ARG(fido_dev_open), DLSYM_ARG(fido_dev_close), DLSYM_ARG(fido_strerr)); } static int verify_features( fido_dev_t *d, const char *path, int log_level, /* the log level to use when device is not FIDO2 with hmac-secret */ bool *ret_has_rk, bool *ret_has_client_pin, bool *ret_has_up, bool *ret_has_uv) { _cleanup_(fido_cbor_info_free_wrapper) fido_cbor_info_t *di = NULL; bool found_extension = false; char **e, **o; const bool *b; bool has_rk = false, has_client_pin = false, has_up = true, has_uv = false; /* Defaults are per table in 5.4 in FIDO2 spec */ size_t n; int r; assert(d); assert(path); if (!sym_fido_dev_is_fido2(d)) return log_full_errno(log_level, SYNTHETIC_ERRNO(ENODEV), "Specified device %s is not a FIDO2 device.", path); di = sym_fido_cbor_info_new(); if (!di) return log_oom(); r = sym_fido_dev_get_cbor_info(d, di); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get CBOR device info for %s: %s", path, sym_fido_strerr(r)); e = sym_fido_cbor_info_extensions_ptr(di); n = sym_fido_cbor_info_extensions_len(di); for (size_t i = 0; i < n; i++) { log_debug("FIDO2 device implements extension: %s", e[i]); if (streq(e[i], "hmac-secret")) found_extension = true; } o = sym_fido_cbor_info_options_name_ptr(di); b = sym_fido_cbor_info_options_value_ptr(di); n = sym_fido_cbor_info_options_len(di); for (size_t i = 0; i < n; i++) { log_debug("FIDO2 device implements option %s: %s", o[i], yes_no(b[i])); if (streq(o[i], "rk")) has_rk = b[i]; if (streq(o[i], "clientPin")) has_client_pin = b[i]; if (streq(o[i], "up")) has_up = b[i]; if (streq(o[i], "uv")) has_uv = b[i]; } if (!found_extension) return log_full_errno(log_level, SYNTHETIC_ERRNO(ENODEV), "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", path); log_debug("Has rk ('Resident Key') support: %s\n" "Has clientPin support: %s\n" "Has up ('User Presence') support: %s\n" "Has uv ('User Verification') support: %s\n", yes_no(has_rk), yes_no(has_client_pin), yes_no(has_up), yes_no(has_uv)); if (ret_has_rk) *ret_has_rk = has_rk; if (ret_has_client_pin) *ret_has_client_pin = has_client_pin; if (ret_has_up) *ret_has_up = has_up; if (ret_has_uv) *ret_has_uv = has_uv; return 0; } static int fido2_assert_set_basic_properties( fido_assert_t *a, const char *rp_id, const void *cid, size_t cid_size) { int r; assert(a); assert(rp_id); assert(cid); assert(cid_size > 0); r = sym_fido_assert_set_rp(a, rp_id); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set FIDO2 assertion ID: %s", sym_fido_strerr(r)); r = sym_fido_assert_set_clientdata_hash(a, (const unsigned char[32]) {}, 32); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set FIDO2 assertion client data hash: %s", sym_fido_strerr(r)); r = sym_fido_assert_allow_cred(a, cid, cid_size); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to add FIDO2 assertion credential ID: %s", sym_fido_strerr(r)); return 0; } static int fido2_common_assert_error_handle(int r) { switch (r) { case FIDO_OK: return 0; 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."); #ifdef FIDO_ERR_UV_BLOCKED case FIDO_ERR_UV_BLOCKED: return log_error_errno(SYNTHETIC_ERRNO(EOWNERDEAD), "Verification of security token is blocked, please remove/reinsert token."); #endif case FIDO_ERR_PIN_INVALID: return log_error_errno(SYNTHETIC_ERRNO(ENOLCK), "PIN of security token incorrect."); case FIDO_ERR_UP_REQUIRED: return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "User presence required."); case FIDO_ERR_ACTION_TIMEOUT: return log_error_errno(SYNTHETIC_ERRNO(ENOSTR), "Token action timeout. (User didn't interact with token quickly enough.)"); default: return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to ask token for assertion: %s", sym_fido_strerr(r)); } } static int fido2_is_cred_in_specific_token( const char *path, const char *rp_id, const void *cid, size_t cid_size, Fido2EnrollFlags flags) { assert(path); assert(rp_id); assert(cid); assert(cid_size); _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL; _cleanup_(fido_assert_free_wrapper) fido_assert_t *a = NULL; bool has_up = false, has_uv = false; int r; d = sym_fido_dev_new(); if (!d) return log_oom(); r = sym_fido_dev_open(d, path); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to open FIDO2 device %s: %s", path, sym_fido_strerr(r)); r = verify_features(d, path, LOG_ERR, NULL, NULL, &has_up, &has_uv); if (r == -ENODEV) { /* Not a FIDO2 device or lacking HMAC-SECRET extension */ log_debug_errno(r, "%s is not a FIDO2 device, or it lacks the hmac-secret extension", path); return false; } if (r < 0) return r; a = sym_fido_assert_new(); if (!a) return log_oom(); r = fido2_assert_set_basic_properties(a, rp_id, cid, cid_size); if (r < 0) return r; /* FIDO2 devices may not support pre-flight requests with UV, at least not * without user interaction [1]. As a result, let's just return true * here and go ahead with trying the unlock directly. * Reference: * 1: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-getAssert-authnr-alg * See section 7.4 */ if (has_uv && FLAGS_SET(flags, FIDO2ENROLL_UV)) { log_debug("Pre-flight requests with UV are unsupported, device: %s", path); return true; } /* According to CTAP 2.1 specification, to do pre-flight we need to set up option to false * with optionally pinUvAuthParam in assertion[1]. But for authenticator that doesn't support * user presence, once up option is present, the authenticator may return CTAP2_ERR_UNSUPPORTED_OPTION[2]. * So we simplely omit the option in that case. * Reference: * 1: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#pre-flight * 2: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetAssertion (in step 5) */ if (has_up) r = sym_fido_assert_set_up(a, FIDO_OPT_FALSE); else r = sym_fido_assert_set_up(a, FIDO_OPT_OMIT); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set assertion user presence: %s", sym_fido_strerr(r)); r = sym_fido_dev_get_assert(d, a, NULL); switch (r) { case FIDO_OK: return true; case FIDO_ERR_NO_CREDENTIALS: return false; default: return fido2_common_assert_error_handle(r); } } static int fido2_use_hmac_hash_specific_token( const char *path, const char *rp_id, const void *salt, size_t salt_size, const void *cid, size_t cid_size, char **pins, Fido2EnrollFlags required, /* client pin/user presence required */ void **ret_hmac, size_t *ret_hmac_size) { _cleanup_(fido_assert_free_wrapper) fido_assert_t *a = NULL; _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL; _cleanup_(erase_and_freep) void *hmac_copy = NULL; bool has_up, has_client_pin, has_uv; size_t hmac_size; const void *hmac; int r; assert(path); assert(rp_id); assert(salt); assert(cid); assert(ret_hmac); assert(ret_hmac_size); d = sym_fido_dev_new(); if (!d) return log_oom(); r = sym_fido_dev_open(d, path); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to open FIDO2 device %s: %s", path, sym_fido_strerr(r)); r = verify_features(d, path, LOG_ERR, NULL, &has_client_pin, &has_up, &has_uv); if (r < 0) return r; if (!has_client_pin && FLAGS_SET(required, FIDO2ENROLL_PIN)) return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "PIN required to unlock, but FIDO2 device %s does not support it.", path); if (!has_up && FLAGS_SET(required, FIDO2ENROLL_UP)) return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "User presence test required to unlock, but FIDO2 device %s does not support it.", path); if (!has_uv && FLAGS_SET(required, FIDO2ENROLL_UV)) return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "User verification required to unlock, but FIDO2 device %s does not support it.", path); a = sym_fido_assert_new(); if (!a) return log_oom(); r = sym_fido_assert_set_extensions(a, FIDO_EXT_HMAC_SECRET); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enable HMAC-SECRET extension on FIDO2 assertion: %s", sym_fido_strerr(r)); r = sym_fido_assert_set_hmac_salt(a, salt, salt_size); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set salt on FIDO2 assertion: %s", sym_fido_strerr(r)); r = fido2_assert_set_basic_properties(a, rp_id, cid, cid_size); if (r < 0) return r; log_info("Asking FIDO2 token for authentication."); if (has_up) { r = sym_fido_assert_set_up(a, FLAGS_SET(required, FIDO2ENROLL_UP) ? FIDO_OPT_TRUE : FIDO_OPT_FALSE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to %s FIDO2 user presence test: %s", enable_disable(FLAGS_SET(required, FIDO2ENROLL_UP)), sym_fido_strerr(r)); if (FLAGS_SET(required, FIDO2ENROLL_UP)) log_notice("%s%sPlease confirm presence on security token to unlock.", emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", emoji_enabled() ? " " : ""); } if (has_uv && !FLAGS_SET(required, FIDO2ENROLL_UV_OMIT)) { r = sym_fido_assert_set_uv(a, FLAGS_SET(required, FIDO2ENROLL_UV) ? FIDO_OPT_TRUE : FIDO_OPT_FALSE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to %s FIDO2 user verification: %s", enable_disable(FLAGS_SET(required, FIDO2ENROLL_UV)), sym_fido_strerr(r)); if (FLAGS_SET(required, FIDO2ENROLL_UV)) log_notice("%s%sPlease verify user on security token to unlock.", emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", emoji_enabled() ? " " : ""); } for (;;) { bool retry_with_up = false, retry_with_pin = false; if (FLAGS_SET(required, FIDO2ENROLL_PIN)) { /* OK, we need a pin, try with all pins in turn */ if (strv_isempty(pins)) r = FIDO_ERR_PIN_REQUIRED; else STRV_FOREACH(i, pins) { r = sym_fido_dev_get_assert(d, a, *i); if (r != FIDO_ERR_PIN_INVALID) break; } } else r = sym_fido_dev_get_assert(d, a, NULL); /* In some conditions, where a PIN or UP is required we might accept that. Let's check the * conditions and if so try immediately again. */ switch (r) { case FIDO_ERR_UP_REQUIRED: /* So the token asked for "up". Try to turn it on, for compat with systemd 248 and try again. */ if (!has_up) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Token asks for user presence test but doesn't advertise 'up' feature."); if (FLAGS_SET(required, FIDO2ENROLL_UP)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Token asks for user presence test but was already enabled."); if (FLAGS_SET(required, FIDO2ENROLL_UP_IF_NEEDED)) { log_notice("%s%sPlease confirm presence on security to unlock.", emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", emoji_enabled() ? " " : ""); retry_with_up = true; } break; case FIDO_ERR_UNSUPPORTED_OPTION: /* AuthenTrend ATKey.Pro returns this instead of FIDO_ERR_UP_REQUIRED, let's handle * it gracefully (also see below.) */ if (has_up && (required & (FIDO2ENROLL_UP|FIDO2ENROLL_UP_IF_NEEDED)) == FIDO2ENROLL_UP_IF_NEEDED) { log_notice("%s%sGot unsupported option error when user presence test is turned off. Trying with user presence test turned on.", emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", emoji_enabled() ? " " : ""); retry_with_up = true; } break; case FIDO_ERR_PIN_REQUIRED: /* A pin was requested. Maybe supply one, if we are configured to do so on request */ if (!has_client_pin) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Token asks for PIN but doesn't advertise 'clientPin' feature."); if (FLAGS_SET(required, FIDO2ENROLL_PIN) && !strv_isempty(pins)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Token asks for PIN but one was already supplied."); if ((required & (FIDO2ENROLL_PIN|FIDO2ENROLL_PIN_IF_NEEDED)) == FIDO2ENROLL_PIN_IF_NEEDED) { /* If a PIN so far wasn't specified but is requested by the device, and * FIDO2ENROLL_PIN_IF_NEEDED is set, then provide it */ log_debug("Retrying to create credential with PIN."); retry_with_pin = true; } break; default: break; } if (!retry_with_up && !retry_with_pin) break; if (retry_with_up) { r = sym_fido_assert_set_up(a, FIDO_OPT_TRUE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enable FIDO2 user presence test: %s", sym_fido_strerr(r)); required |= FIDO2ENROLL_UP; } if (retry_with_pin) required |= FIDO2ENROLL_PIN; } r = fido2_common_assert_error_handle(r); if (r < 0) return r; hmac = sym_fido_assert_hmac_secret_ptr(a, 0); if (!hmac) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve HMAC secret."); hmac_size = sym_fido_assert_hmac_secret_len(a, 0); hmac_copy = memdup(hmac, hmac_size); if (!hmac_copy) return log_oom(); *ret_hmac = TAKE_PTR(hmac_copy); *ret_hmac_size = hmac_size; return 0; } /* COSE_ECDH_ES256 is not usable with fido_cred_set_type() thus it's not listed here. */ static const char *fido2_algorithm_to_string(int alg) { switch(alg) { case COSE_ES256: return "es256"; case COSE_RS256: return "rs256"; case COSE_EDDSA: return "eddsa"; default: return NULL; } } int fido2_use_hmac_hash( const char *device, const char *rp_id, const void *salt, size_t salt_size, const void *cid, size_t cid_size, char **pins, Fido2EnrollFlags required, /* client pin/user presence required */ void **ret_hmac, size_t *ret_hmac_size) { size_t allocated = 64, found = 0; fido_dev_info_t *di = NULL; int r; r = dlopen_libfido2(); if (r < 0) return log_error_errno(r, "FIDO2 support is not installed."); if (device) { r = fido2_is_cred_in_specific_token(device, rp_id, cid, cid_size, required); if (r == 0) /* The caller is expected to attempt other key slots in this case, * therefore, do not spam the console with error logs here. */ return log_debug_errno(SYNTHETIC_ERRNO(EBADSLT), "The credential is not in the token %s.", device); if (r < 0) return log_error_errno(r, "Token returned error during pre-flight: %m"); return fido2_use_hmac_hash_specific_token(device, rp_id, salt, salt_size, cid, cid_size, pins, required, ret_hmac, ret_hmac_size); } di = sym_fido_dev_info_new(allocated); if (!di) return log_oom(); r = sym_fido_dev_info_manifest(di, allocated, &found); if (r == FIDO_ERR_INTERNAL) { /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ r = log_debug_errno(SYNTHETIC_ERRNO(EAGAIN), "Got FIDO_ERR_INTERNAL, assuming no devices."); goto finish; } if (r != FIDO_OK) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); goto finish; } for (size_t i = 0; i < found; i++) { const fido_dev_info_t *entry; const char *path; entry = sym_fido_dev_info_ptr(di, i); if (!entry) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get device information for FIDO device %zu.", i); goto finish; } path = sym_fido_dev_info_path(entry); if (!path) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to query FIDO device path."); goto finish; } r = fido2_is_cred_in_specific_token(path, rp_id, cid, cid_size, required); if (r < 0) { log_error_errno(r, "Token returned error during pre-flight: %m"); goto finish; } if (r == 0) { log_debug("The credential is not in the token %s, skipping.", path); continue; } r = fido2_use_hmac_hash_specific_token(path, rp_id, salt, salt_size, cid, cid_size, pins, required, ret_hmac, ret_hmac_size); if (!IN_SET(r, -EBADSLT, /* device doesn't understand our credential hash */ -ENODEV /* device is not a FIDO2 device with HMAC-SECRET */)) goto finish; } r = -EAGAIN; finish: sym_fido_dev_info_free(&di, allocated); return r; } #define FIDO2_SALT_SIZE 32 int fido2_generate_hmac_hash( const char *device, const char *rp_id, const char *rp_name, const void *user_id, size_t user_id_len, const char *user_name, const char *user_display_name, const char *user_icon, const char *askpw_icon_name, Fido2EnrollFlags lock_with, int cred_alg, void **ret_cid, size_t *ret_cid_size, void **ret_salt, size_t *ret_salt_size, void **ret_secret, size_t *ret_secret_size, char **ret_usedpin, Fido2EnrollFlags *ret_locked_with) { _cleanup_(erase_and_freep) void *salt = NULL, *secret_copy = NULL; _cleanup_(fido_assert_free_wrapper) fido_assert_t *a = NULL; _cleanup_(fido_cred_free_wrapper) fido_cred_t *c = NULL; _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL; _cleanup_(erase_and_freep) char *used_pin = NULL; bool has_rk, has_client_pin, has_up, has_uv; _cleanup_free_ char *cid_copy = NULL; size_t cid_size, secret_size; const void *cid, *secret; int r; assert(device); assert(ret_cid); assert(ret_cid_size); assert(ret_salt); assert(ret_salt_size); assert(ret_secret); assert(ret_secret_size); /* Construction is like this: we generate a salt of 32 bytes. We then ask the FIDO2 device to * HMAC-SHA256 it for us with its internal key. The result is the key used by LUKS and account * authentication. LUKS and UNIX password auth all do their own salting before hashing, so that FIDO2 * device never sees the volume key. * * S = HMAC-SHA256(I, D) * * with: S → LUKS/account authentication key (never stored) * I → internal key on FIDO2 device (stored in the FIDO2 device) * D → salt we generate here (stored in the privileged part of the JSON record) * */ assert(device); assert((lock_with & ~(FIDO2ENROLL_PIN|FIDO2ENROLL_UP|FIDO2ENROLL_UV)) == 0); r = dlopen_libfido2(); if (r < 0) return log_error_errno(r, "FIDO2 token support is not installed."); salt = malloc(FIDO2_SALT_SIZE); if (!salt) return log_oom(); r = crypto_random_bytes(salt, FIDO2_SALT_SIZE); if (r < 0) return log_error_errno(r, "Failed to generate salt: %m"); d = sym_fido_dev_new(); if (!d) return log_oom(); r = sym_fido_dev_open(d, device); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to open FIDO2 device %s: %s", device, sym_fido_strerr(r)); r = verify_features(d, device, LOG_ERR, &has_rk, &has_client_pin, &has_up, &has_uv); if (r < 0) return r; /* While enrolling degrade gracefully if the requested feature set isn't available, but let the user know */ if (!has_client_pin && FLAGS_SET(lock_with, FIDO2ENROLL_PIN)) { log_notice("Requested to lock with PIN, but FIDO2 device %s does not support it, disabling.", device); lock_with &= ~FIDO2ENROLL_PIN; } if (!has_up && FLAGS_SET(lock_with, FIDO2ENROLL_UP)) { log_notice("Locking with user presence test requested, but FIDO2 device %s does not support it, disabling.", device); lock_with &= ~FIDO2ENROLL_UP; } if (!has_uv && FLAGS_SET(lock_with, FIDO2ENROLL_UV)) { log_notice("Locking with user verification test requested, but FIDO2 device %s does not support it, disabling.", device); lock_with &= ~FIDO2ENROLL_UV; } c = sym_fido_cred_new(); if (!c) return log_oom(); r = sym_fido_cred_set_extensions(c, FIDO_EXT_HMAC_SECRET); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enable HMAC-SECRET extension on FIDO2 credential: %s", sym_fido_strerr(r)); r = sym_fido_cred_set_rp(c, rp_id, rp_name); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set FIDO2 credential relying party ID/name: %s", sym_fido_strerr(r)); r = sym_fido_cred_set_type(c, cred_alg); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set FIDO2 credential type to %s: %s", fido2_algorithm_to_string(cred_alg), sym_fido_strerr(r)); r = sym_fido_cred_set_user( c, user_id, user_id_len, user_name, user_display_name, user_icon); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set FIDO2 credential user data: %s", sym_fido_strerr(r)); r = sym_fido_cred_set_clientdata_hash(c, (const unsigned char[32]) {}, 32); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set FIDO2 client data hash: %s", sym_fido_strerr(r)); if (has_rk) { r = sym_fido_cred_set_rk(c, FIDO_OPT_FALSE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to turn off FIDO2 resident key option of credential: %s", sym_fido_strerr(r)); } if (has_uv) { r = sym_fido_cred_set_uv(c, FIDO_OPT_FALSE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to turn off FIDO2 user verification option of credential: %s", sym_fido_strerr(r)); } /* As per specification "up" is assumed to be implicit when making credentials, hence we don't * explicitly enable/disable it here */ log_info("Initializing FIDO2 credential on security token."); if (has_uv || has_up) log_notice("%s%s(Hint: This might require confirmation of user presence on security token.)", emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", emoji_enabled() ? " " : ""); r = sym_fido_dev_make_cred(d, c, NULL); if (r == FIDO_ERR_PIN_REQUIRED) { if (!has_client_pin) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Token asks for PIN but doesn't advertise 'clientPin' feature."); for (;;) { _cleanup_(strv_free_erasep) char **pin = NULL; r = ask_password_auto("Please enter security token PIN:", askpw_icon_name, NULL, "fido2-pin", "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_notice("PIN may not be empty."); continue; } r = sym_fido_dev_make_cred(d, c, *i); if (r == FIDO_OK) { used_pin = strdup(*i); if (!used_pin) return log_oom(); break; } if (r != FIDO_ERR_PIN_INVALID) break; } if (r != FIDO_ERR_PIN_INVALID) break; log_notice("PIN incorrect, please try again."); } } if (r == FIDO_ERR_PIN_AUTH_BLOCKED) return log_notice_errno(SYNTHETIC_ERRNO(EPERM), "Token PIN is currently blocked, please remove and reinsert token."); #ifdef FIDO_ERR_UV_BLOCKED if (r == FIDO_ERR_UV_BLOCKED) return log_notice_errno(SYNTHETIC_ERRNO(EPERM), "Token verification is currently blocked, please remove and reinsert token."); #endif 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_ERR_UNSUPPORTED_ALGORITHM) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Token doesn't support credential algorithm %s.", fido2_algorithm_to_string(cred_alg)); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to generate FIDO2 credential: %s", sym_fido_strerr(r)); cid = sym_fido_cred_id_ptr(c); if (!cid) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get FIDO2 credential ID."); cid_size = sym_fido_cred_id_len(c); a = sym_fido_assert_new(); if (!a) return log_oom(); r = sym_fido_assert_set_extensions(a, FIDO_EXT_HMAC_SECRET); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enable HMAC-SECRET extension on FIDO2 assertion: %s", sym_fido_strerr(r)); r = sym_fido_assert_set_hmac_salt(a, salt, FIDO2_SALT_SIZE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to set salt on FIDO2 assertion: %s", sym_fido_strerr(r)); r = fido2_assert_set_basic_properties(a, rp_id, cid, cid_size); if (r < 0) return r; log_info("Generating secret key on FIDO2 security token."); if (has_up) { r = sym_fido_assert_set_up(a, FLAGS_SET(lock_with, FIDO2ENROLL_UP) ? FIDO_OPT_TRUE : FIDO_OPT_FALSE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to %s FIDO2 user presence test: %s", enable_disable(FLAGS_SET(lock_with, FIDO2ENROLL_UP)), sym_fido_strerr(r)); if (FLAGS_SET(lock_with, FIDO2ENROLL_UP)) log_notice("%s%sIn order to allow secret key generation, please confirm presence on security token.", emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", emoji_enabled() ? " " : ""); } if (has_uv) { r = sym_fido_assert_set_uv(a, FLAGS_SET(lock_with, FIDO2ENROLL_UV) ? FIDO_OPT_TRUE : FIDO_OPT_FALSE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to %s FIDO user verification: %s", enable_disable(FLAGS_SET(lock_with, FIDO2ENROLL_UV)), sym_fido_strerr(r)); if (FLAGS_SET(lock_with, FIDO2ENROLL_UV)) log_notice("%s%sIn order to allow secret key generation, please verify user on security token.", emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", emoji_enabled() ? " " : ""); } for (;;) { bool retry_with_up = false, retry_with_pin = false; r = sym_fido_dev_get_assert(d, a, FLAGS_SET(lock_with, FIDO2ENROLL_PIN) ? used_pin : NULL); switch (r) { case FIDO_ERR_UP_REQUIRED: /* If the token asks for "up" when we turn off, then this might be a feature that * isn't optional. Let's enable it */ if (!has_up) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Token asks for user presence test but doesn't advertise 'up' feature."); if (FLAGS_SET(lock_with, FIDO2ENROLL_UP)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Token asks for user presence test but was already enabled."); log_notice("%s%sLocking without user presence test requested, but FIDO2 device %s requires it, enabling.", emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", emoji_enabled() ? " " : "", device); retry_with_up = true; break; case FIDO_ERR_UNSUPPORTED_OPTION: /* AuthenTrend ATKey.Pro says it supports "up", but if we disable it it will fail * with FIDO_ERR_UNSUPPORTED_OPTION, probably because it isn't actually * optional. Let's see if turning it on works. This is very similar to the * FIDO_ERR_UP_REQUIRED case, but since the error is so vague we implement it * slightly more defensively. */ if (has_up && !FLAGS_SET(lock_with, FIDO2ENROLL_UP)) { log_notice("%s%sGot unsupported option error when user presence test is turned off. Trying with user presence test turned on.", emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "", emoji_enabled() ? " " : ""); retry_with_up = true; } break; case FIDO_ERR_PIN_REQUIRED: if (!has_client_pin) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Token asks for client PIN check but doesn't advertise 'clientPin' feature."); if (FLAGS_SET(lock_with, FIDO2ENROLL_PIN)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Token asks for user client PIN check but was already enabled."); log_debug("Token requires PIN for assertion, enabling."); retry_with_pin = true; break; default: break; } if (!retry_with_up && !retry_with_pin) break; if (retry_with_up) { r = sym_fido_assert_set_up(a, FIDO_OPT_TRUE); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enable FIDO2 user presence test: %s", sym_fido_strerr(r)); lock_with |= FIDO2ENROLL_UP; } if (retry_with_pin) lock_with |= FIDO2ENROLL_PIN; } if (r == FIDO_ERR_ACTION_TIMEOUT) return log_error_errno(SYNTHETIC_ERRNO(ENOSTR), "Token action timeout. (User didn't interact with token quickly enough.)"); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to ask token for assertion: %s", sym_fido_strerr(r)); secret = sym_fido_assert_hmac_secret_ptr(a, 0); if (!secret) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve HMAC secret."); secret_size = sym_fido_assert_hmac_secret_len(a, 0); secret_copy = memdup(secret, secret_size); if (!secret_copy) return log_oom(); cid_copy = memdup(cid, cid_size); if (!cid_copy) return log_oom(); *ret_cid = TAKE_PTR(cid_copy); *ret_cid_size = cid_size; *ret_salt = TAKE_PTR(salt); *ret_salt_size = FIDO2_SALT_SIZE; *ret_secret = TAKE_PTR(secret_copy); *ret_secret_size = secret_size; if (ret_usedpin) *ret_usedpin = TAKE_PTR(used_pin); if (ret_locked_with) *ret_locked_with = lock_with; return 0; } #endif #if HAVE_LIBFIDO2 static int check_device_is_fido2_with_hmac_secret(const char *path) { _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL; int r; d = sym_fido_dev_new(); if (!d) return log_oom(); r = sym_fido_dev_open(d, path); if (r != FIDO_OK) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to open FIDO2 device %s: %s", path, sym_fido_strerr(r)); r = verify_features(d, path, LOG_DEBUG, NULL, NULL, NULL, NULL); if (r == -ENODEV) /* Not a FIDO2 device, or not implementing 'hmac-secret' */ return false; if (r < 0) return r; return true; } #endif int fido2_list_devices(void) { #if HAVE_LIBFIDO2 _cleanup_(table_unrefp) Table *t = NULL; size_t allocated = 64, found = 0; fido_dev_info_t *di = NULL; int r; r = dlopen_libfido2(); if (r < 0) return log_error_errno(r, "FIDO2 token support is not installed."); di = sym_fido_dev_info_new(allocated); if (!di) return log_oom(); r = sym_fido_dev_info_manifest(di, allocated, &found); if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) { /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ log_info("No FIDO2 devices found."); r = 0; goto finish; } if (r != FIDO_OK) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); goto finish; } t = table_new("path", "manufacturer", "product"); if (!t) { r = log_oom(); goto finish; } for (size_t i = 0; i < found; i++) { const fido_dev_info_t *entry; entry = sym_fido_dev_info_ptr(di, i); if (!entry) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get device information for FIDO device %zu.", i); goto finish; } r = check_device_is_fido2_with_hmac_secret(sym_fido_dev_info_path(entry)); if (r < 0) goto finish; if (!r) continue; r = table_add_many( t, TABLE_PATH, sym_fido_dev_info_path(entry), TABLE_STRING, sym_fido_dev_info_manufacturer_string(entry), TABLE_STRING, sym_fido_dev_info_product_string(entry)); if (r < 0) { table_log_add_error(r); goto finish; } } r = table_print(t, stdout); if (r < 0) { log_error_errno(r, "Failed to show device table: %m"); goto finish; } r = 0; finish: sym_fido_dev_info_free(&di, allocated); return r; #else return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "FIDO2 tokens not supported on this build."); #endif } int fido2_find_device_auto(char **ret) { #if HAVE_LIBFIDO2 _cleanup_free_ char *copy = NULL; size_t di_size = 64, found = 0; const fido_dev_info_t *entry; fido_dev_info_t *di = NULL; const char *path; int r; r = dlopen_libfido2(); if (r < 0) return log_error_errno(r, "FIDO2 token support is not installed."); di = sym_fido_dev_info_new(di_size); if (!di) return log_oom(); r = sym_fido_dev_info_manifest(di, di_size, &found); if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) { /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ r = log_error_errno(SYNTHETIC_ERRNO(ENODEV), "No FIDO devices found."); goto finish; } if (r != FIDO_OK) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO devices: %s", sym_fido_strerr(r)); goto finish; } if (found > 1) { r = log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "More than one FIDO device found."); goto finish; } entry = sym_fido_dev_info_ptr(di, 0); if (!entry) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get device information for FIDO device 0."); goto finish; } r = check_device_is_fido2_with_hmac_secret(sym_fido_dev_info_path(entry)); if (r < 0) goto finish; if (!r) { r = log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "FIDO device discovered does not implement FIDO2 with 'hmac-secret' extension."); goto finish; } path = sym_fido_dev_info_path(entry); if (!path) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to query FIDO device path."); goto finish; } copy = strdup(path); if (!copy) { r = log_oom(); goto finish; } *ret = TAKE_PTR(copy); r = 0; finish: sym_fido_dev_info_free(&di, di_size); return r; #else return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "FIDO2 tokens not supported on this build."); #endif } int fido2_have_device(const char *device) { #if HAVE_LIBFIDO2 size_t allocated = 64, found = 0; fido_dev_info_t *di = NULL; int r; /* Return == 0 if not devices are found, > 0 if at least one is found */ r = dlopen_libfido2(); if (r < 0) return log_error_errno(r, "FIDO2 support is not installed."); if (device) { if (access(device, F_OK) < 0) { if (errno == ENOENT) return 0; return log_error_errno(errno, "Failed to determine whether device '%s' exists: %m", device); } return 1; } di = sym_fido_dev_info_new(allocated); if (!di) return log_oom(); r = sym_fido_dev_info_manifest(di, allocated, &found); if (r == FIDO_ERR_INTERNAL) { /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */ r = 0; goto finish; } if (r != FIDO_OK) { r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r)); goto finish; } r = found; finish: sym_fido_dev_info_free(&di, allocated); return r; #else return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "FIDO2 tokens not supported on this build."); #endif } #if HAVE_LIBFIDO2 int parse_fido2_algorithm(const char *s, int *ret) { int a; assert(s); if (streq(s, "es256")) a = COSE_ES256; else if (streq(s, "rs256")) a = COSE_RS256; else if (streq(s, "eddsa")) a = COSE_EDDSA; else return -EINVAL; if (ret) *ret = a; return 0; } #endif