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.