]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/home/homectl-fido2.c
homed: turn libfido2 into a dlopen() type dependency
[thirdparty/systemd.git] / src / home / homectl-fido2.c
1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3 #if HAVE_LIBFIDO2
4 #include <fido.h>
5 #endif
6
7 #include "ask-password-api.h"
8 #include "errno-util.h"
9 #include "format-table.h"
10 #include "hexdecoct.h"
11 #include "homectl-fido2.h"
12 #include "homectl-pkcs11.h"
13 #include "libcrypt-util.h"
14 #include "libfido2-util.h"
15 #include "locale-util.h"
16 #include "memory-util.h"
17 #include "random-util.h"
18 #include "strv.h"
19
20 #if HAVE_LIBFIDO2
21 static int add_fido2_credential_id(
22 JsonVariant **v,
23 const void *cid,
24 size_t cid_size) {
25
26 _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
27 _cleanup_strv_free_ char **l = NULL;
28 _cleanup_free_ char *escaped = NULL;
29 int r;
30
31 assert(v);
32 assert(cid);
33
34 r = base64mem(cid, cid_size, &escaped);
35 if (r < 0)
36 return log_error_errno(r, "Failed to base64 encode FIDO2 credential ID: %m");
37
38 w = json_variant_ref(json_variant_by_key(*v, "fido2HmacCredential"));
39 if (w) {
40 r = json_variant_strv(w, &l);
41 if (r < 0)
42 return log_error_errno(r, "Failed to parse FIDO2 credential ID list: %m");
43
44 if (strv_contains(l, escaped))
45 return 0;
46 }
47
48 r = strv_extend(&l, escaped);
49 if (r < 0)
50 return log_oom();
51
52 w = json_variant_unref(w);
53 r = json_variant_new_array_strv(&w, l);
54 if (r < 0)
55 return log_error_errno(r, "Failed to create FIDO2 credential ID JSON: %m");
56
57 r = json_variant_set_field(v, "fido2HmacCredential", w);
58 if (r < 0)
59 return log_error_errno(r, "Failed to update FIDO2 credential ID: %m");
60
61 return 0;
62 }
63
64 static int add_fido2_salt(
65 JsonVariant **v,
66 const void *cid,
67 size_t cid_size,
68 const void *fido2_salt,
69 size_t fido2_salt_size,
70 const void *secret,
71 size_t secret_size) {
72
73 _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL;
74 _cleanup_(erase_and_freep) char *base64_encoded = NULL, *hashed = NULL;
75 int r;
76
77 /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends
78 * expect a NUL terminated string, and we use a binary key */
79 r = base64mem(secret, secret_size, &base64_encoded);
80 if (r < 0)
81 return log_error_errno(r, "Failed to base64 encode secret key: %m");
82
83 r = hash_password(base64_encoded, &hashed);
84 if (r < 0)
85 return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m");
86
87 r = json_build(&e, JSON_BUILD_OBJECT(
88 JSON_BUILD_PAIR("credential", JSON_BUILD_BASE64(cid, cid_size)),
89 JSON_BUILD_PAIR("salt", JSON_BUILD_BASE64(fido2_salt, fido2_salt_size)),
90 JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(hashed))));
91 if (r < 0)
92 return log_error_errno(r, "Failed to build FIDO2 salt JSON key object: %m");
93
94 w = json_variant_ref(json_variant_by_key(*v, "privileged"));
95 l = json_variant_ref(json_variant_by_key(w, "fido2HmacSalt"));
96
97 r = json_variant_append_array(&l, e);
98 if (r < 0)
99 return log_error_errno(r, "Failed append FIDO2 salt: %m");
100
101 r = json_variant_set_field(&w, "fido2HmacSalt", l);
102 if (r < 0)
103 return log_error_errno(r, "Failed to set FDO2 salt: %m");
104
105 r = json_variant_set_field(v, "privileged", w);
106 if (r < 0)
107 return log_error_errno(r, "Failed to update privileged field: %m");
108
109 return 0;
110 }
111 #endif
112
113 #define FIDO2_SALT_SIZE 32
114
115 int identity_add_fido2_parameters(
116 JsonVariant **v,
117 const char *device) {
118
119 #if HAVE_LIBFIDO2
120 _cleanup_(fido_cbor_info_free_wrapper) fido_cbor_info_t *di = NULL;
121 _cleanup_(fido_assert_free_wrapper) fido_assert_t *a = NULL;
122 _cleanup_(fido_cred_free_wrapper) fido_cred_t *c = NULL;
123 _cleanup_(fido_dev_free_wrapper) fido_dev_t *d = NULL;
124 _cleanup_(erase_and_freep) char *used_pin = NULL;
125 _cleanup_(erase_and_freep) void *salt = NULL;
126 JsonVariant *un, *realm, *rn;
127 bool found_extension = false;
128 const void *cid, *secret;
129 const char *fido_un;
130 size_t n, cid_size, secret_size;
131 char **e;
132 int r;
133
134 /* Construction is like this: we generate a salt of 32 bytes. We then ask the FIDO2 device to
135 * HMAC-SHA256 it for us with its internal key. The result is the key used by LUKS and account
136 * authentication. LUKS and UNIX password auth all do their own salting before hashing, so that FIDO2
137 * device never sees the volume key.
138 *
139 * S = HMAC-SHA256(I, D)
140 *
141 * with: S → LUKS/account authentication key (never stored)
142 * I → internal key on FIDO2 device (stored in the FIDO2 device)
143 * D → salt we generate here (stored in the privileged part of the JSON record)
144 *
145 */
146
147 assert(v);
148 assert(device);
149
150 r = dlopen_libfido2();
151 if (r < 0)
152 return log_error_errno(r, "FIDO2 token support is not installed.");
153
154 salt = malloc(FIDO2_SALT_SIZE);
155 if (!salt)
156 return log_oom();
157
158 r = genuine_random_bytes(salt, FIDO2_SALT_SIZE, RANDOM_BLOCK);
159 if (r < 0)
160 return log_error_errno(r, "Failed to generate salt: %m");
161
162 d = sym_fido_dev_new();
163 if (!d)
164 return log_oom();
165
166 r = sym_fido_dev_open(d, device);
167 if (r != FIDO_OK)
168 return log_error_errno(SYNTHETIC_ERRNO(EIO),
169 "Failed to open FIDO2 device %s: %s", device, sym_fido_strerr(r));
170
171 if (!sym_fido_dev_is_fido2(d))
172 return log_error_errno(SYNTHETIC_ERRNO(ENODEV),
173 "Specified device %s is not a FIDO2 device.", device);
174
175 di = sym_fido_cbor_info_new();
176 if (!di)
177 return log_oom();
178
179 r = sym_fido_dev_get_cbor_info(d, di);
180 if (r != FIDO_OK)
181 return log_error_errno(SYNTHETIC_ERRNO(EIO),
182 "Failed to get CBOR device info for %s: %s", device, sym_fido_strerr(r));
183
184 e = sym_fido_cbor_info_extensions_ptr(di);
185 n = sym_fido_cbor_info_extensions_len(di);
186
187 for (size_t i = 0; i < n; i++)
188 if (streq(e[i], "hmac-secret")) {
189 found_extension = true;
190 break;
191 }
192
193 if (!found_extension)
194 return log_error_errno(SYNTHETIC_ERRNO(ENODEV),
195 "Specified device %s is a FIDO2 device, but does not support the required HMAC-SECRET extension.", device);
196
197 c = sym_fido_cred_new();
198 if (!c)
199 return log_oom();
200
201 r = sym_fido_cred_set_extensions(c, FIDO_EXT_HMAC_SECRET);
202 if (r != FIDO_OK)
203 return log_error_errno(SYNTHETIC_ERRNO(EIO),
204 "Failed to enable HMAC-SECRET extension on FIDO2 credential: %s", sym_fido_strerr(r));
205
206 r = sym_fido_cred_set_rp(c, "io.systemd.home", "Home Directory");
207 if (r != FIDO_OK)
208 return log_error_errno(SYNTHETIC_ERRNO(EIO),
209 "Failed to set FIDO2 credential relying party ID/name: %s", sym_fido_strerr(r));
210
211 r = sym_fido_cred_set_type(c, COSE_ES256);
212 if (r != FIDO_OK)
213 return log_error_errno(SYNTHETIC_ERRNO(EIO),
214 "Failed to set FIDO2 credential type to ES256: %s", sym_fido_strerr(r));
215
216 un = json_variant_by_key(*v, "userName");
217 if (!un)
218 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
219 "userName field of user record is missing");
220 if (!json_variant_is_string(un))
221 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
222 "userName field of user record is not a string");
223
224 realm = json_variant_by_key(*v, "realm");
225 if (realm) {
226 if (!json_variant_is_string(realm))
227 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
228 "realm field of user record is not a string");
229
230 fido_un = strjoina(json_variant_string(un), json_variant_string(realm));
231 } else
232 fido_un = json_variant_string(un);
233
234 rn = json_variant_by_key(*v, "realName");
235 if (rn && !json_variant_is_string(rn))
236 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
237 "realName field of user record is not a string");
238
239 r = sym_fido_cred_set_user(c,
240 (const unsigned char*) fido_un, strlen(fido_un), /* We pass the user ID and name as the same */
241 fido_un,
242 rn ? json_variant_string(rn) : NULL,
243 NULL /* icon URL */);
244 if (r != FIDO_OK)
245 return log_error_errno(SYNTHETIC_ERRNO(EIO),
246 "Failed to set FIDO2 credential user data: %s", sym_fido_strerr(r));
247
248 r = sym_fido_cred_set_clientdata_hash(c, (const unsigned char[32]) {}, 32);
249 if (r != FIDO_OK)
250 return log_error_errno(SYNTHETIC_ERRNO(EIO),
251 "Failed to set FIDO2 client data hash: %s", sym_fido_strerr(r));
252
253 r = sym_fido_cred_set_rk(c, FIDO_OPT_FALSE);
254 if (r != FIDO_OK)
255 return log_error_errno(SYNTHETIC_ERRNO(EIO),
256 "Failed to turn off FIDO2 resident key option of credential: %s", sym_fido_strerr(r));
257
258 r = sym_fido_cred_set_uv(c, FIDO_OPT_FALSE);
259 if (r != FIDO_OK)
260 return log_error_errno(SYNTHETIC_ERRNO(EIO),
261 "Failed to turn off FIDO2 user verification option of credential: %s", sym_fido_strerr(r));
262
263 log_info("Initializing FIDO2 credential on security token.");
264
265 log_notice("%s%s(Hint: This might require verification of user presence on security token.)",
266 emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "",
267 emoji_enabled() ? " " : "");
268
269 r = sym_fido_dev_make_cred(d, c, NULL);
270 if (r == FIDO_ERR_PIN_REQUIRED) {
271 _cleanup_free_ char *text = NULL;
272
273 if (asprintf(&text, "Please enter security token PIN:") < 0)
274 return log_oom();
275
276 for (;;) {
277 _cleanup_(strv_free_erasep) char **pin = NULL;
278 char **i;
279
280 r = ask_password_auto(text, "user-home", NULL, "fido2-pin", USEC_INFINITY, 0, &pin);
281 if (r < 0)
282 return log_error_errno(r, "Failed to acquire user PIN: %m");
283
284 r = FIDO_ERR_PIN_INVALID;
285 STRV_FOREACH(i, pin) {
286 if (isempty(*i)) {
287 log_info("PIN may not be empty.");
288 continue;
289 }
290
291 r = sym_fido_dev_make_cred(d, c, *i);
292 if (r == FIDO_OK) {
293 used_pin = strdup(*i);
294 if (!used_pin)
295 return log_oom();
296 break;
297 }
298 if (r != FIDO_ERR_PIN_INVALID)
299 break;
300 }
301
302 if (r != FIDO_ERR_PIN_INVALID)
303 break;
304
305 log_notice("PIN incorrect, please try again.");
306 }
307 }
308 if (r == FIDO_ERR_PIN_AUTH_BLOCKED)
309 return log_notice_errno(SYNTHETIC_ERRNO(EPERM),
310 "Token PIN is currently blocked, please remove and reinsert token.");
311 if (r == FIDO_ERR_ACTION_TIMEOUT)
312 return log_error_errno(SYNTHETIC_ERRNO(ENOSTR),
313 "Token action timeout. (User didn't interact with token quickly enough.)");
314 if (r != FIDO_OK)
315 return log_error_errno(SYNTHETIC_ERRNO(EIO),
316 "Failed to generate FIDO2 credential: %s", sym_fido_strerr(r));
317
318 cid = sym_fido_cred_id_ptr(c);
319 if (!cid)
320 return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to get FIDO2 credential ID.");
321
322 cid_size = sym_fido_cred_id_len(c);
323
324 a = sym_fido_assert_new();
325 if (!a)
326 return log_oom();
327
328 r = sym_fido_assert_set_extensions(a, FIDO_EXT_HMAC_SECRET);
329 if (r != FIDO_OK)
330 return log_error_errno(SYNTHETIC_ERRNO(EIO),
331 "Failed to enable HMAC-SECRET extension on FIDO2 assertion: %s", sym_fido_strerr(r));
332
333 r = sym_fido_assert_set_hmac_salt(a, salt, FIDO2_SALT_SIZE);
334 if (r != FIDO_OK)
335 return log_error_errno(SYNTHETIC_ERRNO(EIO),
336 "Failed to set salt on FIDO2 assertion: %s", sym_fido_strerr(r));
337
338 r = sym_fido_assert_set_rp(a, "io.systemd.home");
339 if (r != FIDO_OK)
340 return log_error_errno(SYNTHETIC_ERRNO(EIO),
341 "Failed to set FIDO2 assertion ID: %s", sym_fido_strerr(r));
342
343 r = sym_fido_assert_set_clientdata_hash(a, (const unsigned char[32]) {}, 32);
344 if (r != FIDO_OK)
345 return log_error_errno(SYNTHETIC_ERRNO(EIO),
346 "Failed to set FIDO2 assertion client data hash: %s", sym_fido_strerr(r));
347
348 r = sym_fido_assert_allow_cred(a, cid, cid_size);
349 if (r != FIDO_OK)
350 return log_error_errno(SYNTHETIC_ERRNO(EIO),
351 "Failed to add FIDO2 assertion credential ID: %s", sym_fido_strerr(r));
352
353 r = sym_fido_assert_set_up(a, FIDO_OPT_FALSE);
354 if (r != FIDO_OK)
355 return log_error_errno(SYNTHETIC_ERRNO(EIO),
356 "Failed to turn off FIDO2 assertion user presence: %s", sym_fido_strerr(r));
357
358 log_info("Generating secret key on FIDO2 security token.");
359
360 r = sym_fido_dev_get_assert(d, a, used_pin);
361 if (r == FIDO_ERR_UP_REQUIRED) {
362 r = sym_fido_assert_set_up(a, FIDO_OPT_TRUE);
363 if (r != FIDO_OK)
364 return log_error_errno(SYNTHETIC_ERRNO(EIO),
365 "Failed to turn on FIDO2 assertion user presence: %s", sym_fido_strerr(r));
366
367 log_notice("%s%sIn order to allow secret key generation, please verify presence on security token.",
368 emoji_enabled() ? special_glyph(SPECIAL_GLYPH_TOUCH) : "",
369 emoji_enabled() ? " " : "");
370
371 r = sym_fido_dev_get_assert(d, a, used_pin);
372 }
373 if (r == FIDO_ERR_ACTION_TIMEOUT)
374 return log_error_errno(SYNTHETIC_ERRNO(ENOSTR),
375 "Token action timeout. (User didn't interact with token quickly enough.)");
376 if (r != FIDO_OK)
377 return log_error_errno(SYNTHETIC_ERRNO(EIO),
378 "Failed to ask token for assertion: %s", sym_fido_strerr(r));
379
380 secret = sym_fido_assert_hmac_secret_ptr(a, 0);
381 if (!secret)
382 return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to retrieve HMAC secret.");
383
384 secret_size = sym_fido_assert_hmac_secret_len(a, 0);
385
386 r = add_fido2_credential_id(v, cid, cid_size);
387 if (r < 0)
388 return r;
389
390 r = add_fido2_salt(v,
391 cid,
392 cid_size,
393 salt,
394 FIDO2_SALT_SIZE,
395 secret,
396 secret_size);
397 if (r < 0)
398 return r;
399
400 /* If we acquired the PIN also include it in the secret section of the record, so that systemd-homed
401 * can use it if it needs to, given that it likely needs to decrypt the key again to pass to LUKS or
402 * fscrypt. */
403 r = identity_add_token_pin(v, used_pin);
404 if (r < 0)
405 return r;
406
407 return 0;
408 #else
409 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
410 "FIDO2 tokens not supported on this build.");
411 #endif
412 }
413
414 int list_fido2_devices(void) {
415 #if HAVE_LIBFIDO2
416 _cleanup_(table_unrefp) Table *t = NULL;
417 size_t allocated = 64, found = 0;
418 fido_dev_info_t *di = NULL;
419 int r;
420
421 r = dlopen_libfido2();
422 if (r < 0)
423 return log_error_errno(r, "FIDO2 token support is not installed.");
424
425 di = sym_fido_dev_info_new(allocated);
426 if (!di)
427 return log_oom();
428
429 r = sym_fido_dev_info_manifest(di, allocated, &found);
430 if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) {
431 /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */
432 log_info("No FIDO2 devices found.");
433 r = 0;
434 goto finish;
435 }
436 if (r != FIDO_OK) {
437 r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r));
438 goto finish;
439 }
440
441 t = table_new("path", "manufacturer", "product");
442 if (!t) {
443 r = log_oom();
444 goto finish;
445 }
446
447 for (size_t i = 0; i < found; i++) {
448 const fido_dev_info_t *entry;
449
450 entry = sym_fido_dev_info_ptr(di, i);
451 if (!entry) {
452 r = log_error_errno(SYNTHETIC_ERRNO(EIO),
453 "Failed to get device information for FIDO device %zu.", i);
454 goto finish;
455 }
456
457 r = table_add_many(
458 t,
459 TABLE_PATH, sym_fido_dev_info_path(entry),
460 TABLE_STRING, sym_fido_dev_info_manufacturer_string(entry),
461 TABLE_STRING, sym_fido_dev_info_product_string(entry));
462 if (r < 0) {
463 table_log_add_error(r);
464 goto finish;
465 }
466 }
467
468 r = table_print(t, stdout);
469 if (r < 0) {
470 log_error_errno(r, "Failed to show device table: %m");
471 goto finish;
472 }
473
474 r = 0;
475
476 finish:
477 sym_fido_dev_info_free(&di, allocated);
478 return r;
479 #else
480 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
481 "FIDO2 tokens not supported on this build.");
482 #endif
483 }
484
485 int find_fido2_auto(char **ret) {
486 #if HAVE_LIBFIDO2
487 _cleanup_free_ char *copy = NULL;
488 size_t di_size = 64, found = 0;
489 const fido_dev_info_t *entry;
490 fido_dev_info_t *di = NULL;
491 const char *path;
492 int r;
493
494 r = dlopen_libfido2();
495 if (r < 0)
496 return log_error_errno(r, "FIDO2 token support is not installed.");
497
498 di = sym_fido_dev_info_new(di_size);
499 if (!di)
500 return log_oom();
501
502 r = sym_fido_dev_info_manifest(di, di_size, &found);
503 if (r == FIDO_ERR_INTERNAL || (r == FIDO_OK && found == 0)) {
504 /* The library returns FIDO_ERR_INTERNAL when no devices are found. I wish it wouldn't. */
505 r = log_error_errno(SYNTHETIC_ERRNO(ENODEV), "No FIDO2 devices found.");
506 goto finish;
507 }
508 if (r != FIDO_OK) {
509 r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to enumerate FIDO2 devices: %s", sym_fido_strerr(r));
510 goto finish;
511 }
512 if (found > 1) {
513 r = log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "More than one FIDO2 device found.");
514 goto finish;
515 }
516
517 entry = sym_fido_dev_info_ptr(di, 0);
518 if (!entry) {
519 r = log_error_errno(SYNTHETIC_ERRNO(EIO),
520 "Failed to get device information for FIDO device 0.");
521 goto finish;
522 }
523
524 path = sym_fido_dev_info_path(entry);
525 if (!path) {
526 r = log_error_errno(SYNTHETIC_ERRNO(EIO),
527 "Failed to query FIDO device path.");
528 goto finish;
529 }
530
531 copy = strdup(path);
532 if (!copy) {
533 r = log_oom();
534 goto finish;
535 }
536
537 *ret = TAKE_PTR(copy);
538 r = 0;
539
540 finish:
541 sym_fido_dev_info_free(&di, di_size);
542 return r;
543 #else
544 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
545 "FIDO2 tokens not supported on this build.");
546 #endif
547 }