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 8e874cb99d..2297f8a1af 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);