vmspawn: deliver credentials via initrd cpio under SEV-SNP (#42272)

Re-enables `--set-credential=` / `--load-credential=` under
`--coco=sev-snp` by packaging credentials into a cpio appended to the
initrd, mirroring what `systemd-stub` does for ESP-sourced credentials.
The initrd is covered by the launch measurement via `kernel-hashes=on`,
so the credentials are too.

Tested end-to-end on an SNP-capable host: credentials passed via
`--set-credential=` land in `/run/credentials/@encrypted/` inside the
guest.
This commit is contained in:
Lennart Poettering
2026-06-25 22:08:08 +02:00
committed by GitHub
8 changed files with 522 additions and 147 deletions

View File

@@ -352,13 +352,20 @@
SNP-capable hardware and firmware. <option>--firmware=</option> must point to a raw SNP-built
OVMF <filename>.fd</filename> image; the standard pflash + NVRAM split is not supported under
SNP, so the firmware is loaded via QEMU's <option>-bios</option> and Secure Boot is
unavailable. SMBIOS credentials passed via <option>--set-credential=</option> or
<option>--load-credential=</option> are rejected because they are outside the SNP launch
measurement. Direct kernel boot via <option>--linux=</option> is required so that the
kernel, initrd and command line are hashed into the launch measurement
(<literal>kernel-hashes=on</literal>); booting the kernel off the disk image via the
firmware would leave it outside the measurement. A vTPM, if attached via
<option>--tpm=</option>, must be treated as untrusted by the guest.</para>
unavailable. Direct kernel boot via <option>--linux=</option> is required so that the kernel,
initrd and command line are hashed into the launch measurement
(<literal>kernel-hashes=on</literal>); booting the kernel off the disk image via the firmware
would leave it outside the measurement. Credentials passed via <option>--set-credential=</option>
or <option>--load-credential=</option> are bundled into a cpio archive appended to the initrd
(mirroring what <command>systemd-stub</command> does for ESP credentials), so they enter the
launch measurement via <literal>kernel-hashes=on</literal>; 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 <filename>/.extra/system_credentials/</filename>). A vTPM,
if attached via <option>--tpm=</option>, must be treated as untrusted by the guest.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
@@ -864,6 +871,19 @@
<varname>systemd.set_credential_binary=</varname> which is not a confidential channel. Do not use
this for passing secrets to the VM in that case.</para>
<para>Under <option>--coco=sev-snp</option>, 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
<filename>.extra/system_credentials/<replaceable>ID</replaceable>.cred</filename> entries and
appended to the initrd that QEMU loads, so they enter the launch measurement via
<literal>kernel-hashes=on</literal>. 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
<filename>/.extra/system_credentials/</filename>).</para>
<xi:include href="version-info.xml" xpointer="v255"/></listitem>
</varlistentry>
</variablelist>

View File

@@ -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();

185
src/shared/initrd-cpio.c Normal file
View File

@@ -0,0 +1,185 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <stdio.h>
#include <sys/stat.h>
#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;
}

14
src/shared/initrd-cpio.h Normal file
View File

@@ -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/<id>.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);

View File

@@ -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',

View File

@@ -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',

View File

@@ -0,0 +1,86 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#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);

View File

@@ -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.