Read composefs configuration from initrd instead of commandline
authorAlexander Larsson <alexl@redhat.com>
Tue, 8 Aug 2023 11:16:39 +0000 (13:16 +0200)
committerAlexander Larsson <alexl@redhat.com>
Mon, 14 Aug 2023 10:27:47 +0000 (12:27 +0200)
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.

docs/composefs.md
man/ostree-prepare-root.xml
src/boot/dracut/module-setup.sh
src/switchroot/ostree-prepare-root.c

index 8f2c425e96ea09b50d551fcf32740b4bad15221c..bf2161dcc39490cc27e9f176f9233c69b65c0e3a 100644 (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
index 8726ccf18aedef2cae10634622271a25b5872592..820e6a278e9f5b948b6729cc5cc9fcae65b2f70e 100644 (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>
 
index 987e7697c7a1bf67730f54b41fb39d785d5da457..f09a56cc4ea7ab5db3caa2b03eb9329d6cb6e412 100755 (executable)
@@ -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" \
index fe080b4a5155447e5331364324208204c9d9064f..1e013613e37f4331f219a5bf6d382abf552ecca0 100644 (file)
 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;
-
-      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;
+      GBytes *pubkey = l->data;
+
+      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_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->signature_pubkey = g_strdup (ot_composefs + strlen ("signed="));
-        }
-      else if (g_str_has_prefix (ot_composefs, "digest="))
+      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->expected_digest = g_strdup (ot_composefs + strlen ("digest="));
+          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
-        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");
         }