]>
Commit | Line | Data |
---|---|---|
4b9a4b01 LP |
1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
2 | ||
3 | #include <sys/mount.h> | |
4 | ||
738e807e | 5 | #include "confidential-virt.h" |
4b9a4b01 LP |
6 | #include "copy.h" |
7 | #include "creds-util.h" | |
8de7de46 | 8 | #include "escape.h" |
4b9a4b01 LP |
9 | #include "fileio.h" |
10 | #include "format-util.h" | |
11 | #include "fs-util.h" | |
8de7de46 | 12 | #include "hexdecoct.h" |
4b9a4b01 | 13 | #include "import-creds.h" |
738e807e | 14 | #include "initrd-util.h" |
4b9a4b01 LP |
15 | #include "io-util.h" |
16 | #include "mkdir-label.h" | |
17 | #include "mount-util.h" | |
18 | #include "mountpoint-util.h" | |
19 | #include "parse-util.h" | |
20 | #include "path-util.h" | |
21 | #include "proc-cmdline.h" | |
22 | #include "recurse-dir.h" | |
23 | #include "strv.h" | |
13b99dcc | 24 | #include "virt.h" |
4b9a4b01 LP |
25 | |
26 | /* This imports credentials passed in from environments higher up (VM manager, boot loader, …) and rearranges | |
27 | * them so that later code can access them using our regular credential protocol | |
28 | * (i.e. $CREDENTIALS_DIRECTORY). It's supposed to be minimal glue to unify behaviour how PID 1 (and | |
29 | * generators invoked by it) can acquire credentials from outside, to mimic how we support it for containers, | |
30 | * but on VM/physical environments. | |
31 | * | |
8de7de46 | 32 | * This does four things: |
4b9a4b01 LP |
33 | * |
34 | * 1. It imports credentials picked up by sd-boot (and placed in the /.extra/credentials/ dir in the initrd) | |
35 | * and puts them in /run/credentials/@encrypted/. Note that during the initrd→host transition the initrd root | |
36 | * file system is cleaned out, thus it is essential we pick up these files before they are deleted. Note | |
37 | * that these credentials originate from an untrusted source, i.e. the ESP and are not | |
38 | * pre-authenticated. They still have to be authenticated before use. | |
39 | * | |
40 | * 2. It imports credentials from /proc/cmdline and puts them in /run/credentials/@system/. These come from a | |
41 | * trusted environment (i.e. the boot loader), and are typically authenticated (if authentication is done | |
42 | * at all). However, they are world-readable, which might be less than ideal. Hence only use this for data | |
43 | * that doesn't require trust. | |
44 | * | |
45 | * 3. It imports credentials passed in through qemu's fw_cfg logic. Specifically, credential data passed in | |
46 | * /sys/firmware/qemu_fw_cfg/by_name/opt/io.systemd.credentials/ is picked up and also placed in | |
47 | * /run/credentials/@system/. | |
48 | * | |
8de7de46 LP |
49 | * 4. It imports credentials passed in via the DMI/SMBIOS OEM string tables, quite similar to fw_cfg. It |
50 | * looks for strings starting with "io.systemd.credential:" and "io.systemd.credential.binary:". Both | |
51 | * expect a key=value assignment, but in the latter case the value is Base64 decoded, allowing binary | |
52 | * credentials to be passed in. | |
53 | * | |
4b9a4b01 LP |
54 | * If it picked up any credentials it will set the $CREDENTIALS_DIRECTORY and |
55 | * $ENCRYPTED_CREDENTIALS_DIRECTORY environment variables to point to these directories, so that processes | |
56 | * can find them there later on. If "ramfs" is available $CREDENTIALS_DIRECTORY will be backed by it (but | |
57 | * $ENCRYPTED_CREDENTIALS_DIRECTORY is just a regular tmpfs). | |
58 | * | |
59 | * Net result: the service manager can pick up trusted credentials from $CREDENTIALS_DIRECTORY afterwards, | |
60 | * and untrusted ones from $ENCRYPTED_CREDENTIALS_DIRECTORY. */ | |
61 | ||
62 | typedef struct ImportCredentialContext { | |
63 | int target_dir_fd; | |
64 | size_t size_sum; | |
65 | unsigned n_credentials; | |
66 | } ImportCredentialContext; | |
67 | ||
68 | static void import_credentials_context_free(ImportCredentialContext *c) { | |
69 | assert(c); | |
70 | ||
71 | c->target_dir_fd = safe_close(c->target_dir_fd); | |
72 | } | |
73 | ||
bfa6d9cc | 74 | static int acquire_credential_directory(ImportCredentialContext *c, const char *path, bool with_mount) { |
4b9a4b01 LP |
75 | int r; |
76 | ||
77 | assert(c); | |
bfa6d9cc | 78 | assert(path); |
4b9a4b01 LP |
79 | |
80 | if (c->target_dir_fd >= 0) | |
81 | return c->target_dir_fd; | |
82 | ||
bfa6d9cc LP |
83 | r = path_is_mount_point(path, NULL, 0); |
84 | if (r < 0) { | |
85 | if (r != -ENOENT) | |
86 | return log_error_errno(r, "Failed to determine if %s is a mount point: %m", path); | |
4b9a4b01 | 87 | |
bfa6d9cc LP |
88 | r = mkdir_safe_label(path, 0700, 0, 0, MKDIR_WARN_MODE); |
89 | if (r < 0) | |
90 | return log_error_errno(r, "Failed to create %s mount point: %m", path); | |
91 | ||
92 | r = 0; /* Now it exists and is not a mount point */ | |
93 | } | |
94 | if (r > 0) | |
95 | /* If already a mount point, then remount writable */ | |
96 | (void) mount_nofollow_verbose(LOG_WARNING, NULL, path, NULL, MS_BIND|MS_REMOUNT|credentials_fs_mount_flags(/* ro= */ false), NULL); | |
97 | else if (with_mount) | |
98 | /* If not a mount point yet, and the credentials are not encrypted, then let's try to mount a no-swap fs there */ | |
99 | (void) mount_credentials_fs(path, CREDENTIALS_TOTAL_SIZE_MAX, /* ro= */ false); | |
100 | ||
101 | c->target_dir_fd = open(path, O_RDONLY|O_DIRECTORY|O_CLOEXEC); | |
4b9a4b01 | 102 | if (c->target_dir_fd < 0) |
bfa6d9cc | 103 | return log_error_errno(errno, "Failed to open %s: %m", path); |
4b9a4b01 LP |
104 | |
105 | return c->target_dir_fd; | |
106 | } | |
107 | ||
108 | static int open_credential_file_for_write(int target_dir_fd, const char *dir_name, const char *n) { | |
109 | int fd; | |
110 | ||
111 | assert(target_dir_fd >= 0); | |
112 | assert(dir_name); | |
113 | assert(n); | |
114 | ||
115 | fd = openat(target_dir_fd, n, O_WRONLY|O_CLOEXEC|O_CREAT|O_EXCL|O_NOFOLLOW, 0400); | |
116 | if (fd < 0) { | |
117 | if (errno == EEXIST) /* In case of EEXIST we'll only debug log! */ | |
118 | return log_debug_errno(errno, "Credential '%s' set twice, ignoring.", n); | |
119 | ||
120 | return log_error_errno(errno, "Failed to create %s/%s: %m", dir_name, n); | |
121 | } | |
122 | ||
123 | return fd; | |
124 | } | |
125 | ||
126 | static bool credential_size_ok(ImportCredentialContext *c, const char *name, uint64_t size) { | |
127 | assert(c); | |
128 | assert(name); | |
129 | ||
130 | if (size > CREDENTIAL_SIZE_MAX) { | |
131 | log_warning("Credential '%s' is larger than allowed limit (%s > %s), skipping.", name, FORMAT_BYTES(size), FORMAT_BYTES(CREDENTIAL_SIZE_MAX)); | |
132 | return false; | |
133 | } | |
134 | ||
135 | if (size > CREDENTIALS_TOTAL_SIZE_MAX - c->size_sum) { | |
136 | log_warning("Accumulated credential size would be above allowed limit (%s+%s > %s), skipping '%s'.", | |
137 | FORMAT_BYTES(c->size_sum), FORMAT_BYTES(size), FORMAT_BYTES(CREDENTIALS_TOTAL_SIZE_MAX), name); | |
138 | return false; | |
139 | } | |
140 | ||
141 | return true; | |
142 | } | |
143 | ||
144 | static int finalize_credentials_dir(const char *dir, const char *envvar) { | |
145 | int r; | |
146 | ||
147 | assert(dir); | |
148 | assert(envvar); | |
149 | ||
150 | /* Try to make the credentials directory read-only now */ | |
151 | ||
152 | r = make_mount_point(dir); | |
153 | if (r < 0) | |
154 | log_warning_errno(r, "Failed to make '%s' a mount point, ignoring: %m", dir); | |
155 | else | |
bfa6d9cc | 156 | (void) mount_nofollow_verbose(LOG_WARNING, NULL, dir, NULL, MS_BIND|MS_REMOUNT|credentials_fs_mount_flags(/* ro= */ true), NULL); |
4b9a4b01 LP |
157 | |
158 | if (setenv(envvar, dir, /* overwrite= */ true) < 0) | |
159 | return log_error_errno(errno, "Failed to set $%s environment variable: %m", envvar); | |
160 | ||
161 | return 0; | |
162 | } | |
163 | ||
164 | static int import_credentials_boot(void) { | |
165 | _cleanup_(import_credentials_context_free) ImportCredentialContext context = { | |
254d1313 | 166 | .target_dir_fd = -EBADF, |
4b9a4b01 LP |
167 | }; |
168 | int r; | |
169 | ||
170 | /* systemd-stub will wrap sidecar *.cred files from the UEFI kernel image directory into initrd | |
171 | * cpios, so that they unpack into /.extra/. We'll pick them up from there and copy them into /run/ | |
172 | * so that we can access them during the entire runtime (note that the initrd file system is erased | |
173 | * during the initrd → host transition). Note that these credentials originate from an untrusted | |
174 | * source (i.e. the ESP typically) and thus need to be authenticated later. We thus put them in a | |
175 | * directory separate from the usual credentials which are from a trusted source. */ | |
176 | ||
177 | if (!in_initrd()) | |
178 | return 0; | |
179 | ||
180 | FOREACH_STRING(p, | |
181 | "/.extra/credentials/", /* specific to this boot menu */ | |
182 | "/.extra/global_credentials/") { /* boot partition wide */ | |
183 | ||
184 | _cleanup_free_ DirectoryEntries *de = NULL; | |
254d1313 | 185 | _cleanup_close_ int source_dir_fd = -EBADF; |
4b9a4b01 LP |
186 | |
187 | source_dir_fd = open(p, O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); | |
188 | if (source_dir_fd < 0) { | |
189 | if (errno == ENOENT) { | |
190 | log_debug("No credentials passed via %s.", p); | |
191 | continue; | |
192 | } | |
193 | ||
194 | log_warning_errno(errno, "Failed to open '%s', ignoring: %m", p); | |
195 | continue; | |
196 | } | |
197 | ||
198 | r = readdir_all(source_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de); | |
199 | if (r < 0) { | |
200 | log_warning_errno(r, "Failed to read '%s' contents, ignoring: %m", p); | |
201 | continue; | |
202 | } | |
203 | ||
204 | for (size_t i = 0; i < de->n_entries; i++) { | |
205 | const struct dirent *d = de->entries[i]; | |
254d1313 | 206 | _cleanup_close_ int cfd = -EBADF, nfd = -EBADF; |
4b9a4b01 LP |
207 | _cleanup_free_ char *n = NULL; |
208 | const char *e; | |
209 | struct stat st; | |
210 | ||
211 | e = endswith(d->d_name, ".cred"); | |
212 | if (!e) | |
213 | continue; | |
214 | ||
215 | /* drop .cred suffix (which we want in the ESP sidecar dir, but not for our internal | |
216 | * processing) */ | |
217 | n = strndup(d->d_name, e - d->d_name); | |
218 | if (!n) | |
219 | return log_oom(); | |
220 | ||
221 | if (!credential_name_valid(n)) { | |
222 | log_warning("Credential '%s' has invalid name, ignoring.", d->d_name); | |
223 | continue; | |
224 | } | |
225 | ||
226 | cfd = openat(source_dir_fd, d->d_name, O_RDONLY|O_CLOEXEC); | |
227 | if (cfd < 0) { | |
228 | log_warning_errno(errno, "Failed to open %s, ignoring: %m", d->d_name); | |
229 | continue; | |
230 | } | |
231 | ||
232 | if (fstat(cfd, &st) < 0) { | |
233 | log_warning_errno(errno, "Failed to stat %s, ignoring: %m", d->d_name); | |
234 | continue; | |
235 | } | |
236 | ||
237 | r = stat_verify_regular(&st); | |
238 | if (r < 0) { | |
239 | log_warning_errno(r, "Credential file %s is not a regular file, ignoring: %m", d->d_name); | |
240 | continue; | |
241 | } | |
242 | ||
243 | if (!credential_size_ok(&context, n, st.st_size)) | |
244 | continue; | |
245 | ||
bfa6d9cc | 246 | r = acquire_credential_directory(&context, ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, /* with_mount= */ false); |
4b9a4b01 LP |
247 | if (r < 0) |
248 | return r; | |
249 | ||
250 | nfd = open_credential_file_for_write(context.target_dir_fd, ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, n); | |
251 | if (nfd == -EEXIST) | |
252 | continue; | |
253 | if (nfd < 0) | |
1ab8cd79 | 254 | return nfd; |
4b9a4b01 LP |
255 | |
256 | r = copy_bytes(cfd, nfd, st.st_size, 0); | |
257 | if (r < 0) { | |
258 | (void) unlinkat(context.target_dir_fd, n, 0); | |
259 | return log_error_errno(r, "Failed to create credential '%s': %m", n); | |
260 | } | |
261 | ||
262 | context.size_sum += st.st_size; | |
263 | context.n_credentials++; | |
264 | ||
265 | log_debug("Successfully copied boot credential '%s'.", n); | |
266 | } | |
267 | } | |
268 | ||
269 | if (context.n_credentials > 0) { | |
270 | log_debug("Imported %u credentials from boot loader.", context.n_credentials); | |
271 | ||
272 | r = finalize_credentials_dir(ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, "ENCRYPTED_CREDENTIALS_DIRECTORY"); | |
273 | if (r < 0) | |
274 | return r; | |
275 | } | |
276 | ||
277 | return 0; | |
278 | } | |
279 | ||
4b9a4b01 LP |
280 | static int proc_cmdline_callback(const char *key, const char *value, void *data) { |
281 | ImportCredentialContext *c = ASSERT_PTR(data); | |
de70ecb3 | 282 | _cleanup_free_ void *binary = NULL; |
4b9a4b01 | 283 | _cleanup_free_ char *n = NULL; |
254d1313 | 284 | _cleanup_close_ int nfd = -EBADF; |
de70ecb3 LP |
285 | const char *colon, *d; |
286 | bool base64; | |
4b9a4b01 LP |
287 | size_t l; |
288 | int r; | |
289 | ||
290 | assert(key); | |
291 | ||
de70ecb3 LP |
292 | if (proc_cmdline_key_streq(key, "systemd.set_credential")) |
293 | base64 = false; | |
294 | else if (proc_cmdline_key_streq(key, "systemd.set_credential_binary")) | |
295 | base64 = true; | |
296 | else | |
4b9a4b01 LP |
297 | return 0; |
298 | ||
299 | colon = value ? strchr(value, ':') : NULL; | |
300 | if (!colon) { | |
301 | log_warning("Credential assignment through kernel command line lacks ':' character, ignoring: %s", value); | |
302 | return 0; | |
303 | } | |
304 | ||
305 | n = strndup(value, colon - value); | |
306 | if (!n) | |
307 | return log_oom(); | |
308 | ||
309 | if (!credential_name_valid(n)) { | |
310 | log_warning("Credential name '%s' is invalid, ignoring.", n); | |
311 | return 0; | |
312 | } | |
313 | ||
314 | colon++; | |
de70ecb3 LP |
315 | |
316 | if (base64) { | |
317 | r = unbase64mem(colon, SIZE_MAX, &binary, &l); | |
318 | if (r < 0) { | |
319 | log_warning_errno(r, "Failed to decode binary credential '%s' data, ignoring: %m", n); | |
320 | return 0; | |
321 | } | |
322 | ||
323 | d = binary; | |
324 | } else { | |
325 | d = colon; | |
326 | l = strlen(colon); | |
327 | } | |
4b9a4b01 LP |
328 | |
329 | if (!credential_size_ok(c, n, l)) | |
330 | return 0; | |
331 | ||
bfa6d9cc | 332 | r = acquire_credential_directory(c, SYSTEM_CREDENTIALS_DIRECTORY, /* with_mount= */ true); |
4b9a4b01 LP |
333 | if (r < 0) |
334 | return r; | |
335 | ||
336 | nfd = open_credential_file_for_write(c->target_dir_fd, SYSTEM_CREDENTIALS_DIRECTORY, n); | |
337 | if (nfd == -EEXIST) | |
338 | return 0; | |
339 | if (nfd < 0) | |
1ab8cd79 | 340 | return nfd; |
4b9a4b01 | 341 | |
e22c60a9 | 342 | r = loop_write(nfd, d, l); |
4b9a4b01 LP |
343 | if (r < 0) { |
344 | (void) unlinkat(c->target_dir_fd, n, 0); | |
345 | return log_error_errno(r, "Failed to write credential: %m"); | |
346 | } | |
347 | ||
348 | c->size_sum += l; | |
349 | c->n_credentials++; | |
350 | ||
351 | log_debug("Successfully processed kernel command line credential '%s'.", n); | |
352 | ||
353 | return 0; | |
354 | } | |
355 | ||
356 | static int import_credentials_proc_cmdline(ImportCredentialContext *c) { | |
357 | int r; | |
358 | ||
359 | assert(c); | |
360 | ||
361 | r = proc_cmdline_parse(proc_cmdline_callback, c, 0); | |
362 | if (r < 0) | |
363 | return log_error_errno(r, "Failed to parse /proc/cmdline: %m"); | |
364 | ||
365 | return 0; | |
366 | } | |
367 | ||
368 | #define QEMU_FWCFG_PATH "/sys/firmware/qemu_fw_cfg/by_name/opt/io.systemd.credentials" | |
369 | ||
370 | static int import_credentials_qemu(ImportCredentialContext *c) { | |
371 | _cleanup_free_ DirectoryEntries *de = NULL; | |
254d1313 | 372 | _cleanup_close_ int source_dir_fd = -EBADF; |
4b9a4b01 LP |
373 | int r; |
374 | ||
375 | assert(c); | |
376 | ||
13b99dcc LP |
377 | if (detect_container() > 0) /* don't access /sys/ in a container */ |
378 | return 0; | |
379 | ||
738e807e LP |
380 | if (detect_confidential_virtualization() > 0) /* don't trust firmware if confidential VMs */ |
381 | return 0; | |
382 | ||
4b9a4b01 LP |
383 | source_dir_fd = open(QEMU_FWCFG_PATH, O_RDONLY|O_DIRECTORY|O_CLOEXEC); |
384 | if (source_dir_fd < 0) { | |
385 | if (errno == ENOENT) { | |
386 | log_debug("No credentials passed via fw_cfg."); | |
387 | return 0; | |
388 | } | |
389 | ||
390 | log_warning_errno(errno, "Failed to open '" QEMU_FWCFG_PATH "', ignoring: %m"); | |
391 | return 0; | |
392 | } | |
393 | ||
394 | r = readdir_all(source_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de); | |
395 | if (r < 0) { | |
396 | log_warning_errno(r, "Failed to read '" QEMU_FWCFG_PATH "' contents, ignoring: %m"); | |
397 | return 0; | |
398 | } | |
399 | ||
400 | for (size_t i = 0; i < de->n_entries; i++) { | |
401 | const struct dirent *d = de->entries[i]; | |
254d1313 | 402 | _cleanup_close_ int vfd = -EBADF, rfd = -EBADF, nfd = -EBADF; |
4b9a4b01 LP |
403 | _cleanup_free_ char *szs = NULL; |
404 | uint64_t sz; | |
405 | ||
406 | if (!credential_name_valid(d->d_name)) { | |
407 | log_warning("Credential '%s' has invalid name, ignoring.", d->d_name); | |
408 | continue; | |
409 | } | |
410 | ||
411 | vfd = openat(source_dir_fd, d->d_name, O_RDONLY|O_DIRECTORY|O_CLOEXEC); | |
412 | if (vfd < 0) { | |
413 | log_warning_errno(errno, "Failed to open '" QEMU_FWCFG_PATH "'/%s/, ignoring: %m", d->d_name); | |
414 | continue; | |
415 | } | |
416 | ||
417 | r = read_virtual_file_at(vfd, "size", LINE_MAX, &szs, NULL); | |
418 | if (r < 0) { | |
419 | log_warning_errno(r, "Failed to read '" QEMU_FWCFG_PATH "'/%s/size, ignoring: %m", d->d_name); | |
420 | continue; | |
421 | } | |
422 | ||
423 | r = safe_atou64(strstrip(szs), &sz); | |
424 | if (r < 0) { | |
425 | log_warning_errno(r, "Failed to parse size of credential '%s', ignoring: %s", d->d_name, szs); | |
426 | continue; | |
427 | } | |
428 | ||
429 | if (!credential_size_ok(c, d->d_name, sz)) | |
430 | continue; | |
431 | ||
432 | /* Ideally we'd just symlink the data here. Alas the kernel driver exports the raw file as | |
433 | * having size zero, and we'd rather not have applications support such credential | |
434 | * files. Let's hence copy the files to make them regular. */ | |
435 | ||
436 | rfd = openat(vfd, "raw", O_RDONLY|O_CLOEXEC); | |
437 | if (rfd < 0) { | |
1ab8cd79 | 438 | log_warning_errno(errno, "Failed to open '" QEMU_FWCFG_PATH "'/%s/raw, ignoring: %m", d->d_name); |
4b9a4b01 LP |
439 | continue; |
440 | } | |
441 | ||
bfa6d9cc | 442 | r = acquire_credential_directory(c, SYSTEM_CREDENTIALS_DIRECTORY, /* with_mount= */ true); |
4b9a4b01 LP |
443 | if (r < 0) |
444 | return r; | |
445 | ||
446 | nfd = open_credential_file_for_write(c->target_dir_fd, SYSTEM_CREDENTIALS_DIRECTORY, d->d_name); | |
447 | if (nfd == -EEXIST) | |
448 | continue; | |
449 | if (nfd < 0) | |
1ab8cd79 | 450 | return nfd; |
4b9a4b01 LP |
451 | |
452 | r = copy_bytes(rfd, nfd, sz, 0); | |
453 | if (r < 0) { | |
454 | (void) unlinkat(c->target_dir_fd, d->d_name, 0); | |
455 | return log_error_errno(r, "Failed to create credential '%s': %m", d->d_name); | |
456 | } | |
457 | ||
458 | c->size_sum += sz; | |
459 | c->n_credentials++; | |
460 | ||
461 | log_debug("Successfully copied qemu fw_cfg credential '%s'.", d->d_name); | |
462 | } | |
463 | ||
464 | return 0; | |
465 | } | |
466 | ||
8de7de46 LP |
467 | static int parse_smbios_strings(ImportCredentialContext *c, const char *data, size_t size) { |
468 | size_t left, skip; | |
469 | const char *p; | |
470 | int r; | |
471 | ||
472 | assert(c); | |
473 | assert(data || size == 0); | |
474 | ||
475 | /* Unpacks a packed series of SMBIOS OEM vendor strings. These are a series of NUL terminated | |
476 | * strings, one after the other. */ | |
477 | ||
478 | for (p = data, left = size; left > 0; p += skip, left -= skip) { | |
479 | _cleanup_free_ void *buf = NULL; | |
480 | _cleanup_free_ char *cn = NULL; | |
254d1313 | 481 | _cleanup_close_ int nfd = -EBADF; |
8de7de46 LP |
482 | const char *nul, *n, *eq; |
483 | const void *cdata; | |
484 | size_t buflen, cdata_len; | |
485 | bool unbase64; | |
486 | ||
487 | nul = memchr(p, 0, left); | |
488 | if (nul) | |
489 | skip = (nul - p) + 1; | |
490 | else { | |
491 | nul = p + left; | |
492 | skip = left; | |
493 | } | |
494 | ||
495 | if (nul - p == 0) /* Skip empty strings */ | |
496 | continue; | |
497 | ||
498 | /* Only care about strings starting with either of these two prefixes */ | |
499 | if ((n = memory_startswith(p, nul - p, "io.systemd.credential:"))) | |
500 | unbase64 = false; | |
501 | else if ((n = memory_startswith(p, nul - p, "io.systemd.credential.binary:"))) | |
502 | unbase64 = true; | |
503 | else { | |
504 | _cleanup_free_ char *escaped = NULL; | |
505 | ||
506 | escaped = cescape_length(p, nul - p); | |
507 | log_debug("Ignoring OEM string: %s", strnull(escaped)); | |
508 | continue; | |
509 | } | |
510 | ||
511 | eq = memchr(n, '=', nul - n); | |
512 | if (!eq) { | |
513 | log_warning("SMBIOS OEM string lacks '=' character, ignoring."); | |
514 | continue; | |
515 | } | |
516 | ||
517 | cn = memdup_suffix0(n, eq - n); | |
518 | if (!cn) | |
519 | return log_oom(); | |
520 | ||
521 | if (!credential_name_valid(cn)) { | |
522 | log_warning("SMBIOS credential name '%s' is not valid, ignoring: %m", cn); | |
523 | continue; | |
524 | } | |
525 | ||
526 | /* Optionally base64 decode the data, if requested, to allow binary credentials */ | |
527 | if (unbase64) { | |
528 | r = unbase64mem(eq + 1, nul - (eq + 1), &buf, &buflen); | |
529 | if (r < 0) { | |
530 | log_warning_errno(r, "Failed to base64 decode credential '%s', ignoring: %m", cn); | |
531 | continue; | |
532 | } | |
533 | ||
534 | cdata = buf; | |
535 | cdata_len = buflen; | |
536 | } else { | |
537 | cdata = eq + 1; | |
538 | cdata_len = nul - (eq + 1); | |
539 | } | |
540 | ||
541 | if (!credential_size_ok(c, cn, cdata_len)) | |
542 | continue; | |
543 | ||
bfa6d9cc | 544 | r = acquire_credential_directory(c, SYSTEM_CREDENTIALS_DIRECTORY, /* with_mount= */ true); |
8de7de46 LP |
545 | if (r < 0) |
546 | return r; | |
547 | ||
548 | nfd = open_credential_file_for_write(c->target_dir_fd, SYSTEM_CREDENTIALS_DIRECTORY, cn); | |
549 | if (nfd == -EEXIST) | |
550 | continue; | |
551 | if (nfd < 0) | |
552 | return nfd; | |
553 | ||
e22c60a9 | 554 | r = loop_write(nfd, cdata, cdata_len); |
8de7de46 LP |
555 | if (r < 0) { |
556 | (void) unlinkat(c->target_dir_fd, cn, 0); | |
557 | return log_error_errno(r, "Failed to write credential: %m"); | |
558 | } | |
559 | ||
560 | c->size_sum += cdata_len; | |
561 | c->n_credentials++; | |
562 | ||
563 | log_debug("Successfully processed SMBIOS credential '%s'.", cn); | |
564 | } | |
565 | ||
566 | return 0; | |
567 | } | |
568 | ||
569 | static int import_credentials_smbios(ImportCredentialContext *c) { | |
570 | int r; | |
571 | ||
572 | /* Parses DMI OEM strings fields (SMBIOS type 11), as settable with qemu's -smbios type=11,value=… switch. */ | |
573 | ||
13b99dcc LP |
574 | if (detect_container() > 0) /* don't access /sys/ in a container */ |
575 | return 0; | |
576 | ||
738e807e LP |
577 | if (detect_confidential_virtualization() > 0) /* don't trust firmware if confidential VMs */ |
578 | return 0; | |
579 | ||
8de7de46 LP |
580 | for (unsigned i = 0;; i++) { |
581 | struct dmi_field_header { | |
582 | uint8_t type; | |
583 | uint8_t length; | |
584 | uint16_t handle; | |
585 | uint8_t count; | |
586 | char contents[]; | |
587 | } _packed_ *dmi_field_header; | |
588 | _cleanup_free_ char *p = NULL; | |
589 | _cleanup_free_ void *data = NULL; | |
590 | size_t size; | |
591 | ||
592 | assert_cc(offsetof(struct dmi_field_header, contents) == 5); | |
593 | ||
594 | if (asprintf(&p, "/sys/firmware/dmi/entries/11-%u/raw", i) < 0) | |
595 | return log_oom(); | |
596 | ||
597 | r = read_virtual_file(p, sizeof(dmi_field_header) + CREDENTIALS_TOTAL_SIZE_MAX, (char**) &data, &size); | |
598 | if (r < 0) { | |
599 | /* Once we reach ENOENT there are no more DMI Type 11 fields around. */ | |
d8e4960b | 600 | log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r, "Failed to open '%s', ignoring: %m", p); |
8de7de46 LP |
601 | break; |
602 | } | |
603 | ||
604 | if (size < offsetof(struct dmi_field_header, contents)) | |
d8e4960b | 605 | return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "DMI field header of '%s' too short.", p); |
8de7de46 LP |
606 | |
607 | dmi_field_header = data; | |
608 | if (dmi_field_header->type != 11 || | |
609 | dmi_field_header->length != offsetof(struct dmi_field_header, contents)) | |
610 | return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Invalid DMI field header."); | |
611 | ||
612 | r = parse_smbios_strings(c, dmi_field_header->contents, size - offsetof(struct dmi_field_header, contents)); | |
613 | if (r < 0) | |
614 | return r; | |
615 | ||
616 | if (i == UINT_MAX) /* Prevent overflow */ | |
617 | break; | |
618 | } | |
619 | ||
620 | return 0; | |
621 | } | |
622 | ||
0dea5b77 LP |
623 | static int import_credentials_initrd(ImportCredentialContext *c) { |
624 | _cleanup_free_ DirectoryEntries *de = NULL; | |
625 | _cleanup_close_ int source_dir_fd = -EBADF; | |
626 | int r; | |
627 | ||
628 | assert(c); | |
629 | ||
630 | /* This imports credentials from /run/credentials/@initrd/ into our credentials directory and deletes | |
631 | * the source directory afterwards. This is run once after the initrd → host transition. This is | |
632 | * supposed to establish a well-defined avenue for initrd-based host configurators to pass | |
633 | * credentials into the main system. */ | |
634 | ||
635 | if (in_initrd()) | |
636 | return 0; | |
637 | ||
638 | source_dir_fd = open("/run/credentials/@initrd", O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); | |
639 | if (source_dir_fd < 0) { | |
640 | if (errno == ENOENT) | |
641 | log_debug_errno(errno, "No credentials passed from initrd."); | |
642 | else | |
643 | log_warning_errno(errno, "Failed to open '/run/credentials/@initrd', ignoring: %m"); | |
644 | return 0; | |
645 | } | |
646 | ||
647 | r = readdir_all(source_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de); | |
648 | if (r < 0) { | |
649 | log_warning_errno(r, "Failed to read '/run/credentials/@initrd' contents, ignoring: %m"); | |
650 | return 0; | |
651 | } | |
652 | ||
653 | FOREACH_ARRAY(entry, de->entries, de->n_entries) { | |
654 | _cleanup_close_ int cfd = -EBADF, nfd = -EBADF; | |
655 | const struct dirent *d = *entry; | |
656 | struct stat st; | |
657 | ||
658 | if (!credential_name_valid(d->d_name)) { | |
659 | log_warning("Credential '%s' has invalid name, ignoring.", d->d_name); | |
660 | continue; | |
661 | } | |
662 | ||
663 | cfd = openat(source_dir_fd, d->d_name, O_RDONLY|O_CLOEXEC); | |
664 | if (cfd < 0) { | |
665 | log_warning_errno(errno, "Failed to open %s, ignoring: %m", d->d_name); | |
666 | continue; | |
667 | } | |
668 | ||
669 | if (fstat(cfd, &st) < 0) { | |
670 | log_warning_errno(errno, "Failed to stat %s, ignoring: %m", d->d_name); | |
671 | continue; | |
672 | } | |
673 | ||
674 | r = stat_verify_regular(&st); | |
675 | if (r < 0) { | |
676 | log_warning_errno(r, "Credential file %s is not a regular file, ignoring: %m", d->d_name); | |
677 | continue; | |
678 | } | |
679 | ||
680 | if (!credential_size_ok(c, d->d_name, st.st_size)) | |
681 | continue; | |
682 | ||
bfa6d9cc | 683 | r = acquire_credential_directory(c, SYSTEM_CREDENTIALS_DIRECTORY, /* with_mount= */ true); |
0dea5b77 LP |
684 | if (r < 0) |
685 | return r; | |
686 | ||
687 | nfd = open_credential_file_for_write(c->target_dir_fd, SYSTEM_CREDENTIALS_DIRECTORY, d->d_name); | |
688 | if (nfd == -EEXIST) | |
689 | continue; | |
690 | if (nfd < 0) | |
691 | return nfd; | |
692 | ||
693 | r = copy_bytes(cfd, nfd, st.st_size, 0); | |
694 | if (r < 0) { | |
695 | (void) unlinkat(c->target_dir_fd, d->d_name, 0); | |
696 | return log_error_errno(r, "Failed to create credential '%s': %m", d->d_name); | |
697 | } | |
698 | ||
699 | c->size_sum += st.st_size; | |
700 | c->n_credentials++; | |
701 | ||
702 | log_debug("Successfully copied initrd credential '%s'.", d->d_name); | |
703 | ||
704 | (void) unlinkat(source_dir_fd, d->d_name, 0); | |
705 | } | |
706 | ||
707 | source_dir_fd = safe_close(source_dir_fd); | |
708 | ||
709 | if (rmdir("/run/credentials/@initrd") < 0) | |
710 | log_warning_errno(errno, "Failed to remove /run/credentials/@initrd after import, ignoring: %m"); | |
711 | ||
712 | return 0; | |
713 | } | |
714 | ||
4b9a4b01 LP |
715 | static int import_credentials_trusted(void) { |
716 | _cleanup_(import_credentials_context_free) ImportCredentialContext c = { | |
254d1313 | 717 | .target_dir_fd = -EBADF, |
4b9a4b01 | 718 | }; |
0dea5b77 LP |
719 | int q, w, r, y; |
720 | ||
721 | /* This is invoked during early boot when no credentials have been imported so far. (Specifically, if | |
722 | * the $CREDENTIALS_DIRECTORY or $ENCRYPTED_CREDENTIALS_DIRECTORY environment variables are not set | |
723 | * yet.) */ | |
4b9a4b01 LP |
724 | |
725 | r = import_credentials_qemu(&c); | |
8de7de46 | 726 | w = import_credentials_smbios(&c); |
4b9a4b01 | 727 | q = import_credentials_proc_cmdline(&c); |
0dea5b77 | 728 | y = import_credentials_initrd(&c); |
4b9a4b01 LP |
729 | |
730 | if (c.n_credentials > 0) { | |
731 | int z; | |
732 | ||
0dea5b77 | 733 | log_debug("Imported %u credentials from kernel command line/smbios/fw_cfg/initrd.", c.n_credentials); |
4b9a4b01 LP |
734 | |
735 | z = finalize_credentials_dir(SYSTEM_CREDENTIALS_DIRECTORY, "CREDENTIALS_DIRECTORY"); | |
736 | if (z < 0) | |
737 | return z; | |
738 | } | |
739 | ||
0dea5b77 LP |
740 | return r < 0 ? r : w < 0 ? w : q < 0 ? q : y; |
741 | } | |
742 | ||
743 | static int merge_credentials_trusted(const char *creds_dir) { | |
744 | _cleanup_(import_credentials_context_free) ImportCredentialContext c = { | |
745 | .target_dir_fd = -EBADF, | |
746 | }; | |
747 | int r; | |
748 | ||
749 | /* This is invoked after the initrd → host transitions, when credentials already have been imported, | |
750 | * but we might want to import some more from the initrd. */ | |
751 | ||
752 | if (in_initrd()) | |
753 | return 0; | |
754 | ||
755 | /* Do not try to merge initrd credentials into foreign credentials directories */ | |
756 | if (!path_equal_ptr(creds_dir, SYSTEM_CREDENTIALS_DIRECTORY)) { | |
757 | log_debug("Not importing initrd credentials, as foreign $CREDENTIALS_DIRECTORY has been set."); | |
758 | return 0; | |
759 | } | |
760 | ||
761 | r = import_credentials_initrd(&c); | |
762 | ||
763 | if (c.n_credentials > 0) { | |
764 | int z; | |
765 | ||
766 | log_debug("Merged %u credentials from initrd.", c.n_credentials); | |
767 | ||
768 | z = finalize_credentials_dir(SYSTEM_CREDENTIALS_DIRECTORY, "CREDENTIALS_DIRECTORY"); | |
769 | if (z < 0) | |
770 | return z; | |
771 | } | |
772 | ||
773 | return r; | |
4b9a4b01 LP |
774 | } |
775 | ||
776 | static int symlink_credential_dir(const char *envvar, const char *path, const char *where) { | |
777 | int r; | |
778 | ||
779 | assert(envvar); | |
780 | assert(path); | |
781 | assert(where); | |
782 | ||
783 | if (!path_is_valid(path) || !path_is_absolute(path)) | |
784 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "String specified via $%s is not a valid absolute path, refusing: %s", envvar, path); | |
785 | ||
786 | /* If the env var already points to where we intend to create the symlink, then most likely we | |
787 | * already imported some creds earlier, and thus set the env var, and hence don't need to do | |
788 | * anything. */ | |
789 | if (path_equal(path, where)) | |
790 | return 0; | |
791 | ||
792 | r = symlink_idempotent(path, where, /* make_relative= */ true); | |
793 | if (r < 0) | |
794 | return log_error_errno(r, "Failed to link $%s to %s: %m", envvar, where); | |
795 | ||
796 | return 0; | |
797 | } | |
798 | ||
d021aa8e LP |
799 | static int setenv_notify_socket(void) { |
800 | _cleanup_free_ char *address = NULL; | |
801 | int r; | |
802 | ||
803 | r = read_credential_with_decryption("vmm.notify_socket", (void **)&address, /* ret_size= */ NULL); | |
804 | if (r < 0) | |
805 | return log_warning_errno(r, "Failed to read 'vmm.notify_socket' credential, ignoring: %m"); | |
806 | ||
807 | if (isempty(address)) | |
808 | return 0; | |
809 | ||
810 | if (setenv("NOTIFY_SOCKET", address, /* replace= */ 1) < 0) | |
811 | return log_warning_errno(errno, "Failed to set $NOTIFY_SOCKET environment variable, ignoring: %m"); | |
812 | ||
813 | return 1; | |
814 | } | |
815 | ||
7ca59e67 LP |
816 | static int report_credentials_per_func(const char *title, int (*get_directory_func)(const char **ret)) { |
817 | _cleanup_free_ DirectoryEntries *de = NULL; | |
818 | _cleanup_close_ int dir_fd = -EBADF; | |
819 | _cleanup_free_ char *ll = NULL; | |
820 | const char *d = NULL; | |
821 | int r, c = 0; | |
822 | ||
823 | assert(title); | |
824 | assert(get_directory_func); | |
825 | ||
826 | r = get_directory_func(&d); | |
827 | if (r < 0) { | |
828 | if (r == -ENXIO) /* Env var not set */ | |
829 | return 0; | |
830 | ||
831 | return log_warning_errno(r, "Failed to determine %s directory: %m", title); | |
832 | } | |
833 | ||
834 | dir_fd = open(d, O_RDONLY|O_DIRECTORY|O_CLOEXEC); | |
835 | if (dir_fd < 0) | |
836 | return log_warning_errno(errno, "Failed to open credentials directory %s: %m", d); | |
837 | ||
838 | r = readdir_all(dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de); | |
839 | if (r < 0) | |
840 | return log_warning_errno(r, "Failed to enumerate credentials directory %s: %m", d); | |
841 | ||
842 | FOREACH_ARRAY(entry, de->entries, de->n_entries) { | |
843 | const struct dirent *e = *entry; | |
844 | ||
845 | if (!credential_name_valid(e->d_name)) | |
846 | continue; | |
847 | ||
848 | if (!strextend_with_separator(&ll, ", ", e->d_name)) | |
849 | return log_oom(); | |
850 | ||
851 | c++; | |
852 | } | |
853 | ||
854 | if (ll) | |
855 | log_info("Received %s: %s", title, ll); | |
856 | ||
857 | return c; | |
858 | } | |
859 | ||
860 | static void report_credentials(void) { | |
861 | int p, q; | |
862 | ||
863 | p = report_credentials_per_func("regular credentials", get_credentials_dir); | |
864 | q = report_credentials_per_func("untrusted credentials", get_encrypted_credentials_dir); | |
865 | ||
866 | log_full(p > 0 || q > 0 ? LOG_INFO : LOG_DEBUG, | |
867 | "Acquired %i regular credentials, %i untrusted credentials.", | |
868 | p > 0 ? p : 0, | |
869 | q > 0 ? q : 0); | |
870 | } | |
871 | ||
4b9a4b01 LP |
872 | int import_credentials(void) { |
873 | const char *received_creds_dir = NULL, *received_encrypted_creds_dir = NULL; | |
874 | bool envvar_set = false; | |
875 | int r, q; | |
876 | ||
877 | r = get_credentials_dir(&received_creds_dir); | |
878 | if (r < 0 && r != -ENXIO) /* ENXIO → env var not set yet */ | |
879 | log_warning_errno(r, "Failed to determine credentials directory, ignoring: %m"); | |
880 | ||
881 | envvar_set = r >= 0; | |
882 | ||
883 | r = get_encrypted_credentials_dir(&received_encrypted_creds_dir); | |
884 | if (r < 0 && r != -ENXIO) /* ENXIO → env var not set yet */ | |
885 | log_warning_errno(r, "Failed to determine encrypted credentials directory, ignoring: %m"); | |
886 | ||
887 | envvar_set = envvar_set || r >= 0; | |
888 | ||
889 | if (envvar_set) { | |
890 | /* Maybe an earlier stage initrd already set this up? If so, don't try to import anything again. */ | |
891 | log_debug("Not importing credentials, $CREDENTIALS_DIRECTORY or $ENCRYPTED_CREDENTIALS_DIRECTORY already set."); | |
892 | ||
893 | /* But, let's make sure the creds are available from our regular paths. */ | |
894 | if (received_creds_dir) | |
895 | r = symlink_credential_dir("CREDENTIALS_DIRECTORY", received_creds_dir, SYSTEM_CREDENTIALS_DIRECTORY); | |
896 | else | |
897 | r = 0; | |
898 | ||
899 | if (received_encrypted_creds_dir) { | |
900 | q = symlink_credential_dir("ENCRYPTED_CREDENTIALS_DIRECTORY", received_encrypted_creds_dir, ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY); | |
901 | if (r >= 0) | |
902 | r = q; | |
903 | } | |
904 | ||
0dea5b77 LP |
905 | q = merge_credentials_trusted(received_creds_dir); |
906 | if (r >= 0) | |
907 | r = q; | |
908 | ||
4b9a4b01 LP |
909 | } else { |
910 | _cleanup_free_ char *v = NULL; | |
911 | ||
912 | r = proc_cmdline_get_key("systemd.import_credentials", PROC_CMDLINE_STRIP_RD_PREFIX, &v); | |
913 | if (r < 0) | |
914 | log_debug_errno(r, "Failed to check if 'systemd.import_credentials=' kernel command line option is set, ignoring: %m"); | |
915 | else if (r > 0) { | |
916 | r = parse_boolean(v); | |
917 | if (r < 0) | |
918 | log_debug_errno(r, "Failed to parse 'systemd.import_credentials=' parameter, ignoring: %m"); | |
919 | else if (r == 0) { | |
920 | log_notice("systemd.import_credentials=no is set, skipping importing of credentials."); | |
921 | return 0; | |
922 | } | |
923 | } | |
924 | ||
925 | r = import_credentials_boot(); | |
926 | ||
927 | q = import_credentials_trusted(); | |
928 | if (r >= 0) | |
929 | r = q; | |
930 | } | |
931 | ||
7ca59e67 LP |
932 | report_credentials(); |
933 | ||
d021aa8e LP |
934 | /* Propagate vmm_notify_socket credential → $NOTIFY_SOCKET env var */ |
935 | (void) setenv_notify_socket(); | |
4a91ace5 | 936 | |
4b9a4b01 LP |
937 | return r; |
938 | } |