mirror of
https://github.com/systemd/systemd.git
synced 2026-06-30 19:57:29 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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
185
src/shared/initrd-cpio.c
Normal 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
14
src/shared/initrd-cpio.h
Normal 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);
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
86
src/test/test-initrd-cpio.c
Normal file
86
src/test/test-initrd-cpio.c
Normal 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);
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user