summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO4
-rw-r--r--docs/USER_RECORD.md54
-rw-r--r--man/homectl.xml62
-rw-r--r--meson.build16
-rw-r--r--meson_options.txt2
-rw-r--r--src/basic/locale-util.c6
-rw-r--r--src/basic/locale-util.h3
-rw-r--r--src/basic/macro.h6
-rw-r--r--src/home/homectl-fido2.c539
-rw-r--r--src/home/homectl-fido2.h10
-rw-r--r--src/home/homectl-pkcs11.c480
-rw-r--r--src/home/homectl-pkcs11.h11
-rw-r--r--src/home/homectl.c536
-rw-r--r--src/home/homed-home.c12
-rw-r--r--src/home/homework-cifs.c4
-rw-r--r--src/home/homework-cifs.h2
-rw-r--r--src/home/homework-directory.c12
-rw-r--r--src/home/homework-directory.h4
-rw-r--r--src/home/homework-fido2.c197
-rw-r--r--src/home/homework-fido2.h6
-rw-r--r--src/home/homework-fscrypt.c25
-rw-r--r--src/home/homework-fscrypt.h4
-rw-r--r--src/home/homework-luks.c100
-rw-r--r--src/home/homework-luks.h12
-rw-r--r--src/home/homework-pkcs11.c6
-rw-r--r--src/home/homework.c250
-rw-r--r--src/home/homework.h16
-rw-r--r--src/home/meson.build8
-rw-r--r--src/home/pam_systemd_home.c23
-rw-r--r--src/home/user-record-util.c56
-rw-r--r--src/home/user-record-util.h3
-rw-r--r--src/libsystemd/sd-bus/bus-common-errors.c2
-rw-r--r--src/libsystemd/sd-bus/bus-common-errors.h2
-rw-r--r--src/shared/pkcs11-util.c81
-rw-r--r--src/shared/pkcs11-util.h2
-rw-r--r--src/shared/user-record-show.c5
-rw-r--r--src/shared/user-record.c437
-rw-r--r--src/shared/user-record.h25
-rw-r--r--src/test/test-locale-util.c3
-rw-r--r--src/test/test-util.c80
40 files changed, 2360 insertions, 746 deletions
diff --git a/TODO b/TODO
index 6751bd96aa..2ef8035099 100644
--- a/TODO
+++ b/TODO
@@ -150,8 +150,8 @@ Features:
thus allows defining OS images which can be A/B updated and we default to the
newest version automatically, both in nspawn and in sd-boot
-* cryptsetup/homed: also support FIDO2 HMAC password logic for unlocking
- devices. (see: https://github.com/mjec/fido2-hmac-secret)
+* cryptsetup: support FIDO2 tokens for deriving keys (i.e. do what homed can do
+ also in plain cryptsetup)
* systemd-gpt-auto should probably set x-systemd.growfs on the mounts it
creates
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.</para></listitem>
+ token.</para>
+
+ <para>Instead of a valid PKCS#11 URI, the special strings <literal>list</literal> and
+ <literal>auto</literal> may be specified. If <literal>list</literal> is passed, a brief table of
+ suitable, currently plugged in PKCS#11 hardware tokens is shown, along with their URIs. If
+ <literal>auto</literal> 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.</para>
+
+ <para>Note that many hardware security tokens implement both PKCS#11/PIV and FIDO2 with the
+ <literal>hmac-secret</literal> extension (for example: the YubiKey 5 series), as supported with the
+ <option>--fido2-device=</option> 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.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>--fido2-device=</option><replaceable>PATH</replaceable></term>
+
+ <listitem><para>Takes a path to a Linux <literal>hidraw</literal> device
+ (e.g. <filename>/dev/hidraw1</filename>), referring to a FIDO2 security token implementing the
+ <literal>hmac-secret</literal> 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.</para>
+
+ <para>Instead of a valid path to a FIDO2 <literal>hidraw</literal> device the special strings
+ <literal>list</literal> and <literal>auto</literal> may be specified. If <literal>list</literal> is
+ passed, a brief table of suitable discovered FIDO2 devices is shown. If <literal>auto</literal> 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.</para>
+
+ <para>Note that FIDO2 devices suitable for this option must implement the
+ <literal>hmac-secret</literal> 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.</para>
+
+ <para>Note that many hardware security tokens implement both FIDO2 and PKCS#11/PIV (and thus may be
+ used with either <option>--fido2-device=</option> or <option>--pkcs11-token-uri=</option>), for a
+ discussion see above.</para></listitem>
</varlistentry>
<varlistentry>
@@ -810,7 +852,7 @@
</example>
<example>
- <title>Set up authentication with a YubiKey security token:</title>
+ <title>Set up authentication with a YubiKey security token using PKCS#11/PIV:</title>
<programlisting># 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</programlisting>
+ </example>
+
+ <example>
+ <title>Set up authentication with a FIDO2 security token:</title>
-# 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=…</programlisting>
+ <programlisting># Allow a FIDO2 security token to unlock the account of user 'nihilbaxter'.
+homectl update nihilbaxter --fido2-device=auto</programlisting>
</example>
</refsect1>
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 <fido.h>
+#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));
-
- 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.)");
- } else
- break; /* done */
+ 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.)");
- r = user_record_set_hashed_password(hr, original_hashed_passwords);
- if (r < 0)
- return r;
+ 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 = 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");
+ 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 */
}
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 = 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);
+ 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 <fido.h>
+
+#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<nr>' where '<nr>' 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 },
-
- { "secret", JSON_VARIANT_OBJECT, dispatch_secret, 0, 0 },
- { "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 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 },
/* 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;
}