--- /dev/null
+/* 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;
+}
--- /dev/null
+/* 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);