Read composefs configuration from initrd instead of commandline

This drops the `ot-composefs` kernel commandline in favour
of a `[composefs]` section in the `prepare-rootfs.conf` file.

You can set `composefs.enabled` to `signed`, `yes`, `no` or `maybe`,
with `maybe` being the default.

You can also set `composefs.keypath` (or rely on the default
`/etc/ostree/initramfs-root-binding.key`) to point to ed25519 public
keys, one of which which the commit must be signed with, or boot
fails.

The ostree dracut module adds `/etc/ostree/initramfs-root-binding.key`
to the initrd if it exists.

NOTE: This drop the option to define a digest in the commandline.
However, that was currently unused
(i.e. ComposefsConfig.expected_digest was never read).

Additionally it very hard to actually store the composefs digest in
the initrd, as the initrd is typically part of the commit and thus the
composefs. It may be possible to handle this, but lets add it back
when we know exactly how that will work.
This commit is contained in:
Alexander Larsson 2023-08-08 13:16:39 +02:00
parent 2cc6b53199
commit 81fa214155
4 changed files with 117 additions and 84 deletions

View File

@ -30,21 +30,13 @@ have a `.ostree.cfs` file in the deployment directory which is a mountable
composefs metadata file, with a "backing store" directory that is
shared with the current `/ostree/repo/objects`.
### Kernel argument ot-composefs
### composefs configuration
The `ostree-prepare-root` binary will look for a kernel argument called `ot-composefs`.
The `ostree-prepare-root` binary will look for `ostree/prepare-root.conf` in `/etc` and
`/usr/lib` in the initramfs. Using that configuration file you can enable composefs,
and specify an Ed25519 public key to validate the booted commit.
The default value is `maybe` (this will likely become a build and initramfs-configurable option)
in the future too.
The possible values are:
- `off`: Never use composefs
- `maybe`: Use composefs if supported and there is a composefs image in the deployment directory
- `on`: Require composefs
- `digest=<sha256>`: Require the mounted composefs image to have a particular digest
- `signed=<path>`: Require that the commit is signed as validated by the ed25519 public key specified
by `path` (the path is resolved in the initrd).
See the manpage for `ostree-prepare-root` for details of how to configure it.
### Injecting composefs digests
@ -56,20 +48,20 @@ covering the composefs fsverity digest with a signature.
### Signatures
If a commit is signed with a ed25519 private key (see `ostree
--sign`), and `signed=/path/to/public.key` is specified on the
commandline, then the initrd will find the commit being booted in the
system repo and validate its signature against the public key. It will
then ensure that the composefs digest being booted has an fs-verity
digest matching the one in the commit. This allows a fully trusted
read-only /usr.
If a commit is signed with an Ed25519 private key (see `ostree
--sign`), and `composefs.keyfile` is specified in `prepare-root.conf`,
then the initrd will find the commit being booted in the system repo
and validate its signature against the public key. It will then ensure
that the composefs digest being booted has an fs-verity digest
matching the one in the commit. This allows a fully trusted read-only
/usr.
The exact usage of the signature is up to the user, but a common way
to use it with transien keys. This is done like this:
to use it with transient keys. This is done like this:
* Generate a new keypair before each build
* Embed the public key in the initrd that is part of the commit.
* Ensure the kernel commandline has `ot-signed=/path/to/key`
* After commiting, run `ostree --sign` with the private key.
* Ensure the initrd has a `prepare-root.conf` with `keyfile=/path/to/key`
* After committing, run `ostree --sign` with the private key.
* Throw away the private key.
When a transient key is used this way, that ties the initrd with the

View File

@ -32,7 +32,7 @@ License along with this library. If not, see <https://www.gnu.org/licenses/>.
<surname>Walters</surname>
<email>walters@verbum.org</email>
</author>
</authorgroup>g
</authorgroup>
</refentryinfo>
<refmeta>
@ -111,8 +111,25 @@ License along with this library. If not, see <https://www.gnu.org/licenses/>.
<variablelist>
<varlistentry>
<term><varname>sysroot.readonly</varname></term>
<listitem><para>A boolean value; the default is false. If this is set to <literal>true</literal>, then the <literal>/sysroot</literal> mount point is mounted read-only.</para></listitem>
</varlistentry>
<listitem><para>A boolean value; the default is <literal>false</literal>. If this is set to <literal>true</literal>, then the <literal>/sysroot</literal> mount point is mounted read-only.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>composefs.enabled</varname></term>
<listitem><para>This can be <literal>yes</literal>, <literal>no</literal>. <literal>maybe</literal> or
<literal>signed</literal>. The default is <literal>maybe</literal>. If set to <literal>yes</literal> or
<literal>signed</literal>, then composefs is always used, and the boot fails if it is not
available. Additionally if set to <literal>signed</literal>, boot will fail if the image cannot be
validated by a public key. If set to <literal>maybe</literal>, then composefs is used if supported.
</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>composefs.keypath</varname></term>
<listitem><para>Path to a file with Ed25519 public keys in the initramfs, used if
<literal>composefs.enabled</literal> is set to <literal>signed</literal>. The default value for this is
<literal>/etc/ostree/initramfs-root-binding.key</literal>. For a valid signed boot the target OSTree
commit must be signed by at least one public key in this file, and the commitfs digest listed in the
commit must match the target composefs image.</para></listitem>
</varlistentry>
</variablelist>
</refsect1>

View File

@ -38,6 +38,9 @@ install() {
inst_simple "$r/ostree/prepare-root.conf"
fi
done
if test -f "/etc/ostree/initramfs-root-binding.key"; then
inst_simple "/etc/ostree/initramfs-root-binding.key"
fi
inst_simple "${systemdsystemunitdir}/ostree-prepare-root.service"
mkdir -p "${initdir}${systemdsystemconfdir}/initrd-root-fs.target.wants"
ln_r "${systemdsystemunitdir}/ostree-prepare-root.service" \

View File

@ -80,11 +80,14 @@
const char *config_roots[] = { "/usr/lib", "/etc" };
#define PREPARE_ROOT_CONFIG_PATH "ostree/prepare-root.conf"
#define DEFAULT_KEYPATH "/etc/ostree/initramfs-root-binding.key"
#define SYSROOT_KEY "sysroot"
#define READONLY_KEY "readonly"
// The kernel argument we support to configure composefs.
#define OT_COMPOSEFS_KARG "ot-composefs"
#define COMPOSEFS_KEY "composefs"
#define ENABLED_KEY "enabled"
#define KEYPATH_KEY "keypath"
#define OSTREE_PREPARE_ROOT_DEPLOYMENT_MSG \
SD_ID128_MAKE (71, 70, 33, 6a, 73, ba, 46, 01, ba, d3, 1a, f8, 88, aa, 0d, f7)
@ -250,21 +253,24 @@ load_commit_for_deploy (const char *root_mountpoint, const char *deploy_path, GV
}
static gboolean
validate_signature (GBytes *data, GVariant *signatures, const guchar *pubkey, size_t pubkey_size)
validate_signature (GBytes *data, GVariant *signatures, GList *pubkeys)
{
g_autoptr (GBytes) pubkey_buf = g_bytes_new_static (pubkey, pubkey_size);
for (gsize i = 0; i < g_variant_n_children (signatures); i++)
for (GList *l = pubkeys; l != NULL; l = l->next)
{
g_autoptr (GError) local_error = NULL;
g_autoptr (GVariant) child = g_variant_get_child_value (signatures, i);
g_autoptr (GBytes) signature = g_variant_get_data_as_bytes (child);
bool valid = false;
GBytes *pubkey = l->data;
if (!otcore_validate_ed25519_signature (data, pubkey_buf, signature, &valid, &local_error))
errx (EXIT_FAILURE, "signature verification failed: %s", local_error->message);
if (valid)
return TRUE;
for (gsize i = 0; i < g_variant_n_children (signatures); i++)
{
g_autoptr (GError) local_error = NULL;
g_autoptr (GVariant) child = g_variant_get_child_value (signatures, i);
g_autoptr (GBytes) signature = g_variant_get_data_as_bytes (child);
bool valid = false;
if (!otcore_validate_ed25519_signature (data, pubkey, signature, &valid, &local_error))
errx (EXIT_FAILURE, "signature verification failed: %s", local_error->message);
if (valid)
return TRUE;
}
}
return FALSE;
@ -274,53 +280,76 @@ validate_signature (GBytes *data, GVariant *signatures, const guchar *pubkey, si
typedef struct
{
OtTristate enabled;
gboolean is_signed;
char *signature_pubkey;
char *expected_digest;
GList *pubkeys;
} ComposefsConfig;
static void
free_composefs_config (ComposefsConfig *config)
{
free (config->signature_pubkey);
free (config->expected_digest);
free (config);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC (ComposefsConfig, free_composefs_config)
static ComposefsConfig *
load_composefs_config (GError **error)
load_composefs_config (GKeyFile *config, GError **error)
{
GLNX_AUTO_PREFIX_ERROR ("Loading composefs config", error);
g_autoptr (ComposefsConfig) ret = g_new0 (ComposefsConfig, 1);
ret->enabled = OT_TRISTATE_MAYBE;
// TODO: Drop this kernel argument in favor of just the config file in the initramfs
autofree char *ot_composefs = read_proc_cmdline_key (OT_COMPOSEFS_KARG);
if (ot_composefs)
g_autoptr (ComposefsConfig) ret = g_new0 (ComposefsConfig, 1);
g_autofree char *enabled = g_key_file_get_value (config, COMPOSEFS_KEY, ENABLED_KEY, NULL);
if (g_strcmp0 (enabled, "signed") == 0)
{
if (strcmp (ot_composefs, "off") == 0)
ret->enabled = OT_TRISTATE_NO;
else if (strcmp (ot_composefs, "maybe") == 0)
ret->enabled = OT_TRISTATE_MAYBE;
else if (strcmp (ot_composefs, "on") == 0)
ret->enabled = OT_TRISTATE_YES;
else if (g_str_has_prefix (ot_composefs, "signed="))
ret->enabled = OT_TRISTATE_YES;
ret->is_signed = true;
}
else if (!ot_keyfile_get_tristate_with_default (config, COMPOSEFS_KEY, ENABLED_KEY,
OT_TRISTATE_MAYBE, &ret->enabled, error))
return NULL;
if (!ot_keyfile_get_value_with_default (config, COMPOSEFS_KEY, KEYPATH_KEY, DEFAULT_KEYPATH,
&ret->signature_pubkey, error))
return NULL;
if (ret->is_signed)
{
g_autofree char *pubkeys = NULL;
gsize pubkeys_size;
/* Load keys */
if (!g_file_get_contents (ret->signature_pubkey, &pubkeys, &pubkeys_size, error))
return glnx_prefix_error_null (error, "Reading public key file '%s'",
ret->signature_pubkey);
/* Raw binary form if right size */
if (pubkeys_size == OSTREE_SIGN_ED25519_PUBKEY_SIZE)
ret->pubkeys = g_list_append (ret->pubkeys,
g_bytes_new_take (g_steal_pointer (&pubkeys), pubkeys_size));
else /* otherwise text with base64 key per line */
{
ret->enabled = OT_TRISTATE_YES;
ret->signature_pubkey = g_strdup (ot_composefs + strlen ("signed="));
g_auto (GStrv) lines = g_strsplit (pubkeys, "\n", -1);
for (char **iter = lines; *iter; iter++)
{
const char *line = *iter;
if (strlen (line) > 0)
{
g_autofree guchar *pubkey = NULL;
gsize pubkey_size;
pubkey = g_base64_decode (line, &pubkey_size);
ret->pubkeys = g_list_append (
ret->pubkeys, g_bytes_new_take (g_steal_pointer (&pubkey), pubkey_size));
}
}
}
else if (g_str_has_prefix (ot_composefs, "digest="))
{
ret->enabled = OT_TRISTATE_YES;
ret->expected_digest = g_strdup (ot_composefs + strlen ("digest="));
}
else
return glnx_null_throw (error, "Unsupported %s option: '%s'", OT_COMPOSEFS_KARG,
ot_composefs);
// In theory it's OK to have both a signature and an expected digest,
// but since there's no valid reason to do both, let's not support it.
g_assert (!(ret->signature_pubkey && ret->expected_digest));
if (ret->pubkeys == NULL)
return glnx_null_throw (error, "public key file specified, but no public keys found");
}
return g_steal_pointer (&ret);
@ -347,7 +376,7 @@ main (int argc, char *argv[])
// We always parse the composefs config, because we want to detect and error
// out if it's enabled, but not supported at compile time.
g_autoptr (ComposefsConfig) composefs_config = load_composefs_config (&error);
g_autoptr (ComposefsConfig) composefs_config = load_composefs_config (config, &error);
if (!composefs_config)
errx (EXIT_FAILURE, "%s", error->message);
@ -425,22 +454,15 @@ main (int argc, char *argv[])
1,
};
g_autofree char *expected_digest_owned = NULL;
const char *expected_digest = expected_digest_owned;
if (composefs_config->signature_pubkey)
g_autofree char *expected_digest = NULL;
if (composefs_config->is_signed)
{
g_assert (expected_digest == NULL);
const char *composefs_pubkey = composefs_config->signature_pubkey;
g_autoptr (GError) local_error = NULL;
g_autofree char *pubkey = NULL;
gsize pubkey_size;
g_autoptr (GVariant) commit = NULL;
g_autoptr (GVariant) commitmeta = NULL;
if (!g_file_get_contents (composefs_pubkey, &pubkey, &pubkey_size, &local_error))
errx (EXIT_FAILURE, "Failed to load public key '%s': %s", composefs_pubkey,
local_error->message);
if (!load_commit_for_deploy (root_mountpoint, deploy_path, &commit, &commitmeta,
&local_error))
errx (EXIT_FAILURE, "Error loading signatures from repo: %s", local_error->message);
@ -451,7 +473,7 @@ main (int argc, char *argv[])
errx (EXIT_FAILURE, "Signature validation requested, but no signatures in commit");
g_autoptr (GBytes) commit_data = g_variant_get_data_as_bytes (commit);
if (!validate_signature (commit_data, signatures, (guchar *)pubkey, pubkey_size))
if (!validate_signature (commit_data, signatures, composefs_config->pubkeys))
errx (EXIT_FAILURE, "No valid signatures found for public key");
g_print ("composefs+ostree: Validated commit signature using '%s'\n", composefs_pubkey);
@ -468,9 +490,8 @@ main (int argc, char *argv[])
if (!cfs_digest_buf)
errx (EXIT_FAILURE, "Failed to query digest: %s", error->message);
expected_digest_owned = g_malloc (OSTREE_SHA256_STRING_LEN + 1);
ot_bin2hex (expected_digest_owned, cfs_digest_buf, g_variant_get_size (cfs_digest_v));
expected_digest = expected_digest_owned;
expected_digest = g_malloc (OSTREE_SHA256_STRING_LEN + 1);
ot_bin2hex (expected_digest, cfs_digest_buf, g_variant_get_size (cfs_digest_v));
}
cfs_options.flags = LCFS_MOUNT_FLAGS_READONLY;
@ -489,7 +510,7 @@ main (int argc, char *argv[])
// If we're not verifying a digest, then we *must* also have signatures disabled.
// Or stated in reverse: if signature verification is enabled, then digest verification
// must also be.
g_assert (!composefs_config->signature_pubkey);
g_assert (!composefs_config->is_signed);
g_print ("composefs: Mounting with no digest or signature check\n");
}