diff --git a/man/systemd-firstboot.xml b/man/systemd-firstboot.xml index f7719df5ceb..09089829359 100644 --- a/man/systemd-firstboot.xml +++ b/man/systemd-firstboot.xml @@ -158,6 +158,22 @@ + + + Provisions a PEM-encoded X.509 certificate for one of the kernel keyrings + systemd-keyring-setup.service8 + manages, for example to verify extension images with, as part of the addon written with + . KEYRING is one of dm-verity, + fs-verity and bpf. The file is included as credential + keyring-setup.KEYRING.NAME, + with NAME defaulting to the file name without its suffix. It must consist + of lowercase letters, digits, ., _ and -. A + = is only taken as the separator if it precedes any / in the + argument. May be used multiple times. + + + + diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index eee0e8cfc82..5ec1a24a5fe 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -67,6 +67,7 @@ #include "vconsole-util.h" #include "verbs.h" #include "vfat-util.h" +#include "voa-util.h" static char *arg_root = NULL; static char *arg_image = NULL; @@ -103,6 +104,7 @@ static bool arg_chrome = true; static bool arg_mute_console = false; static LabelContext *arg_label_context = NULL; static char *arg_addon = NULL; +static MachineCredentialContext arg_keys = {}; STATIC_DESTRUCTOR_REGISTER(arg_root, freep); STATIC_DESTRUCTOR_REGISTER(arg_image, freep); @@ -118,6 +120,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_kernel_cmdline, freep); STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_label_context, mac_label_context_freep); STATIC_DESTRUCTOR_REGISTER(arg_addon, freep); +STATIC_DESTRUCTOR_REGISTER(arg_keys, machine_credential_context_done); COMMAND( "systemd-firstboot\0", @@ -1420,6 +1423,86 @@ static int process_reset(int rfd) { return 0; } +static int parse_key_argument(const char *s) { + int r; + + assert(s); + + /* Takes "KEYRING:[NAME=]PATH". The certificate in the file becomes the credential + * "keyring-setup.KEYRING.NAME", which systemd-keyring-setup enrolls into the kernel keyring at boot, + * with NAME defaulting to the file name without its suffix. */ + + const char *e = strchr(s, ':'); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing keyring in '%s', expected KEYRING:[NAME=]PATH.", s); + + _cleanup_free_ char *keyring = strndup(s, e - s); + if (!keyring) + return log_oom(); + if (!STR_IN_SET(keyring, "dm-verity", "fs-verity", "bpf")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown keyring '%s', expected dm-verity, fs-verity or bpf.", keyring); + + /* A '=' only separates the name if it comes before any '/', so that a path containing '=' can be + * passed without a name */ + _cleanup_free_ char *name = NULL, *path = NULL; + const char *k = e + 1, *eq = strchr(k, '='), *slash = strchr(k, '/'); + if (eq && (!slash || eq < slash)) { + name = strndup(k, eq - k); + path = strdup(eq + 1); + } else { + _cleanup_free_ char *fn = NULL; + r = path_extract_filename(k, &fn); + if (r < 0) + return log_error_errno(r, "Failed to extract file name from '%s': %m", k); + + char *dot = strrchr(fn, '.'); + if (dot && dot != fn) + *dot = 0; + + name = TAKE_PTR(fn); + path = strdup(k); + } + if (!name || !path) + return log_oom(); + + if (isempty(path)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing key file path in '%s'.", s); + if (isempty(name) || name[0] == '.') + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing key name in '%s'.", s); + if (!voa_identifier_is_valid(name, /* allow_colon= */ false)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid key name '%s', expected lowercase letters, digits, '.', '_' and '-'.", name); + + _cleanup_free_ char *id = strjoin("keyring-setup.", keyring, ".", name); + if (!id) + return log_oom(); + if (!credential_name_valid(id)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid key name '%s'.", name); + + /* The file is read securely as it might not be a public key after all, and with the limit the + * credential has at boot, rather than silently cut off there */ + _cleanup_(erase_and_freep) char *data = NULL; + size_t size; + r = read_full_file_full( + AT_FDCWD, path, + /* offset= */ UINT64_MAX, + CREDENTIAL_SIZE_MAX, + READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, + /* bind_name= */ NULL, + &data, &size); + if (r < 0) + return log_error_errno(r, "Failed to read key file '%s': %m", path); + if (size == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Key file '%s' is empty.", path); + + r = machine_credential_add(&arg_keys, id, data, size); + if (r == -EEXIST) + return log_error_errno(r, "Key '%s' specified more than once.", name); + if (r < 0) + return log_error_errno(r, "Failed to add key '%s': %m", name); + + return 0; +} + static int addon_add_credential(MachineCredentialContext *creds, const char *id, const char *value) { int r; @@ -1502,6 +1585,12 @@ static int write_addon(void) { return r; } + FOREACH_ARRAY(k, arg_keys.credentials, arg_keys.n_credentials) { + r = machine_credential_add(&creds, k->id, k->data, k->size); + if (r < 0) + return log_error_errno(r, "Failed to add key '%s': %m", k->id); + } + if (creds.n_credentials == 0 && isempty(arg_kernel_cmdline)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No settings specified, refusing to write an empty addon."); @@ -1631,6 +1720,12 @@ static int parse_argv(int argc, char *argv[]) { return r; break; + OPTION_LONG("key", "KEYRING:[NAME=]PATH", "Provision a certificate for a kernel keyring via the addon"): + r = parse_key_argument(opts.arg); + if (r < 0) + return r; + break; + OPTION_LONG("force", NULL, "Overwrite existing files"): arg_force = true; break; @@ -1859,6 +1954,9 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--machine-id=/--setup-machine-id only works with --root= or --image=."); + if (arg_keys.n_credentials > 0 && !arg_addon) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--key= requires --addon=."); + if (arg_addon) { if (!arg_image) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--addon= requires --image=.");