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