diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index 3a84196b62..fd73e00770 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -352,13 +352,20 @@ SNP-capable hardware and firmware. must point to a raw SNP-built OVMF .fd image; the standard pflash + NVRAM split is not supported under SNP, so the firmware is loaded via QEMU's and Secure Boot is - unavailable. SMBIOS credentials passed via or - are rejected because they are outside the SNP launch - measurement. Direct kernel boot via is required so that the - kernel, initrd and command line are hashed into the launch measurement - (kernel-hashes=on); booting the kernel off the disk image via the - firmware would leave it outside the measurement. A vTPM, if attached via - , must be treated as untrusted by the guest. + unavailable. Direct kernel boot via is required so that the kernel, + initrd and command line are hashed into the launch measurement + (kernel-hashes=on); booting the kernel off the disk image via the firmware + would leave it outside the measurement. Credentials passed via + or are bundled into a cpio archive appended to the initrd + (mirroring what systemd-stub does for ESP credentials), so they enter the + launch measurement via kernel-hashes=on; the SMBIOS and fw_cfg channels + normally used to deliver credentials are not used because they are unmeasured and would be + discarded by PID1 in confidential guests. This channel is measured but not confidential with + respect to the host or VMM: the initrd (and thus the credentials it carries) is supplied to QEMU + as plaintext and only its hash enters the launch measurement, which guarantees integrity but does + not keep the credentials secret from the host. This requires the guest to run a sufficiently + recent version of systemd (supporting /.extra/system_credentials/). A vTPM, + if attached via , must be treated as untrusted by the guest. @@ -864,6 +871,19 @@ systemd.set_credential_binary= which is not a confidential channel. Do not use this for passing secrets to the VM in that case. + Under , SMBIOS and fw_cfg are not covered by the SNP launch + measurement and are discarded by PID1 in confidential guests. Credentials are therefore packaged + into a cpio archive containing + .extra/system_credentials/ID.cred entries and + appended to the initrd that QEMU loads, so they enter the launch measurement via + kernel-hashes=on. PID1 imports them from the initramfs at boot. As with the + kernel command line, this is a measured but not a confidential channel: QEMU receives the initrd + (and thus the embedded credentials) as plaintext from the host and only its hash is covered by the + launch measurement, so a modified initrd produces a different launch measurement that a relying + party can detect via remote attestation, but the credentials are not hidden from the host or VMM. + This requires the guest to run a sufficiently recent version of systemd (supporting + /.extra/system_credentials/). + diff --git a/src/core/import-creds.c b/src/core/import-creds.c index 488d63eb41..98675d2fc5 100644 --- a/src/core/import-creds.c +++ b/src/core/import-creds.c @@ -168,18 +168,121 @@ static int finalize_credentials_dir(const char *dir, const char *envvar) { return 0; } -static int import_credentials_boot(void) { - _cleanup_(import_credentials_context_done) ImportCredentialsContext context = { - .target_dir_fd = -EBADF, - }; +static int import_credentials_from_initrd_path( + ImportCredentialsContext *c, + const char *source_path, + const char *target_dir, + bool with_mount) { + + _cleanup_free_ DirectoryEntries *de = NULL; + _cleanup_close_ int source_dir_fd = -EBADF; int r; - /* systemd-stub will wrap sidecar *.cred files from the UEFI kernel image directory into initrd - * cpios, so that they unpack into /.extra/. We'll pick them up from there and copy them into /run/ - * so that we can access them during the entire runtime (note that the initrd file system is erased - * during the initrd → host transition). Note that these credentials originate from an untrusted - * source (i.e. the ESP typically) and thus need to be authenticated later. We thus put them in a - * directory separate from the usual credentials which are from a trusted source. */ + source_dir_fd = open(source_path, O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); + if (source_dir_fd < 0) { + if (errno == ENOENT) { + log_debug("No credentials passed via %s.", source_path); + return 0; + } + + log_warning_errno(errno, "Failed to open '%s', ignoring: %m", source_path); + return 0; + } + + r = readdir_all(source_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_MUST_BE_REGULAR, &de); + if (r < 0) { + log_warning_errno(r, "Failed to read '%s' contents, ignoring: %m", source_path); + return 0; + } + + FOREACH_ARRAY(i, de->entries, de->n_entries) { + const struct dirent *d = *i; + _cleanup_close_ int cfd = -EBADF, nfd = -EBADF; + _cleanup_free_ char *n = NULL; + const char *e; + struct stat st; + + e = endswith(d->d_name, ".cred"); + if (!e) + continue; + + /* drop .cred suffix (which we want in the ESP sidecar dir, but not for our internal + * processing) */ + n = strndup(d->d_name, e - d->d_name); + if (!n) + return log_oom(); + + if (!credential_name_valid(n)) { + log_warning("Credential '%s' has invalid name, ignoring.", d->d_name); + continue; + } + + cfd = openat(source_dir_fd, d->d_name, O_RDONLY|O_CLOEXEC); + if (cfd < 0) { + log_warning_errno(errno, "Failed to open %s, ignoring: %m", d->d_name); + continue; + } + + if (fstat(cfd, &st) < 0) { + log_warning_errno(errno, "Failed to stat %s, ignoring: %m", d->d_name); + continue; + } + + r = stat_verify_regular(&st); + if (r < 0) { + log_warning_errno(r, "Credential file %s is not a regular file, ignoring: %m", d->d_name); + continue; + } + + if (!credential_size_ok(c, n, st.st_size)) + continue; + + r = acquire_credential_directory(c, target_dir, with_mount); + if (r < 0) + return r; + + nfd = open_credential_file_for_write(c->target_dir_fd, target_dir, n); + if (nfd == -EEXIST) + continue; + if (nfd < 0) + return nfd; + + r = copy_bytes(cfd, nfd, st.st_size, 0); + if (r < 0) { + (void) unlinkat(c->target_dir_fd, n, 0); + return log_error_errno(r, "Failed to create credential '%s': %m", n); + } + + c->size_sum += st.st_size; + c->n_credentials++; + + log_debug("Successfully copied boot credential '%s'.", n); + } + + return 0; +} + +static int import_credentials_boot(ImportCredentialsContext *system_ctx) { + _cleanup_(import_credentials_context_done) ImportCredentialsContext encrypted_ctx = { + .target_dir_fd = -EBADF, + }; + unsigned n_system_before; + int r; + + assert(system_ctx); + + /* The initrd may contain two flavours of credentials placed under /.extra/, copied + * across the initrd → host transition before the initrd tmpfs is erased: + * + * - /.extra/credentials/ and /.extra/global_credentials/ — placed by systemd-stub + * from the EFI System Partition. Trust model: untrusted, because the ESP can + * be mounted and edited offline. They are routed into the @encrypted bucket + * where consumers must authenticate them before use (via LoadCredentialEncrypted=). + * + * - /.extra/system_credentials/ — placed by host-side producers that take responsibility + * for the trust (e.g. systemd-vmspawn when its cpio is covered by the SEV-SNP launch + * measurement or when the host is the trust root in non-confidential setups). Routed + * into the @system bucket where consumers can use them directly via LoadCredential=. */ if (!in_initrd()) return 0; @@ -187,100 +290,36 @@ static int import_credentials_boot(void) { FOREACH_STRING(p, "/.extra/credentials/", /* specific to this boot menu */ "/.extra/global_credentials/") { /* boot partition wide */ - - _cleanup_free_ DirectoryEntries *de = NULL; - _cleanup_close_ int source_dir_fd = -EBADF; - - source_dir_fd = open(p, O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); - if (source_dir_fd < 0) { - if (errno == ENOENT) { - log_debug("No credentials passed via %s.", p); - continue; - } - - log_warning_errno(errno, "Failed to open '%s', ignoring: %m", p); - continue; - } - - r = readdir_all(source_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de); - if (r < 0) { - log_warning_errno(r, "Failed to read '%s' contents, ignoring: %m", p); - continue; - } - - FOREACH_ARRAY(i, de->entries, de->n_entries) { - const struct dirent *d = *i; - _cleanup_close_ int cfd = -EBADF, nfd = -EBADF; - _cleanup_free_ char *n = NULL; - const char *e; - struct stat st; - - e = endswith(d->d_name, ".cred"); - if (!e) - continue; - - /* drop .cred suffix (which we want in the ESP sidecar dir, but not for our internal - * processing) */ - n = strndup(d->d_name, e - d->d_name); - if (!n) - return log_oom(); - - if (!credential_name_valid(n)) { - log_warning("Credential '%s' has invalid name, ignoring.", d->d_name); - continue; - } - - cfd = openat(source_dir_fd, d->d_name, O_RDONLY|O_CLOEXEC); - if (cfd < 0) { - log_warning_errno(errno, "Failed to open %s, ignoring: %m", d->d_name); - continue; - } - - if (fstat(cfd, &st) < 0) { - log_warning_errno(errno, "Failed to stat %s, ignoring: %m", d->d_name); - continue; - } - - r = stat_verify_regular(&st); - if (r < 0) { - log_warning_errno(r, "Credential file %s is not a regular file, ignoring: %m", d->d_name); - continue; - } - - if (!credential_size_ok(&context, n, st.st_size)) - continue; - - r = acquire_credential_directory(&context, ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, /* with_mount= */ false); - if (r < 0) - return r; - - nfd = open_credential_file_for_write(context.target_dir_fd, ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, n); - if (nfd == -EEXIST) - continue; - if (nfd < 0) - return nfd; - - r = copy_bytes(cfd, nfd, st.st_size, 0); - if (r < 0) { - (void) unlinkat(context.target_dir_fd, n, 0); - return log_error_errno(r, "Failed to create credential '%s': %m", n); - } - - context.size_sum += st.st_size; - context.n_credentials++; - - log_debug("Successfully copied boot credential '%s'.", n); - } + r = import_credentials_from_initrd_path( + &encrypted_ctx, p, + ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, + /* with_mount= */ false); + if (r < 0) + return r; } - if (context.n_credentials > 0) { - log_debug("Imported %u credentials from boot loader.", context.n_credentials); + n_system_before = system_ctx->n_credentials; + + r = import_credentials_from_initrd_path( + system_ctx, "/.extra/system_credentials/", + SYSTEM_CREDENTIALS_DIRECTORY, + /* with_mount= */ true); + if (r < 0) + return r; + + if (encrypted_ctx.n_credentials > 0) { + log_debug("Imported %u encrypted credentials from boot loader.", encrypted_ctx.n_credentials); r = finalize_credentials_dir(ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, "ENCRYPTED_CREDENTIALS_DIRECTORY"); if (r < 0) return r; } + if (system_ctx->n_credentials > n_system_before) + log_debug("Imported %u trusted credentials from boot loader.", system_ctx->n_credentials - n_system_before); + + /* The @system credentials directory is shared with import_credentials_trusted(); the caller finalizes. */ + return 0; } @@ -701,28 +740,27 @@ static int import_credentials_initrd(ImportCredentialsContext *c) { return 0; } -static int import_credentials_trusted(void) { - _cleanup_(import_credentials_context_done) ImportCredentialsContext c = { - .target_dir_fd = -EBADF, - }; - int r, ret = 0; +static int import_credentials_trusted(ImportCredentialsContext *c) { + unsigned n_before; + int ret = 0; + + assert(c); /* This is invoked during early boot when no credentials have been imported so far. (Specifically, if * the $CREDENTIALS_DIRECTORY or $ENCRYPTED_CREDENTIALS_DIRECTORY environment variables are not set * yet.) */ - RET_GATHER(ret, import_credentials_qemu(&c)); - RET_GATHER(ret, import_credentials_smbios(&c)); - RET_GATHER(ret, import_credentials_proc_cmdline(&c)); - RET_GATHER(ret, import_credentials_initrd(&c)); + n_before = c->n_credentials; - if (c.n_credentials > 0) { - log_debug("Imported %u credentials from kernel command line/smbios/fw_cfg/initrd.", c.n_credentials); + RET_GATHER(ret, import_credentials_qemu(c)); + RET_GATHER(ret, import_credentials_smbios(c)); + RET_GATHER(ret, import_credentials_proc_cmdline(c)); + RET_GATHER(ret, import_credentials_initrd(c)); - r = finalize_credentials_dir(SYSTEM_CREDENTIALS_DIRECTORY, "CREDENTIALS_DIRECTORY"); - if (r < 0) - return r; - } + if (c->n_credentials > n_before) + log_debug("Imported %u credentials from kernel command line/smbios/fw_cfg/initrd.", c->n_credentials - n_before); + + /* The @system credentials directory is shared with import_credentials_boot(); the caller finalizes. */ return ret; } @@ -884,6 +922,9 @@ int import_credentials(void) { RET_GATHER(r, merge_credentials_trusted(received_creds_dir)); } else { + _cleanup_(import_credentials_context_done) ImportCredentialsContext system_ctx = { + .target_dir_fd = -EBADF, + }; bool import; r = proc_cmdline_get_bool("systemd.import_credentials", PROC_CMDLINE_STRIP_RD_PREFIX|PROC_CMDLINE_TRUE_WHEN_MISSING, &import); @@ -894,8 +935,12 @@ int import_credentials(void) { return 0; } - r = import_credentials_boot(); - RET_GATHER(r, import_credentials_trusted()); + /* System credential context is shared so a single CREDENTIALS_TOTAL_SIZE_MAX is enforced. */ + r = import_credentials_boot(&system_ctx); + RET_GATHER(r, import_credentials_trusted(&system_ctx)); + + if (system_ctx.n_credentials > 0) + RET_GATHER(r, finalize_credentials_dir(SYSTEM_CREDENTIALS_DIRECTORY, "CREDENTIALS_DIRECTORY")); } report_credentials(); diff --git a/src/shared/initrd-cpio.c b/src/shared/initrd-cpio.c new file mode 100644 index 0000000000..e19bbcd341 --- /dev/null +++ b/src/shared/initrd-cpio.c @@ -0,0 +1,185 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "alloc-util.h" +#include "creds-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "initrd-cpio.h" +#include "io-util.h" +#include "log.h" +#include "machine-credential.h" +#include "memory-util.h" +#include "memstream-util.h" +#include "string-util.h" +#include "tmpfile-util.h" + +static void write_cpio_word(FILE *f, uint32_t v) { + assert(f); + + /* Writes a CPIO header 8 character hex value */ + + fprintf(f, "%08" PRIx32, v); +} + +static int append_pad4(FILE *f) { + off_t p; + + assert(f); + + /* Appends NUL bytes until the stream position is a multiple of 4 */ + + p = ftello(f); + if (p < 0) + return -errno; + + for (size_t pad = (4 - ((size_t) p & 3)) & 3; pad > 0; pad--) + fputc(0, f); + + return 0; +} + +static int append_cpio_entry( + FILE *f, + uint32_t mode, /* full mode incl. S_IFDIR or S_IFREG */ + const char *path, + const void *data, /* NULL for directories */ + size_t data_size, /* 0 for directories */ + uint32_t *inode_counter) { + + int r; + + assert(f); + assert(path); + assert(data || data_size == 0); + assert(inode_counter); + + if (data_size > UINT32_MAX) /* cpio cannot deal with > 32-bit file sizes */ + return -EFBIG; + + if (*inode_counter == UINT32_MAX) /* more than 2^32-1 inodes? cpio cannot represent that either */ + return -EOVERFLOW; + + size_t namesize = strlen(path) + 1; + if (namesize > UINT32_MAX) /* cpio also cannot deal with names > 32-bit */ + return -ENAMETOOLONG; + + fputs("070701", f); /* magic ID */ + write_cpio_word(f, (*inode_counter)++); /* inode */ + write_cpio_word(f, mode); /* mode */ + write_cpio_word(f, 0); /* uid */ + write_cpio_word(f, 0); /* gid */ + write_cpio_word(f, 1); /* nlink */ + write_cpio_word(f, 0); /* mtime */ + write_cpio_word(f, data_size); /* size */ + write_cpio_word(f, 0); /* major(dev) */ + write_cpio_word(f, 0); /* minor(dev) */ + write_cpio_word(f, 0); /* major(rdev) */ + write_cpio_word(f, 0); /* minor(rdev) */ + write_cpio_word(f, namesize); /* fname size */ + write_cpio_word(f, 0); /* crc */ + + fwrite(path, 1, namesize, f); + + r = append_pad4(f); + if (r < 0) + return r; + + if (data_size > 0) + fwrite(data, 1, data_size, f); + + return append_pad4(f); +} + +static void append_cpio_trailer(FILE *f) { + static const char trailer[] = + "070701" + "00000000" + "00000000" + "00000000" + "00000000" + "00000001" + "00000000" + "00000000" + "00000000" + "00000000" + "00000000" + "00000000" + "0000000b" + "00000000" + "TRAILER!!!\0\0\0"; /* There's a fourth NUL byte appended here, because this is a string */ + + assert_cc(sizeof(trailer) % 4 == 0); + assert(f); + + fwrite(trailer, 1, sizeof trailer, f); +} + +int initrd_cpio_credentials_to_tempfile( + const MachineCredentialContext *creds, + char **ret_path) { + + _cleanup_(memstream_done) MemStream m = {}; + _cleanup_(unlink_and_freep) char *path = NULL; + _cleanup_close_ int fd = -EBADF; + _cleanup_(erase_and_freep) char *buf = NULL; /* holds plaintext credential bytes; scrub on free */ + size_t buf_size = 0; + uint32_t inode = 1; + FILE *f; + int r; + + assert(creds); + assert(ret_path); + + if (creds->n_credentials == 0) { + *ret_path = NULL; + return 0; + } + + f = memstream_init(&m); + if (!f) + return log_oom(); + + r = append_cpio_entry(f, S_IFDIR | 0555, ".extra", NULL, 0, &inode); + if (r < 0) + return log_error_errno(r, "Failed to write '.extra' directory entry to credentials cpio: %m"); + r = append_cpio_entry(f, S_IFDIR | 0500, ".extra/system_credentials", NULL, 0, &inode); + if (r < 0) + return log_error_errno(r, "Failed to write '.extra/system_credentials' directory entry to credentials cpio: %m"); + + FOREACH_ARRAY(c, creds->credentials, creds->n_credentials) { + if (!credential_name_valid(c->id)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid credential name '%s'.", strna(c->id)); + + _cleanup_free_ char *cpath = strjoin(".extra/system_credentials/", c->id, ".cred"); + if (!cpath) + return log_oom(); + + r = append_cpio_entry(f, S_IFREG | 0400, cpath, c->data, c->size, &inode); + if (r < 0) + return log_error_errno(r, "Failed to write credential '%s' to credentials cpio: %m", c->id); + } + + append_cpio_trailer(f); + + r = memstream_finalize(&m, &buf, &buf_size); + if (r < 0) + return log_error_errno(r, "Failed to finalize credentials cpio: %m"); + + r = tempfn_random_child(NULL, "credentials-cpio", &path); + if (r < 0) + return log_error_errno(r, "Failed to generate temp file name: %m"); + + fd = open(path, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0600); + if (fd < 0) + return log_error_errno(errno, "Failed to create temp file %s: %m", path); + + r = loop_write(fd, buf, buf_size); + if (r < 0) + return log_error_errno(r, "Failed to write credentials cpio: %m"); + + *ret_path = TAKE_PTR(path); + return 0; +} diff --git a/src/shared/initrd-cpio.h b/src/shared/initrd-cpio.h new file mode 100644 index 0000000000..dd6a737636 --- /dev/null +++ b/src/shared/initrd-cpio.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "shared-forward.h" + +/* Builds a new CPIO archive containing each credential as a + * file under .extra/system_credentials/.cred, writes it to a + * freshly-created temp file, and returns the path in *ret_path. + * Caller takes ownership and is responsible for unlink()+free(). + * If creds contains no credentials, no file will be created and + * *ret_path is set to NULL. */ +int initrd_cpio_credentials_to_tempfile( + const MachineCredentialContext *creds, + char **ret_path); diff --git a/src/shared/meson.build b/src/shared/meson.build index 52df34bb0f..6fa4216305 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -103,6 +103,7 @@ shared_sources = files( 'image-policy.c', 'import-util.c', 'in-addr-prefix-util.c', + 'initrd-cpio.c', 'install-file.c', 'install-printf.c', 'install.c', diff --git a/src/test/meson.build b/src/test/meson.build index 0501ac0b0c..cd54f4d8dc 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -126,6 +126,7 @@ simple_tests += files( 'test-import-util.c', 'test-in-addr-prefix-util.c', 'test-in-addr-util.c', + 'test-initrd-cpio.c', 'test-install-file.c', 'test-install-root.c', 'test-io-util.c', diff --git a/src/test/test-initrd-cpio.c b/src/test/test-initrd-cpio.c new file mode 100644 index 0000000000..cea23a6f1c --- /dev/null +++ b/src/test/test-initrd-cpio.c @@ -0,0 +1,86 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include +#include + +#include "alloc-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "initrd-cpio.h" +#include "machine-credential.h" +#include "path-util.h" +#include "rm-rf.h" +#include "tests.h" +#include "tmpfile-util.h" + +TEST_RET(initrd_cpio_credentials_basic) { + struct stat st; + size_t foo_size, bar_size; + _cleanup_free_ char *cmd = NULL, *extra_path = NULL, *creds_path = NULL, *foo_path = NULL, *bar_path = NULL, *foo_content = NULL, *bar_content = NULL; + _cleanup_(machine_credential_context_done) MachineCredentialContext creds = {}; + _cleanup_(unlink_and_freep) char *cpio_path = NULL; + _cleanup_(rm_rf_physical_and_freep) char *extract_dir = NULL; + int r; + + r = find_executable("cpio", NULL); + if (r < 0) + return log_tests_skipped_errno(r, "Could not find cpio binary: %m"); + + ASSERT_OK(machine_credential_add(&creds, "foo", "hello", 5)); + ASSERT_OK(machine_credential_add(&creds, "bar", "abc\0def", 7)); + + ASSERT_OK(initrd_cpio_credentials_to_tempfile(&creds, &cpio_path)); + ASSERT_NOT_NULL(cpio_path); + + ASSERT_OK(mkdtemp_malloc(NULL, &extract_dir)); + ASSERT_OK(asprintf(&cmd, "cd %s && cpio -idm < %s", extract_dir, cpio_path)); + ASSERT_OK_ZERO_ERRNO(system(cmd)); + + ASSERT_NOT_NULL(extra_path = path_join(extract_dir, ".extra")); + ASSERT_OK_ERRNO(stat(extra_path, &st)); + ASSERT_TRUE(S_ISDIR(st.st_mode)); + ASSERT_EQ((mode_t) (st.st_mode & 07777), (mode_t) 0555); + + ASSERT_NOT_NULL(creds_path = path_join(extract_dir, ".extra/system_credentials")); + ASSERT_OK_ERRNO(stat(creds_path, &st)); + ASSERT_TRUE(S_ISDIR(st.st_mode)); + ASSERT_EQ((mode_t) (st.st_mode & 07777), (mode_t) 0500); + + ASSERT_NOT_NULL(foo_path = path_join(extract_dir, ".extra/system_credentials/foo.cred")); + ASSERT_OK_ERRNO(stat(foo_path, &st)); + ASSERT_TRUE(S_ISREG(st.st_mode)); + ASSERT_EQ((mode_t) (st.st_mode & 07777), (mode_t) 0400); + ASSERT_OK(read_full_file(foo_path, &foo_content, &foo_size)); + ASSERT_EQ(foo_size, 5U); + ASSERT_EQ(memcmp(foo_content, "hello", 5), 0); + + ASSERT_NOT_NULL(bar_path = path_join(extract_dir, ".extra/system_credentials/bar.cred")); + ASSERT_OK_ERRNO(stat(bar_path, &st)); + ASSERT_TRUE(S_ISREG(st.st_mode)); + ASSERT_EQ((mode_t) (st.st_mode & 07777), (mode_t) 0400); + ASSERT_OK(read_full_file(bar_path, &bar_content, &bar_size)); + ASSERT_EQ(bar_size, 7U); + ASSERT_EQ(memcmp(bar_content, "abc\0def", 7), 0); + + return 0; +} + +TEST(initrd_cpio_credentials_rejects_invalid_name) { + _cleanup_(machine_credential_context_done) MachineCredentialContext creds = {}; + _cleanup_(unlink_and_freep) char *cpio_path = NULL; + + /* Bypass the validating machine_credential_add()/_set()/_load() helpers and inject an id that would + * escape the .extra/system_credentials/ directory, to exercise the writer's defense-in-depth check. */ + ASSERT_NOT_NULL(creds.credentials = new0(MachineCredential, 1)); + creds.n_credentials = 1; + ASSERT_NOT_NULL(creds.credentials[0].id = strdup("../../etc/evil")); + ASSERT_NOT_NULL(creds.credentials[0].data = memdup("x", 1)); + creds.credentials[0].size = 1; + + ASSERT_ERROR(initrd_cpio_credentials_to_tempfile(&creds, &cpio_path), EINVAL); + ASSERT_NULL(cpio_path); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 36966122f4..b8d42c5587 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -46,6 +46,7 @@ #include "hostname-setup.h" #include "hostname-util.h" #include "id128-util.h" +#include "initrd-cpio.h" #include "kernel-image.h" #include "log.h" #include "machine-bind-user.h" @@ -57,8 +58,8 @@ #include "namespace-util.h" #include "netif-util.h" #include "nsresource.h" -#include "osc-context.h" #include "options.h" +#include "osc-context.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" @@ -3424,26 +3425,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { } } - char *initrd = NULL; - _cleanup_(rm_rf_physical_and_freep) char *merged_initrd = NULL; - size_t n_initrds = strv_length(arg_initrds); - - if (n_initrds == 1) - initrd = arg_initrds[0]; - else if (n_initrds > 1) { - r = merge_initrds(&merged_initrd); - if (r < 0) - return r; - - initrd = merged_initrd; - } - - if (initrd) { - r = strv_extend_many(&cmdline, "-initrd", initrd); - if (r < 0) - return log_oom(); - } - if (arg_forward_journal) { _cleanup_free_ char *listen_address = NULL; if (asprintf(&listen_address, "vsock:2:%u", child_cid) < 0) @@ -3545,9 +3526,54 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { return log_error_errno(r, "Failed to add VSOCK credential: %m"); } - r = cmdline_add_credentials(&cmdline, smbios_dir_fd, smbios_dir); - if (r < 0) - return r; + /* Under --coco=sev-snp the SMBIOS and fw_cfg channels normally used to deliver credentials are + * not covered by the launch measurement and are silently discarded by the guest PID1 in + * confidential VMs. Instead, package credentials into a cpio archive appended to the initrd + * (mirroring what systemd-stub does for ESP credentials) so they enter the launch measurement + * via QEMU's "kernel-hashes=on". The new initrd path requires a guest PID1 that knows about + * /.extra/system_credentials/, so we keep this scoped to SNP for now. Non-CoCo guests + * continue to use the SMBIOS path below, which works with older systemd versions too. + * Must run after all credential-mutating calls above so the cpio captures the complete set. */ + bool use_initrd_cpio = arg_confidential_computing == COCO_AMD_SEV_SNP && + arg_credentials.n_credentials > 0; + + _cleanup_(unlink_and_freep) char *credentials_cpio_path = NULL; + if (use_initrd_cpio) { + r = initrd_cpio_credentials_to_tempfile(&arg_credentials, &credentials_cpio_path); + if (r < 0) + return r; + r = strv_extend(&arg_initrds, credentials_cpio_path); + if (r < 0) + return log_oom(); + } + + char *initrd = NULL; + _cleanup_(rm_rf_physical_and_freep) char *merged_initrd = NULL; + size_t n_initrds = strv_length(arg_initrds); + + if (n_initrds == 1) + initrd = arg_initrds[0]; + else if (n_initrds > 1) { + r = merge_initrds(&merged_initrd); + if (r < 0) + return r; + + initrd = merged_initrd; + } + + if (initrd) { + r = strv_extend_many(&cmdline, "-initrd", initrd); + if (r < 0) + return log_oom(); + } + + /* Under SNP, credentials flow via the initrd cpio above. For everyone else, use the + * SMBIOS/fw_cfg/cmdline path. */ + if (!use_initrd_cpio) { + r = cmdline_add_credentials(&cmdline, smbios_dir_fd, smbios_dir); + if (r < 0) + return r; + } r = cmdline_add_kernel_cmdline(&cmdline, smbios_dir_fd, smbios_dir); if (r < 0) @@ -4115,9 +4141,6 @@ static int verify_arguments(void) { if (set_contains(arg_firmware_features_include, "secure-boot")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--secure-boot=yes cannot be combined with --coco."); - if (arg_credentials.n_credentials != 0) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "SMBIOS credentials aren't trusted by the confidential computing guest and will be rejected."); if (arg_tpm > 0) log_warning("TPM can't be trusted by the confidential computing guest"); /* kernel-hashes=on only covers what QEMU itself loads via -kernel/-initrd/-append.