ot-builtin-summary: Verify signature when viewing a summary file
authorPhilip Withnall <pwithnall@gnome.org>
Tue, 4 Aug 2026 15:52:53 +0000 (16:52 +0100)
committerPhilip Withnall <pwithnall@gnome.org>
Tue, 4 Aug 2026 16:31:36 +0000 (17:31 +0100)
So now we can run `ostree summary --repo . --view --gpg-homedir
/path/to/servers/gpg-homedir` and it’ll verify the GPG signatures.
That’s not so useful for verification per-se, since if you’re running
this on a server you’d expect the signatures you’ve generated to verify
correctly. It’s more useful to see how many signatures are on the
`summary` and which subkeys were used to generate them, when messing
around with multiple signing keys.

Aside from this, there is no way (that I know of) to inspect the
signatures on a summary file without checking out a copy of the
repository and hoping that the client has got a full copy of the keyring
containing all relevant subkeys.

The verification output is very similar to that of `ostree show`.

Note that signatures are not verified if running with `--raw`,
`--list-metadata-keys` or `--print-metadata-key`, since clients may be
parsing the output of those commands.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>
src/ostree/ot-builtin-summary.c
tests/test-summary-view.sh

index d131f80ba9ff6e3c81c40f5d75b776a0402f4b46..f899d91ca7e52ae4e1615e31d09231007750b30d 100644 (file)
@@ -93,10 +93,14 @@ build_additional_metadata (const char *const *args, GError **error)
   return g_variant_ref_sink (g_variant_builder_end (builder));
 }
 
+/* @out_summary_data is guaranteed to return non-NULL, but @out_summary_sig_data
+ * may return NULL (with no error) if the summary is unsigned. */
 static gboolean
-get_summary_data (OstreeRepo *repo, GBytes **out_summary_data, GError **error)
+get_summary_data (OstreeRepo *repo, GBytes **out_summary_data, GBytes **out_summary_sig_data,
+                  GError **error)
 {
   g_assert (out_summary_data != NULL);
+  g_assert (out_summary_sig_data != NULL);
 
   g_autoptr (GBytes) summary_data = NULL;
   glnx_autofd int fd = -1;
@@ -106,7 +110,19 @@ get_summary_data (OstreeRepo *repo, GBytes **out_summary_data, GError **error)
   if (!summary_data)
     return FALSE;
 
+  g_autoptr (GBytes) summary_sig_data = NULL;
+  glnx_autofd int sig_fd = -1;
+  if (!ot_openat_ignore_enoent (repo->repo_dir_fd, "summary.sig", &sig_fd, error))
+    return FALSE;
+  if (sig_fd >= 0)
+    {
+      summary_sig_data = ot_fd_readall_or_mmap (sig_fd, 0, error);
+      if (!summary_sig_data)
+        return FALSE;
+    }
+
   *out_summary_data = g_steal_pointer (&summary_data);
+  *out_summary_sig_data = g_steal_pointer (&summary_sig_data);
 
   return TRUE;
 }
@@ -119,6 +135,9 @@ ostree_builtin_summary (int argc, char **argv, OstreeCommandInvocation *invocati
   g_autoptr (OstreeRepo) repo = NULL;
   g_autoptr (OstreeSign) sign = NULL;
   OstreeDumpFlags flags = OSTREE_DUMP_NONE;
+  g_autoptr (GBytes) summary_data = NULL;
+  g_autoptr (GBytes) summary_sig_data = NULL;
+  gboolean show_signatures = FALSE;
 
   context = g_option_context_new ("");
 
@@ -188,34 +207,32 @@ ostree_builtin_summary (int argc, char **argv, OstreeCommandInvocation *invocati
     }
   else if (opt_view || opt_raw)
     {
-      g_autoptr (GBytes) summary_data = NULL;
-
       if (opt_raw)
         flags |= OSTREE_DUMP_RAW;
 
-      if (!get_summary_data (repo, &summary_data, error))
+      if (!get_summary_data (repo, &summary_data, &summary_sig_data, error))
         return FALSE;
 
       ot_dump_summary_bytes (summary_data, flags);
+      show_signatures = !opt_raw;
     }
   else if (opt_list_metadata_keys)
     {
-      g_autoptr (GBytes) summary_data = NULL;
-
-      if (!get_summary_data (repo, &summary_data, error))
+      if (!get_summary_data (repo, &summary_data, &summary_sig_data, error))
         return FALSE;
 
       ot_dump_summary_metadata_keys (summary_data);
+      show_signatures = FALSE;
     }
   else if (opt_print_metadata_key)
     {
-      g_autoptr (GBytes) summary_data = NULL;
-
-      if (!get_summary_data (repo, &summary_data, error))
+      if (!get_summary_data (repo, &summary_data, &summary_sig_data, error))
         return FALSE;
 
       if (!ot_dump_summary_metadata_key (summary_data, opt_print_metadata_key, error))
         return FALSE;
+
+      show_signatures = FALSE;
     }
   else
     {
@@ -224,5 +241,49 @@ ostree_builtin_summary (int argc, char **argv, OstreeCommandInvocation *invocati
       return FALSE;
     }
 
+#ifndef OSTREE_DISABLE_GPGME
+  if (show_signatures && summary_sig_data == NULL)
+    {
+      g_print ("Summary is unsigned\n");
+    }
+  else if (show_signatures)
+    {
+      g_autoptr (OstreeGpgVerifyResult) result = NULL;
+      g_autoptr (GFile) gpg_homedir
+          = opt_gpg_homedir ? g_file_new_for_path (opt_gpg_homedir) : NULL;
+      g_autoptr (GError) local_error = NULL;
+
+      g_assert (summary_data != NULL && summary_sig_data != NULL);
+
+      result = ostree_repo_verify_local_summary (repo, summary_data, summary_sig_data, gpg_homedir,
+                                                 NULL, cancellable, &local_error);
+
+      if (g_error_matches (local_error, OSTREE_GPG_ERROR, OSTREE_GPG_ERROR_NO_SIGNATURE))
+        {
+          /* Ignore */
+        }
+      else if (local_error != NULL)
+        {
+          g_propagate_error (error, g_steal_pointer (&local_error));
+          return FALSE;
+        }
+      else
+        {
+          unsigned int n_sigs = ostree_gpg_verify_result_count_all (result);
+          g_print ("\nFound %u signature%s on the summary:\n", n_sigs, n_sigs == 1 ? "" : "s");
+
+          g_autoptr (GString) buffer = g_string_sized_new (256);
+          for (unsigned int ii = 0; ii < n_sigs; ii++)
+            {
+              g_string_append_c (buffer, '\n');
+              ostree_gpg_verify_result_describe (result, ii, buffer, "  ",
+                                                 OSTREE_GPG_SIGNATURE_FORMAT_DEFAULT);
+            }
+
+          g_print ("%s", buffer->str);
+        }
+    }
+#endif /* OSTREE_DISABLE_GPGME */
+
   return TRUE;
 }
index 473c6db54ef80c08dc7cdf9fcb889749eef1ffa3..c1d81bdd942608a754064309e77f7b330d356013 100755 (executable)
@@ -27,8 +27,10 @@ set -euo pipefail
 echo "1..2"
 
 COMMIT_SIGN=""
+SUMMARY_HOMEDIR=""
 if has_ostree_feature gpgme; then
     COMMIT_SIGN="--gpg-homedir=${TEST_GPG_KEYHOME} --gpg-sign=${TEST_GPG_KEYID_1}"
+    SUMMARY_HOMEDIR="--gpg-homedir=${TEST_GPG_KEYHOME}"
 fi
 
 setup_fake_remote_repo1 "archive" "${COMMIT_SIGN}"
@@ -40,7 +42,7 @@ echo 'hello world some object' > hello-world
 ${CMD_PREFIX} ostree  --repo=${test_tmpdir}/ostree-srv/gnomerepo commit ${COMMIT_SIGN} -b other -s "A commit" -m "Example commit body"
 
 # Generate the summary file.
-${CMD_PREFIX} ostree --repo=${test_tmpdir}/ostree-srv/gnomerepo summary -u
+${CMD_PREFIX} ostree --repo=${test_tmpdir}/ostree-srv/gnomerepo summary -u ${COMMIT_SIGN}
 
 # Check out the repository.
 prev_dir=`pwd`
@@ -51,17 +53,22 @@ ${CMD_PREFIX} ostree --repo=repo pull --mirror origin
 
 # Check the summary file exists in the checkout, and can be viewed.
 assert_has_file repo/summary
-${OSTREE} summary --view > summary.txt
+${OSTREE} summary ${SUMMARY_HOMEDIR} --view > summary.txt
 assert_file_has_content_literal summary.txt "* main"
 assert_file_has_content_literal summary.txt "* other"
 assert_file_has_content_literal summary.txt "ostree.summary.last-modified"
 assert_file_has_content_literal summary.txt "Timestamp (ostree.commit.timestamp): "
 assert_file_has_content_literal summary.txt "Version (ostree.commit.version): 3.2"
+if has_ostree_feature gpgme; then
+    assert_file_has_content_literal summary.txt "Good signature from"
+fi
 echo "ok view summary"
 
-# Check the summary can be viewed raw too.
-${OSTREE} summary --raw > raw-summary.txt
+# Check the summary can be viewed raw too, but that it doesn’t include signature information.
+${OSTREE} summary ${SUMMARY_HOMEDIR} --raw > raw-summary.txt
 assert_file_has_content_literal raw-summary.txt "('main', ("
 assert_file_has_content_literal raw-summary.txt "('other', ("
 assert_file_has_content_literal raw-summary.txt "'ostree.summary.last-modified': <uint64"
+assert_not_file_has_content raw-summary.txt "Found [0-9]+ signature"
+assert_not_file_has_content raw-summary.txt "Summary is unsigned"
 echo "ok view summary raw"