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 diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md index 7b6fe47665..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. @@ -864,13 +885,20 @@ 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 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` diff --git a/man/homectl.xml b/man/homectl.xml index c5d9630632..134a60bb97 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 diff --git a/meson.build b/meson.build index 08f322117f..fc75fddf6b 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', @@ -2146,7 +2157,8 @@ if conf.get('ENABLE_HOMED') == 1 libcrypt, libopenssl, libfdisk, - libp11kit], + libp11kit, + libfido2], install_rpath : rootlibexecdir, install : true, install_dir : rootlibexecdir) @@ -2173,6 +2185,7 @@ if conf.get('ENABLE_HOMED') == 1 libcrypt, libopenssl, libp11kit, + libfido2, libpwquality], install_rpath : rootlibexecdir, install : true, @@ -3575,6 +3588,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'], 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/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/home/homectl-fido2.c b/src/home/homectl-fido2.c new file mode 100644 index 0000000000..b7b2c1a3b5 --- /dev/null +++ b/src/home/homectl-fido2.c @@ -0,0 +1,539 @@ +/* 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 "homectl-pkcs11.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; + + /* 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."); +#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-pkcs11.c b/src/home/homectl-pkcs11.c new file mode 100644 index 0000000000..830aafaab1 --- /dev/null +++ b/src/home/homectl-pkcs11.c @@ -0,0 +1,480 @@ +/* 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" +#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; +} + +#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 new file mode 100644 index 0000000000..0403c73ea1 --- /dev/null +++ b/src/home/homectl-pkcs11.h @@ -0,0 +1,11 @@ +/* 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); + +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 47a506f5e0..74c967eb26 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,15 +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 "libcrypt-util.h" +#include "homectl-fido2.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" @@ -31,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" @@ -56,6 +51,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 +69,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 +80,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) { @@ -236,7 +234,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 +245,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 +261,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,26 +313,38 @@ 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; } 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) 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)) { 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 +352,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 +360,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 @@ -889,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_pkcs11_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, "pkcs11Pin")); - - 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, "pkcs11Pin", 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_pkcs11_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; @@ -1246,7 +926,13 @@ 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; + } + + STRV_FOREACH(i, arg_fido2_device) { + r = identity_add_fido2_parameters(&v, *i); if (r < 0) return r; } @@ -1423,7 +1109,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) @@ -1437,25 +1123,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; @@ -1566,14 +1255,20 @@ 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; + } + + 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; @@ -1592,6 +1287,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; @@ -1620,6 +1335,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; @@ -1631,7 +1352,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); @@ -1655,13 +1376,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); @@ -1688,13 +1412,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); @@ -1732,6 +1459,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 +1911,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 +2059,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 +2137,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 }, {} @@ -3365,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); @@ -3377,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; @@ -3388,6 +3133,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 +3238,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/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-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/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 2c5664aae1..797f3a3c6d 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 @@ -65,6 +69,10 @@ systemd_homed_sources += [homed_gperf_c] homectl_sources = files(''' home-util.c home-util.h + homectl-fido2.c + homectl-fido2.h + homectl-pkcs11.c + homectl-pkcs11.h homectl.c pwquality-util.c pwquality-util.h diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c index dcf26ddaf5..2c2c7a0819 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; @@ -375,6 +375,21 @@ 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_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; @@ -388,7 +403,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 +422,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 +441,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 0881d1472c..5d0ac86533 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; @@ -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; @@ -1062,12 +1090,22 @@ 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; 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 6afc8df19a..2458298825 100644 --- a/src/home/user-record-util.h +++ b/src/home/user-record-util.h @@ -47,8 +47,9 @@ 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_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/pkcs11-util.c b/src/shared/pkcs11-util.c index b4a7d86afe..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); @@ -211,28 +232,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 +244,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) @@ -702,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) @@ -740,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. */ @@ -786,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; } @@ -820,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 @@ -883,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); 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 83d86f69e7..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; @@ -95,6 +96,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; @@ -120,7 +137,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); @@ -147,6 +164,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); @@ -620,8 +642,10 @@ 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 }, + { "fido2UserPresencePermitted", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, fido2_user_presence_permitted), 0 }, {}, }; @@ -706,7 +730,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; } @@ -766,13 +790,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 }, {}, }; @@ -906,66 +1058,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 }, {}, }; @@ -1247,84 +1400,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 }, {}, }; @@ -1684,6 +1838,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 9fd10610d9..e75f0ff00b 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; @@ -239,7 +256,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; @@ -309,6 +326,12 @@ 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; + int fido2_user_presence_permitted; + JsonVariant *json; } UserRecord; 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[]) { 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; }