From 6b76a3b930b794b5c43bed3b27cb0b0f65b96d08 Mon Sep 17 00:00:00 2001
From: Simon McVittie <smcv@collabora.com>
Date: Wed, 31 Jul 2024 13:15:30 +0100
Subject: [PATCH] pv-runtime: Add more robust mechanism to take CA certificates
 from the host

This is a redo of commit d6a519f5 "pv-runtime: Add a mechanism to take
custom CA certificates from the host" which aims to be more robust against
host operating systems that might not have the Debian-compatible layout
that we expect.

This reverts commit 9e3d9c85a6fb855993a8e505d1bce474797bd444.

steamrt/tasks#416

Signed-off-by: Simon McVittie <smcv@collabora.com>
---
 pressure-vessel/runtime.c      | 126 +++++++++++++++++++++++++++++++++
 pressure-vessel/runtime.h      |   4 ++
 pressure-vessel/wrap-context.c |   3 +
 pressure-vessel/wrap-context.h |   1 +
 pressure-vessel/wrap.c         |   3 +
 5 files changed, 137 insertions(+)

diff --git a/pressure-vessel/runtime.c b/pressure-vessel/runtime.c
index ed51f95fc..7f3ee72e6 100644
--- a/pressure-vessel/runtime.c
+++ b/pressure-vessel/runtime.c
@@ -3062,6 +3062,118 @@ bind_gfx_provider (PvRuntime *self,
   return TRUE;
 }
 
+static FlatpakBwrap *
+pv_runtime_import_ca_certs (PvRuntime *self,
+                            GError **error)
+{
+  static const char ca_path[] = "/etc/ssl/certs";
+  static const char * const required_names[] =
+  {
+    /* /etc/ssl/certs/ca-certificates.crt is assumed to be an
+     * OpenSSL-compatible CAfile (concatenation of all trusted root certs),
+     * also used by other TLS libraries like GNUTLS */
+    "ca-certificates.crt",
+    /* /etc/ssl/certs/ is assumed to be an OpenSSL-compatible CApath
+     * (one file per trusted root cert with names based on a truncated
+     * hash), mainly only used by OpenSSL. This is the hash for
+     * "ISRG Root X1", the root CA behind Let's Encrypt, which happens
+     * to be the CA used to sign repo.steampowered.com at the time
+     * of writing... */
+    "4042bcee.0",
+    /* ... and this is the hash for "DigiCert High Assurance EV Root CA"
+     * which happens to be the CA used to sign store.steampowered.com.
+     * If both are present, then we assume all the other common CAs
+     * are too. */
+    "244b5494.0",
+    /* Get these hashes from:
+     * openssl x509 -noout -subject_hash -in /path/to/cert.crt */
+  };
+  gboolean found[G_N_ELEMENTS (required_names)] = { FALSE };
+  g_autoptr(FlatpakBwrap) bwrap = flatpak_bwrap_new (flatpak_bwrap_empty_env);
+  g_auto(SrtDirIter) iter = SRT_DIR_ITER_CLEARED;
+  glnx_autofd int dirfd = -1;
+  struct dirent *dent;
+  size_t i;
+
+  flatpak_bwrap_add_args (bwrap,
+                          "--tmpfs", ca_path,
+                          NULL);
+
+  /* This is a developer-facing rather than end-user-facing flag, so
+   * for simplicity this assumes that the runtime is Debian-based, and
+   * that the host OS has also been set up to be compatible with
+   * Debian's layout for CA certificates (like Arch is). */
+  dirfd = _srt_sysroot_open (self->host_root,
+                             ca_path,
+                             (SRT_RESOLVE_FLAGS_MUST_BE_DIRECTORY
+                              | SRT_RESOLVE_FLAGS_READABLE),
+                             NULL,
+                             error);
+
+  if (dirfd < 0)
+    return NULL;
+
+  /* Check that we have a minimal Debian-compatible layout. If we don't,
+   * we'll just fail and the caller will have to deal with that. */
+
+  if (!_srt_dir_iter_init_take_fd (&iter, &dirfd,
+                                   SRT_DIR_ITER_FLAGS_NONE,
+                                   self->arbitrary_dirent_order,
+                                   error))
+    return NULL;
+
+  while (_srt_dir_iter_next_dent (&iter, &dent, NULL, NULL) && dent != NULL)
+    {
+      const char *member = dent->d_name;
+
+      if (g_str_equal (member, "ca-certificates.crt")
+          || g_str_has_suffix (member, ".0"))
+        {
+          g_autoptr(GError) local_error = NULL;
+          g_autofree gchar *logical_path = NULL;
+          g_autofree gchar *resolved = NULL;
+          glnx_autofd int fd = -1;
+
+          logical_path = g_build_filename (ca_path, member, NULL);
+          fd = _srt_sysroot_open (self->host_root,
+                                  logical_path,
+                                  (SRT_RESOLVE_FLAGS_READABLE
+                                   | SRT_RESOLVE_FLAGS_MUST_BE_REGULAR
+                                   | SRT_RESOLVE_FLAGS_RETURN_ABSOLUTE),
+                                  &resolved,
+                                  &local_error);
+
+          if (fd < 0)
+            {
+              g_warning ("%s", local_error->message);
+              continue;
+            }
+
+          if (!pv_runtime_bind_into_container (self, bwrap,
+                                               resolved, NULL, 0,
+                                               logical_path,
+                                               PV_RUNTIME_EMULATION_ROOTS_BOTH,
+                                               error))
+            return NULL;
+
+          for (i = 0; i < G_N_ELEMENTS (required_names); i++)
+            {
+              if (g_str_equal (member, required_names[i]))
+                found[i] = TRUE;
+            }
+        }
+    }
+
+  for (i = 0; i < G_N_ELEMENTS (required_names); i++)
+    {
+      if (!found[i])
+        return glnx_null_throw (error, "Required filename %s/%s not found",
+                                ca_path, required_names[i]);
+    }
+
+  return g_steal_pointer (&bwrap);
+}
+
 static gboolean
 bind_runtime_base (PvRuntime *self,
                    FlatpakExports *exports,
@@ -3425,6 +3537,20 @@ bind_runtime_base (PvRuntime *self,
         }
     }
 
+  if (self->flags & PV_RUNTIME_FLAGS_IMPORT_CA_CERTS)
+    {
+      g_autoptr(FlatpakBwrap) ca_args = NULL;
+      g_autoptr(GError) local_error = NULL;
+
+      ca_args = pv_runtime_import_ca_certs (self, &local_error);
+
+      if (ca_args != NULL)
+        flatpak_bwrap_append_bwrap (bwrap, ca_args);
+      else
+        g_warning ("Not importing host CA certificates: %s",
+                   local_error->message);
+    }
+
   return TRUE;
 }
 
diff --git a/pressure-vessel/runtime.h b/pressure-vessel/runtime.h
index 2ac21d8ee..6af7e7255 100644
--- a/pressure-vessel/runtime.h
+++ b/pressure-vessel/runtime.h
@@ -50,6 +50,8 @@
  *  root filesystem overlay for an interpreter like FEX-Emu
  * @PV_RUNTIME_FLAGS_DETERMINISTIC: Try harder to achieve deterministic
  *  order, even where it shouldn't matter functionally
+ * @PV_RUNTIME_FLAGS_IMPORT_CA_CERTS: Try to import CA certificates from
+ *  the host system, which is assumed to be Debian-compatible
  * @PV_RUNTIME_FLAGS_NONE: None of the above
  *
  * Flags affecting how we set up the runtime.
@@ -66,6 +68,7 @@ typedef enum
   PV_RUNTIME_FLAGS_FLATPAK_SUBSANDBOX = (1 << 7),
   PV_RUNTIME_FLAGS_INTERPRETER_ROOT = (1 << 8),
   PV_RUNTIME_FLAGS_DETERMINISTIC = (1 << 9),
+  PV_RUNTIME_FLAGS_IMPORT_CA_CERTS = (1 << 10),
   PV_RUNTIME_FLAGS_NONE = 0
 } PvRuntimeFlags;
 
@@ -86,6 +89,7 @@ typedef enum
    | PV_RUNTIME_FLAGS_FLATPAK_SUBSANDBOX \
    | PV_RUNTIME_FLAGS_INTERPRETER_ROOT \
    | PV_RUNTIME_FLAGS_DETERMINISTIC \
+   | PV_RUNTIME_FLAGS_IMPORT_CA_CERTS \
    )
 
 typedef enum
diff --git a/pressure-vessel/wrap-context.c b/pressure-vessel/wrap-context.c
index 6e83c03bb..3b2621576 100644
--- a/pressure-vessel/wrap-context.c
+++ b/pressure-vessel/wrap-context.c
@@ -56,6 +56,7 @@ pv_wrap_options_init (PvWrapOptions *self)
   self->gc_runtimes = TRUE;
   self->generate_locales = TRUE;
   self->graphics_provider = FALSE;
+  self->import_ca_certs = FALSE;
   self->import_vulkan_layers = TRUE;
   self->launcher = FALSE;
   self->only_prepare = FALSE;
@@ -537,6 +538,8 @@ pv_wrap_options_parse_environment (PvWrapOptions *self,
                                                         self->remove_game_overlay);
   self->systemd_scope = _srt_boolean_environment ("PRESSURE_VESSEL_SYSTEMD_SCOPE",
                                                   self->systemd_scope);
+  self->import_ca_certs = _srt_boolean_environment ("PRESSURE_VESSEL_IMPORT_CA_CERTS",
+                                                    self->import_ca_certs);
   self->import_vulkan_layers = _srt_boolean_environment ("PRESSURE_VESSEL_IMPORT_VULKAN_LAYERS",
                                                          self->import_vulkan_layers);
 
diff --git a/pressure-vessel/wrap-context.h b/pressure-vessel/wrap-context.h
index 644194da3..56d5bcfd7 100644
--- a/pressure-vessel/wrap-context.h
+++ b/pressure-vessel/wrap-context.h
@@ -79,6 +79,7 @@ typedef struct
   gboolean devel;
   gboolean gc_runtimes;
   gboolean generate_locales;
+  gboolean import_ca_certs;
   gboolean import_vulkan_layers;
   gboolean launcher;
   gboolean only_prepare;
diff --git a/pressure-vessel/wrap.c b/pressure-vessel/wrap.c
index 1411570e6..8657f904c 100644
--- a/pressure-vessel/wrap.c
+++ b/pressure-vessel/wrap.c
@@ -629,6 +629,9 @@ main (int argc,
       if (_srt_util_is_debugging ())
         flags |= PV_RUNTIME_FLAGS_VERBOSE;
 
+      if (self->options.import_ca_certs)
+        flags |= PV_RUNTIME_FLAGS_IMPORT_CA_CERTS;
+
       if (self->options.import_vulkan_layers)
         flags |= PV_RUNTIME_FLAGS_IMPORT_VULKAN_LAYERS;
 
-- 
GitLab