File NetworkManager-CVE-2025-9615.patch of Package NetworkManager

From 4fa8aa1599c01b37a24127b304eded8b741f143f Mon Sep 17 00:00:00 2001
From: Jonathan Kang <jonathankang@gnome.org>
Date: Thu, 19 Mar 2026 14:47:30 +0800
Subject: [PATCH] CVE-2025-9615

---
 Makefile.am                                   |  26 +++
 libnm-core/nm-core-internal.h                 |   8 +
 libnm-core/nm-setting-8021x.c                 | 112 +++++++++-
 libnm-core/nm-setting-connection.c            |  41 ++++
 libnm-core/nm-setting-private.h               |   5 +
 libnm-core/nm-setting.c                       |  29 +++
 libnm-core/nm-setting.h                       |   5 +
 src/devices/nm-device-ethernet.c              |   4 +-
 src/devices/nm-device-macsec.c                |   4 +-
 src/devices/nm-device-private.h               |   2 +
 src/devices/nm-device.c                       | 125 +++++++++++
 src/devices/wifi/nm-device-wifi.c             |   4 +-
 src/meson.build                               |   1 +
 src/nm-core-utils.c                           | 182 ++++++++++++++++
 src/nm-core-utils.h                           |  15 ++
 src/nm-daemon-helper/README.md                |  10 +
 src/nm-daemon-helper/meson.build              |  15 ++
 src/nm-daemon-helper/nm-daemon-helper.c       | 193 +++++++++++++++++
 src/supplicant/nm-supplicant-config.c         | 148 +++++++++----
 src/supplicant/nm-supplicant-config.h         |   5 +-
 src/supplicant/nm-supplicant-interface.c      | 194 +++++++++++++++---
 src/supplicant/tests/test-supplicant-config.c |   3 +-
 22 files changed, 1058 insertions(+), 73 deletions(-)
 create mode 100644 src/nm-daemon-helper/README.md
 create mode 100644 src/nm-daemon-helper/meson.build
 create mode 100644 src/nm-daemon-helper/nm-daemon-helper.c

diff --git a/Makefile.am b/Makefile.am
index b37aa4e712..d6c32018ca 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -4156,6 +4156,32 @@ EXTRA_DIST += \
 	dispatcher/tests/meson.build \
 	$(NULL)
 
+###############################################################################
+# src/nm-daemon-helper
+###############################################################################
+
+libexec_PROGRAMS += src/nm-daemon-helper/nm-daemon-helper
+
+src_nm_daemon_helper_nm_daemon_helper_CPPFLAGS = \
+	$(dflt_cppflags) \
+	-I$(srcdir)/shared \
+	-I$(srcdir)/src \
+	-I$(builddir)/src \
+	$(NULL)
+
+src_nm_daemon_helper_nm_daemon_helper_LDFLAGS = \
+	-Wl,--version-script="$(srcdir)/linker-script-binary.ver" \
+	$(SANITIZER_EXEC_LDFLAGS) \
+	$(NULL)
+
+src_nm_daemon_helper_nm_daemon_helper_LDADD = \
+	shared/nm-std-aux/libnm-std-aux.la \
+	$(NULL)
+
+EXTRA_DIST += \
+	src/nm-daemon-helper/meson.build \
+	$(NULL)
+
 ###############################################################################
 # clients
 ###############################################################################
diff --git a/libnm-core/nm-core-internal.h b/libnm-core/nm-core-internal.h
index b7222df20a..dfc7e399de 100644
--- a/libnm-core/nm-core-internal.h
+++ b/libnm-core/nm-core-internal.h
@@ -901,4 +901,12 @@ gboolean _nmtst_variant_attribute_spec_assert_sorted (const NMVariantAttributeSp
 const NMVariantAttributeSpec *_nm_variant_attribute_spec_find_binary_search (const NMVariantAttributeSpec *const*array,
                                                                              gsize len,
                                                                              const char *name);
+
+/*****************************************************************************/
+
+guint       _nm_setting_connection_get_num_permissions_users(NMSettingConnection *setting);
+const char *_nm_setting_connection_get_first_permissions_user(NMSettingConnection *setting);
+
+void _nm_setting_get_private_files(NMSetting *setting, GPtrArray *files);
+
 #endif
diff --git a/libnm-core/nm-setting-8021x.c b/libnm-core/nm-setting-8021x.c
index 8464100d2f..3ec8b62e58 100644
--- a/libnm-core/nm-setting-8021x.c
+++ b/libnm-core/nm-setting-8021x.c
@@ -3033,6 +3033,95 @@ need_secrets (NMSetting *setting)
 
 /*****************************************************************************/
 
+#define NM_MORE_ASSERT_ONCE(more_assert_level)                                                    \
+    ((NM_MORE_ASSERTS >= (more_assert_level)) && ({                                               \
+         static volatile int _assert_once = 0;                                                    \
+                                                                                                  \
+         G_STATIC_ASSERT_EXPR((more_assert_level) > 0);                                           \
+                                                                                                  \
+         G_UNLIKELY(_assert_once == 0 && g_atomic_int_compare_and_exchange(&_assert_once, 0, 1)); \
+     }))
+
+static void
+get_private_files(NMSetting *setting, GPtrArray *files)
+{
+    const struct {
+        const char *property;
+        NMSetting8021xCKScheme (*get_scheme_func)(NMSetting8021x *);
+        const char *(*get_path_func)(NMSetting8021x *);
+    } cert_props[] = {
+        {NM_SETTING_802_1X_CA_CERT,
+         nm_setting_802_1x_get_ca_cert_scheme,
+         nm_setting_802_1x_get_ca_cert_path},
+        {NM_SETTING_802_1X_CLIENT_CERT,
+         nm_setting_802_1x_get_client_cert_scheme,
+         nm_setting_802_1x_get_client_cert_path},
+        {NM_SETTING_802_1X_PRIVATE_KEY,
+         nm_setting_802_1x_get_private_key_scheme,
+         nm_setting_802_1x_get_private_key_path},
+        {NM_SETTING_802_1X_PHASE2_CA_CERT,
+         nm_setting_802_1x_get_phase2_ca_cert_scheme,
+         nm_setting_802_1x_get_phase2_ca_cert_path},
+        {NM_SETTING_802_1X_PHASE2_CLIENT_CERT,
+         nm_setting_802_1x_get_phase2_client_cert_scheme,
+         nm_setting_802_1x_get_phase2_client_cert_path},
+        {NM_SETTING_802_1X_PHASE2_PRIVATE_KEY,
+         nm_setting_802_1x_get_phase2_private_key_scheme,
+         nm_setting_802_1x_get_phase2_private_key_path},
+    };
+    NMSetting8021x *s_8021x = NM_SETTING_802_1X(setting);
+    const char     *path;
+    guint           i;
+
+    if (NM_MORE_ASSERT_ONCE(5)) {
+        GObjectClass        *klass;
+        gs_free GParamSpec **properties = NULL;
+        guint                n_properties;
+        gboolean             found;
+        guint                j;
+
+        /* Check that all the properties in the setting with flag CERT_KEY_FILE
+         * are listed in the table, and vice versa. */
+
+        klass = G_OBJECT_GET_CLASS(setting);
+
+        properties = g_object_class_list_properties(klass, &n_properties);
+        for (i = 0; i < n_properties; i++) {
+            if (!(properties[i]->flags & NM_SETTING_PARAM_CERT_KEY_FILE))
+                continue;
+
+            found = FALSE;
+            for (j = 0; j < G_N_ELEMENTS(cert_props); j++) {
+                if (nm_streq0(properties[i]->name, cert_props[j].property)) {
+                    found = TRUE;
+                    break;
+                }
+            }
+
+            nm_assert(found);
+        }
+
+        for (i = 0; i < G_N_ELEMENTS(cert_props); i++) {
+            GParamSpec *prop;
+
+            prop = g_object_class_find_property(klass, cert_props[i].property);
+            nm_assert(prop);
+            nm_assert(prop->flags & NM_SETTING_PARAM_CERT_KEY_FILE);
+        }
+    }
+
+    for (i = 0; i < G_N_ELEMENTS(cert_props); i++) {
+        if (cert_props[i].get_scheme_func(s_8021x) == NM_SETTING_802_1X_CK_SCHEME_PATH) {
+            path = cert_props[i].get_path_func(s_8021x);
+            if (path) {
+                g_ptr_array_add(files, (gpointer) path);
+            }
+        }
+    }
+}
+
+/*****************************************************************************/
+
 static void
 get_property (GObject *object, guint prop_id,
               GValue *value, GParamSpec *pspec)
@@ -3438,8 +3527,9 @@ nm_setting_802_1x_class_init (NMSetting8021xClass *klass)
 	object_class->set_property = set_property;
 	object_class->finalize     = finalize;
 
-	setting_class->verify       = verify;
-	setting_class->need_secrets = need_secrets;
+	setting_class->verify            = verify;
+	setting_class->need_secrets      = need_secrets;
+	setting_class->get_private_files = get_private_files;
 
 	/**
 	 * NMSetting8021x:eap:
@@ -3549,7 +3639,8 @@ nm_setting_802_1x_class_init (NMSetting8021xClass *klass)
 	    g_param_spec_boxed (NM_SETTING_802_1X_CA_CERT, "", "",
 	                        G_TYPE_BYTES,
 	                        G_PARAM_READWRITE |
-	                        G_PARAM_STATIC_STRINGS);
+	                        G_PARAM_STATIC_STRINGS |
+				NM_SETTING_PARAM_CERT_KEY_FILE);
 
 	/**
 	 * NMSetting8021x:ca-cert-password:
@@ -3698,7 +3789,8 @@ nm_setting_802_1x_class_init (NMSetting8021xClass *klass)
 	    g_param_spec_boxed (NM_SETTING_802_1X_CLIENT_CERT, "", "",
 	                        G_TYPE_BYTES,
 	                        G_PARAM_READWRITE |
-	                        G_PARAM_STATIC_STRINGS);
+	                        G_PARAM_STATIC_STRINGS |
+				NM_SETTING_PARAM_CERT_KEY_FILE);
 
 	/**
 	 * NMSetting8021x:client-cert-password:
@@ -3905,7 +3997,8 @@ nm_setting_802_1x_class_init (NMSetting8021xClass *klass)
 	    g_param_spec_boxed (NM_SETTING_802_1X_PHASE2_CA_CERT, "", "",
 	                        G_TYPE_BYTES,
 	                        G_PARAM_READWRITE |
-	                        G_PARAM_STATIC_STRINGS);
+	                        G_PARAM_STATIC_STRINGS |
+				NM_SETTING_PARAM_CERT_KEY_FILE);
 
 	/**
 	 * NMSetting8021x:phase2-ca-cert-password:
@@ -4052,7 +4145,8 @@ nm_setting_802_1x_class_init (NMSetting8021xClass *klass)
 	    g_param_spec_boxed (NM_SETTING_802_1X_PHASE2_CLIENT_CERT, "", "",
 	                        G_TYPE_BYTES,
 	                        G_PARAM_READWRITE |
-	                        G_PARAM_STATIC_STRINGS);
+	                        G_PARAM_STATIC_STRINGS |
+				NM_SETTING_PARAM_CERT_KEY_FILE);
 
 	/**
 	 * NMSetting8021x:phase2-client-cert-password:
@@ -4213,7 +4307,8 @@ nm_setting_802_1x_class_init (NMSetting8021xClass *klass)
 	    g_param_spec_boxed (NM_SETTING_802_1X_PRIVATE_KEY, "", "",
 	                        G_TYPE_BYTES,
 	                        G_PARAM_READWRITE |
-	                        G_PARAM_STATIC_STRINGS);
+	                        G_PARAM_STATIC_STRINGS |
+				NM_SETTING_PARAM_CERT_KEY_FILE);
 
 	/**
 	 * NMSetting8021x:private-key-password:
@@ -4295,7 +4390,8 @@ nm_setting_802_1x_class_init (NMSetting8021xClass *klass)
 	    g_param_spec_boxed (NM_SETTING_802_1X_PHASE2_PRIVATE_KEY, "", "",
 	                        G_TYPE_BYTES,
 	                        G_PARAM_READWRITE |
-	                        G_PARAM_STATIC_STRINGS);
+	                        G_PARAM_STATIC_STRINGS |
+				NM_SETTING_PARAM_CERT_KEY_FILE);
 
 	/**
 	 * NMSetting8021x:phase2-private-key-password:
diff --git a/libnm-core/nm-setting-connection.c b/libnm-core/nm-setting-connection.c
index 17058ea7a5..35caedf259 100644
--- a/libnm-core/nm-setting-connection.c
+++ b/libnm-core/nm-setting-connection.c
@@ -357,6 +357,47 @@ nm_setting_connection_permissions_user_allowed (NMSettingConnection *setting,
 	return FALSE;
 }
 
+guint
+_nm_setting_connection_get_num_permissions_users(NMSettingConnection *setting)
+{
+    NMSettingConnectionPrivate *priv;
+    guint                       i;
+    guint                       count = 0;
+
+    nm_assert(NM_IS_SETTING_CONNECTION(setting));
+    priv = NM_SETTING_CONNECTION_GET_PRIVATE(setting);
+
+    for (i = 0; priv->permissions && i < g_slist_length (priv->permissions); i++) {
+        const Permission *permission = &g_array_index(priv->permissions, Permission, i);
+
+        if (permission->ptype == PERM_TYPE_USER) {
+            count++;
+        }
+    }
+
+    return count;
+}
+
+const char *
+_nm_setting_connection_get_first_permissions_user(NMSettingConnection *setting)
+{
+    NMSettingConnectionPrivate *priv;
+    guint                       i;
+
+    nm_assert(NM_IS_SETTING_CONNECTION(setting));
+    priv = NM_SETTING_CONNECTION_GET_PRIVATE(setting);
+
+    for (i = 0; priv->permissions && i < g_slist_length (priv->permissions); i++) {
+        const Permission *permission = &g_array_index(priv->permissions, Permission, i);
+
+        if (permission->ptype == PERM_TYPE_USER) {
+            return permission->item;
+        }
+    }
+
+    return NULL;
+}
+
 /**
  * nm_setting_connection_add_permission:
  * @setting: the #NMSettingConnection
diff --git a/libnm-core/nm-setting-private.h b/libnm-core/nm-setting-private.h
index 326b05da8b..5e51f4e5d3 100644
--- a/libnm-core/nm-setting-private.h
+++ b/libnm-core/nm-setting-private.h
@@ -71,6 +71,11 @@ gboolean _nm_setting_clear_secrets (NMSetting *setting,
  */
 #define NM_SETTING_PARAM_TO_DBUS_IGNORE_FLAGS (1 << (7 + G_PARAM_USER_SHIFT))
 
+/* The property can refer to a certificate or key stored on disk. As such,
+ * special care is needed when accessing the file for private connections.
+ */
+#define NM_SETTING_PARAM_CERT_KEY_FILE (1 << (8 + G_PARAM_USER_SHIFT))
+
 extern const NMSettInfoPropertType nm_sett_info_propert_type_deprecated_interface_name;
 extern const NMSettInfoPropertType nm_sett_info_propert_type_deprecated_ignore_i;
 extern const NMSettInfoPropertType nm_sett_info_propert_type_deprecated_ignore_u;
diff --git a/libnm-core/nm-setting.c b/libnm-core/nm-setting.c
index b7be9c657f..33bf0117b5 100644
--- a/libnm-core/nm-setting.c
+++ b/libnm-core/nm-setting.c
@@ -972,6 +972,34 @@ init_from_dbus (NMSetting *setting,
 	return TRUE;
 }
 
+static void
+get_private_files(NMSetting *setting, GPtrArray *files)
+{
+    if (NM_MORE_ASSERTS) {
+        GParamSpec **properties;
+        guint        n_properties;
+        int          i;
+
+        properties = g_object_class_list_properties(G_OBJECT_GET_CLASS(setting), &n_properties);
+        for (i = 0; i < n_properties; i++) {
+            if (properties[i]->flags & NM_SETTING_PARAM_CERT_KEY_FILE) {
+                /* Certificates and keys needs special handling, see setting 802.1X */
+                nm_assert_not_reached();
+            }
+        }
+        g_free(properties);
+    }
+}
+
+void
+_nm_setting_get_private_files(NMSetting *setting, GPtrArray *files)
+{
+    g_return_if_fail(NM_IS_SETTING(setting));
+    g_return_if_fail(files);
+
+    NM_SETTING_GET_CLASS(setting)->get_private_files(setting, files);
+}
+
 /**
  * nm_setting_get_dbus_property_type:
  * @setting: an #NMSetting
@@ -2662,6 +2690,7 @@ nm_setting_class_init (NMSettingClass *setting_class)
 	setting_class->enumerate_values          = enumerate_values;
 	setting_class->aggregate                 = aggregate;
 	setting_class->init_from_dbus            = init_from_dbus;
+	setting_class->get_private_files         = get_private_files;
 
 	/**
 	 * NMSetting:name:
diff --git a/libnm-core/nm-setting.h b/libnm-core/nm-setting.h
index 26c1b1ee8d..090547a115 100644
--- a/libnm-core/nm-setting.h
+++ b/libnm-core/nm-setting.h
@@ -204,6 +204,11 @@ typedef struct {
 	                                  NMSettingSecretFlags flags,
 	                                  GError **error);
 
+	/* returns a list of certificate/key files referenced in the connection.
+	 * When the connection is private, we need to verify that the owner of
+	 * the connection has access to them. */
+    	void (*get_private_files)(NMSetting *setting, GPtrArray *files);
+
 	/*< private >*/
 	gboolean    (*clear_secrets) (const struct _NMSettInfoSetting *sett_info,
 	                              guint property_idx,
diff --git a/src/devices/nm-device-ethernet.c b/src/devices/nm-device-ethernet.c
index 86e50c5274..ffb6a28257 100644
--- a/src/devices/nm-device-ethernet.c
+++ b/src/devices/nm-device-ethernet.c
@@ -605,10 +605,10 @@ build_supplicant_config (NMDeviceEthernet *self,
 	mtu = nm_platform_link_get_mtu (nm_device_get_platform (NM_DEVICE (self)),
 	                                nm_device_get_ifindex (NM_DEVICE (self)));
 
-	config = nm_supplicant_config_new (FALSE, FALSE, FALSE, FALSE);
+	config = nm_supplicant_config_new (FALSE, FALSE, FALSE, FALSE, nm_utils_get_connection_first_permissions_user(connection));
 
 	security = nm_connection_get_setting_802_1x (connection);
-	if (!nm_supplicant_config_add_setting_8021x (config, security, con_uuid, mtu, TRUE, error)) {
+	if (!nm_supplicant_config_add_setting_8021x (config, security, con_uuid, mtu, TRUE, nm_device_get_private_files(NM_DEVICE(self)), error)) {
 		g_prefix_error (error, "802-1x-setting: ");
 		g_clear_object (&config);
 	}
diff --git a/src/devices/nm-device-macsec.c b/src/devices/nm-device-macsec.c
index c9592a4971..13e233a57c 100644
--- a/src/devices/nm-device-macsec.c
+++ b/src/devices/nm-device-macsec.c
@@ -226,7 +226,7 @@ build_supplicant_config (NMDeviceMacsec *self, GError **error)
 	mtu = nm_platform_link_get_mtu (nm_device_get_platform (NM_DEVICE (self)),
 	                                nm_device_get_ifindex (NM_DEVICE (self)));
 
-	config = nm_supplicant_config_new (FALSE, FALSE, FALSE, FALSE);
+	config = nm_supplicant_config_new (FALSE, FALSE, FALSE, FALSE, nm_utils_get_connection_first_permissions_user(connection));
 
 	s_macsec = nm_device_get_applied_setting (NM_DEVICE (self), NM_TYPE_SETTING_MACSEC);
 
@@ -239,7 +239,7 @@ build_supplicant_config (NMDeviceMacsec *self, GError **error)
 
 	if (nm_setting_macsec_get_mode (s_macsec) == NM_SETTING_MACSEC_MODE_EAP) {
 		s_8021x = nm_connection_get_setting_802_1x (connection);
-		if (!nm_supplicant_config_add_setting_8021x (config, s_8021x, con_uuid, mtu, TRUE, error)) {
+		if (!nm_supplicant_config_add_setting_8021x (config, s_8021x, con_uuid, mtu, TRUE, nm_device_get_private_files(NM_DEVICE(self)), error)) {
 			g_prefix_error (error, "802-1x-setting: ");
 			return NULL;
 		}
diff --git a/src/devices/nm-device-private.h b/src/devices/nm-device-private.h
index e87733ef3f..af3eeb23e2 100644
--- a/src/devices/nm-device-private.h
+++ b/src/devices/nm-device-private.h
@@ -202,4 +202,6 @@ gboolean nm_device_match_parent_hwaddr (NMDevice *device,
                                         NMConnection *connection,
                                         gboolean fail_if_no_hwaddr);
 
+GHashTable *nm_device_get_private_files(NMDevice *self);
+
 #endif /* NM_DEVICE_PRIVATE_H */
diff --git a/src/devices/nm-device.c b/src/devices/nm-device.c
index 72ec29a121..7e98df71f9 100644
--- a/src/devices/nm-device.c
+++ b/src/devices/nm-device.c
@@ -108,6 +108,12 @@ typedef enum {
 	CLEANUP_TYPE_DECONFIGURE,
 } CleanupType;
 
+typedef enum {
+    PRIVATE_FILES_STATE_UNKNOWN = 0,
+    PRIVATE_FILES_STATE_READING,
+    PRIVATE_FILES_STATE_DONE,
+} PrivateFilesState;
+
 typedef struct {
 	CList lst_slave;
 	NMDevice *slave;
@@ -612,6 +618,13 @@ typedef struct _NMDevicePrivate {
 		guint64 tx_bytes;
 		guint64 rx_bytes;
 	} stats;
+
+	struct {
+        	GHashTable       *table;
+        	GCancellable     *cancellable;
+        	char             *user;
+        	PrivateFilesState state;
+	} private_files;
 } NMDevicePrivate;
 
 G_DEFINE_ABSTRACT_TYPE (NMDevice, nm_device, NM_TYPE_DBUS_OBJECT)
@@ -6976,6 +6989,49 @@ tc_commit (NMDevice *self)
 	return TRUE;
 }
 
+static void
+read_private_files_cb(GObject *source_object, GAsyncResult *result, gpointer data)
+{
+    gs_unref_hashtable GHashTable *table = NULL;
+    gs_free_error GError          *error = NULL;
+    NMDevice                      *self;
+    NMDevicePrivate               *priv;
+
+    table = nm_utils_read_private_files_finish(result, &error);
+    if (nm_utils_error_is_cancelled(error, FALSE))
+        return;
+
+    self = NM_DEVICE(data);
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (error) {
+        NMConnection *connection = nm_device_get_applied_connection(self);
+
+        _LOGW(LOGD_DEVICE,
+              "could not read files for private connection %s owned by user '%s': %s",
+              connection ? nm_connection_get_uuid(connection) : NULL,
+              priv->private_files.user,
+              error->message);
+        nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+        return;
+    }
+
+    _LOGD(LOGD_DEVICE, "private files successfully read");
+
+    priv->private_files.state = PRIVATE_FILES_STATE_DONE;
+    priv->private_files.table = g_steal_pointer(&table);
+    g_clear_pointer(&priv->private_files.user, g_free);
+    g_clear_object(&priv->private_files.cancellable);
+
+    nm_device_activate_schedule_stage2_device_config(self);
+}
+
+GHashTable *
+nm_device_get_private_files(NMDevice *self)
+{
+    return NM_DEVICE_GET_PRIVATE(self)->private_files.table;
+}
+
 /*
  * activate_stage2_device_config
  *
@@ -6988,11 +7044,74 @@ activate_stage2_device_config (NMDevice *self)
 {
 	NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE (self);
 	NMDeviceClass *klass;
+	NMConnection *applied;
 	NMActStageReturn ret;
 	gboolean no_firmware = FALSE;
 	CList *iter;
 
 	nm_device_state_changed (self, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE);
+	
+	applied = nm_device_get_applied_connection(self);
+	
+	/* If the connection is private (owned by a specific user), we need to
+         * verify that the user has permission to access any files specified in
+         * the connection, such as certificates and keys. We do that by calling
+         * nm_utils_read_private_files() and saving the file contents in a hash
+         * table that can be accessed later during the activation. It is important
+         * to never access the files again to avoid TOCTOU bugs.
+         */
+        switch (priv->private_files.state) {
+        case PRIVATE_FILES_STATE_UNKNOWN:
+        {
+            gs_free const char **paths = NULL;
+            NMSettingConnection *s_con;
+            const char          *user;
+
+            s_con = nm_connection_get_setting_connection(applied);
+            nm_assert(s_con);
+            user = _nm_setting_connection_get_first_permissions_user(s_con);
+
+            priv->private_files.user = g_strdup(user);
+            if (!priv->private_files.user) {
+                priv->private_files.state = PRIVATE_FILES_STATE_DONE;
+                break;
+            }
+
+            paths = nm_utils_get_connection_private_files_paths(applied);
+            if (!paths) {
+                priv->private_files.state = PRIVATE_FILES_STATE_DONE;
+                break;
+            }
+
+            if (_nm_setting_connection_get_num_permissions_users(s_con) > 1) {
+                _LOGW(LOGD_DEVICE,
+                      "private connections with multiple users are not allowed to reference "
+                      "certificates and keys on the filesystem. Specify only one user in the "
+                      "connection.permissions property.");
+                nm_device_state_changed(self,
+                                        NM_DEVICE_STATE_FAILED,
+                                        NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+                return;
+            }
+
+            priv->private_files.state       = PRIVATE_FILES_STATE_READING;
+            priv->private_files.cancellable = g_cancellable_new();
+
+            _LOGD(LOGD_DEVICE, "reading private files");
+            nm_utils_read_private_files(paths,
+                                        priv->private_files.user,
+                                        priv->private_files.cancellable,
+                                        read_private_files_cb,
+                                        self);
+            return;
+        }
+        case PRIVATE_FILES_STATE_READING:
+            /* wait */
+            return;
+        case PRIVATE_FILES_STATE_DONE:
+            /* proceed */
+            break;
+        }
 
 	if (!nm_device_sys_iface_state_is_external_or_assume (self))
 		_ethtool_state_set (self);
@@ -15195,6 +15314,12 @@ nm_device_cleanup (NMDevice *self, NMDeviceStateReason reason, CleanupType clean
 	if (NM_DEVICE_GET_CLASS (self)->deactivate)
 		NM_DEVICE_GET_CLASS (self)->deactivate (self);
 
+        /* Clean up private files */
+        nm_clear_g_cancellable(&priv->private_files.cancellable);
+        g_clear_pointer(&priv->private_files.table, g_hash_table_unref);
+        g_clear_pointer(&priv->private_files.user, g_free);
+        priv->private_files.state = PRIVATE_FILES_STATE_UNKNOWN;
+
 	ifindex = nm_device_get_ip_ifindex (self);
 
 	if (cleanup_type == CLEANUP_TYPE_DECONFIGURE) {
diff --git a/src/devices/wifi/nm-device-wifi.c b/src/devices/wifi/nm-device-wifi.c
index 98f9df25fa..8fbaf317bd 100644
--- a/src/devices/wifi/nm-device-wifi.c
+++ b/src/devices/wifi/nm-device-wifi.c
@@ -2476,7 +2476,8 @@ build_supplicant_config (NMDeviceWifi *self,
 		nm_supplicant_interface_get_pmf_support (priv->sup_iface) == NM_SUPPLICANT_FEATURE_YES,
 		nm_supplicant_interface_get_fils_support (priv->sup_iface) == NM_SUPPLICANT_FEATURE_YES,
 		nm_supplicant_interface_get_ft_support (priv->sup_iface) == NM_SUPPLICANT_FEATURE_YES,
-		nm_supplicant_interface_get_sha384_support (priv->sup_iface) == NM_SUPPLICANT_FEATURE_YES);
+		nm_supplicant_interface_get_sha384_support (priv->sup_iface) == NM_SUPPLICANT_FEATURE_YES,
+		nm_utils_get_connection_first_permissions_user(connection));
 
 	/* Warn if AP mode may not be supported */
 	if (   g_strcmp0 (nm_setting_wireless_get_mode (s_wireless), NM_SETTING_WIRELESS_MODE_AP) == 0
@@ -2536,6 +2537,7 @@ build_supplicant_config (NMDeviceWifi *self,
 		                                                         mtu,
 		                                                         pmf,
 		                                                         fils,
+									 nm_device_get_private_files(NM_DEVICE(self)),
 		                                                         error)) {
 			g_prefix_error (error, "802-11-wireless-security: ");
 			goto error;
diff --git a/src/meson.build b/src/meson.build
index 748fa519bc..a2fa0c5b16 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -237,6 +237,7 @@ if enable_tests
 
   subdir('dnsmasq/tests')
   subdir('ndisc/tests')
+  subdir('nm-daemon-helper')
   subdir('platform/tests')
   subdir('supplicant/tests')
   subdir('tests')
diff --git a/src/nm-core-utils.c b/src/nm-core-utils.c
index e059994aeb..6e5093836f 100644
--- a/src/nm-core-utils.c
+++ b/src/nm-core-utils.c
@@ -4084,3 +4084,185 @@ NM_UTILS_LOOKUP_STR_DEFINE (nm_activation_type_to_string, NMActivationType,
 	NM_UTILS_LOOKUP_STR_ITEM (NM_ACTIVATION_TYPE_ASSUME,   "assume"),
 	NM_UTILS_LOOKUP_STR_ITEM (NM_ACTIVATION_TYPE_EXTERNAL, "external"),
 )
+
+const char *
+nm_utils_get_connection_first_permissions_user(NMConnection *connection)
+{
+    NMSettingConnection *s_con;
+
+    s_con = nm_connection_get_setting_connection(connection);
+    nm_assert(s_con);
+
+    return _nm_setting_connection_get_first_permissions_user(s_con);
+}
+
+/*****************************************************************************/
+
+const char **
+nm_utils_get_connection_private_files_paths(NMConnection *connection)
+{
+    GPtrArray *files;
+    gs_free NMSetting **settings = NULL;
+    guint num_settings;
+    guint i;
+
+    files = g_ptr_array_new();
+    settings = nm_connection_get_settings(connection, &num_settings);
+    for (i = 0; i < num_settings; i++) {
+        _nm_setting_get_private_files(settings[i], files);
+    }
+    g_ptr_array_add(files, NULL);
+
+    return (const char **) g_ptr_array_free(files, files->len == 1);
+}
+
+typedef struct _ReadInfo ReadInfo;
+
+typedef struct {
+    char     *path;
+    ReadInfo *read_info;
+} FileInfo;
+
+struct _ReadInfo {
+    GTask      *task;
+    GHashTable *table;
+    GPtrArray  *file_infos; /* of FileInfo */
+    GError     *first_error;
+    guint       num_pending;
+};
+
+static void
+read_file_helper_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    FileInfo              *file_info = user_data;
+    ReadInfo              *read_info = file_info->read_info;
+    gs_unref_bytes GBytes *output    = NULL;
+    gs_free_error GError  *error     = NULL;
+
+    output = nm_utils_spawn_helper_finish_binary(result, &error);
+
+    nm_assert(read_info->num_pending > 0);
+    read_info->num_pending--;
+
+    if (nm_utils_error_is_cancelled(error, FALSE)) {
+        /* nop */
+    } else if (error) {
+        nm_log_dbg(LOGD_CORE,
+                   "read-private-files: failed to read file '%s': %s",
+                   file_info->path,
+                   error->message);
+        if (!read_info->first_error) {
+            /* @error just says "helper process exited with status X".
+             * Return a more human-friendly one. */
+            read_info->first_error = g_error_new(NM_UTILS_ERROR,
+                                                 NM_UTILS_ERROR_UNKNOWN,
+                                                 "error reading file '%s'",
+                                                 file_info->path);
+        }
+    } else {
+        nm_log_dbg(LOGD_SUPPLICANT,
+                   "read-private-files: successfully read file '%s'",
+                   file_info->path);
+
+        /* Store the file contents in the hash table */
+        if (!read_info->table) {
+            read_info->table = g_hash_table_new_full(nm_str_hash,
+                                                     g_str_equal,
+                                                     g_free,
+                                                     (GDestroyNotify) g_bytes_unref);
+        }
+        g_hash_table_insert(read_info->table,
+                            g_steal_pointer(&file_info->path),
+                            g_steal_pointer(&output));
+    }
+
+    g_clear_pointer(&file_info->path, g_free);
+
+    /* If all operations are completed, return  */
+    if (read_info->num_pending == 0) {
+        if (read_info->first_error) {
+            g_task_return_error(read_info->task, g_steal_pointer(&read_info->first_error));
+        } else {
+            g_task_return_pointer(read_info->task,
+                                  g_steal_pointer(&read_info->table),
+                                  (GDestroyNotify) g_hash_table_unref);
+        }
+
+        if (read_info->table)
+            g_hash_table_unref(read_info->table);
+        if (read_info->file_infos)
+            g_ptr_array_unref(read_info->file_infos);
+
+        g_object_unref(read_info->task);
+        g_free(read_info);
+    }
+}
+
+/**
+ * nm_utils_read_private_files:
+ * @paths: array of file paths to be read
+ * @user: name of the user to impersonate when reading the files
+ * @cancellable: cancellable to cancel the operation
+ * @callback: callback to invoke on completion
+ * @cb_data: data for @callback
+ *
+ * Reads the given list of files @paths on behalf of user @user. Invokes
+ * @callback asynchronously on completion. The callback must use
+ * nm_utils_read_private_files_finish() to obtain the result.
+ */
+void
+nm_utils_read_private_files(const char *const  *paths,
+                            const char         *user,
+                            GCancellable       *cancellable,
+                            GAsyncReadyCallback callback,
+                            gpointer            cb_data)
+{
+    ReadInfo *read_info;
+    FileInfo *file_info;
+    guint     i;
+
+    g_return_if_fail(paths && paths[0]);
+    g_return_if_fail(cancellable);
+    g_return_if_fail(callback);
+    g_return_if_fail(cb_data);
+
+    read_info  = g_new(ReadInfo, 1);
+    *read_info = (ReadInfo) {
+        .task = nm_g_task_new(NULL, cancellable, nm_utils_read_private_files, callback, cb_data),
+        .file_infos = g_ptr_array_new_with_free_func(g_free),
+    };
+
+    for (i = 0; paths[i]; i++) {
+        file_info  = g_new(FileInfo, 1);
+        *file_info = (FileInfo) {
+            .path      = g_strdup(paths[i]),
+            .read_info = read_info,
+        };
+        g_ptr_array_add(read_info->file_infos, file_info);
+        read_info->num_pending++;
+
+        nm_utils_spawn_helper(NM_MAKE_STRV("read-file-as-user", user, paths[i]),
+                              TRUE,
+                              cancellable,
+                              read_file_helper_cb,
+                              file_info);
+    }
+}
+
+/**
+ * nm_utils_read_private_files_finish:
+ * @result: the GAsyncResult
+ * @error: on return, the error
+ *
+ * Returns the files read by nm_utils_read_private_files(). The return value
+ * is a hash table {char * -> GBytes *}. Free it with g_hash_table_unref().
+ */
+GHashTable *
+nm_utils_read_private_files_finish(GAsyncResult *result, GError **error)
+{
+    GTask *task = G_TASK(result);
+
+    nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_read_private_files));
+
+    return g_task_propagate_pointer(task, error);
+}
diff --git a/src/nm-core-utils.h b/src/nm-core-utils.h
index 7d63d06afc..0e7a09e908 100644
--- a/src/nm-core-utils.h
+++ b/src/nm-core-utils.h
@@ -479,4 +479,19 @@ const char *nm_utils_parse_dns_domain (const char *domain, gboolean *is_routing)
 #define NM_UTILS_ERROR_MSG_REQ_UID_UKNOWN    "Unable to determine UID of the request"
 #define NM_UTILS_ERROR_MSG_INSUFF_PRIV       "Insufficient privileges"
 
+/*****************************************************************************/
+
+const char *nm_utils_get_connection_first_permissions_user(NMConnection *connection);
+
+/*****************************************************************************/
+
+const char **nm_utils_get_connection_private_files_paths(NMConnection *connection);
+
+void        nm_utils_read_private_files(const char *const  *paths,
+                                        const char         *user,
+                                        GCancellable       *cancellable,
+                                        GAsyncReadyCallback callback,
+                                        gpointer            cb_data);
+GHashTable *nm_utils_read_private_files_finish(GAsyncResult *result, GError **error);
+
 #endif /* __NM_CORE_UTILS_H__ */
diff --git a/src/nm-daemon-helper/README.md b/src/nm-daemon-helper/README.md
new file mode 100644
index 0000000000..e9b458688e
--- /dev/null
+++ b/src/nm-daemon-helper/README.md
@@ -0,0 +1,10 @@
+nm-daemon-helper
+----------------
+
+A internal helper application that is spawned by NetworkManager to
+perform certain actions which can't be done in the daemon.
+
+Currently it's used to read files on behalf of unprivileged users
+(which requires a seteuid that affects all the threads of the process).
+
+This is not directly useful to the user.
diff --git a/src/nm-daemon-helper/meson.build b/src/nm-daemon-helper/meson.build
new file mode 100644
index 0000000000..da0d6571e1
--- /dev/null
+++ b/src/nm-daemon-helper/meson.build
@@ -0,0 +1,15 @@
+executable(
+  'nm-daemon-helper',
+  'nm-daemon-helper.c',
+  include_directories : [
+    src_inc,
+    top_inc,
+  ],
+  link_with: [
+    libnm_std_aux,
+  ],
+  link_args: ldflags_linker_script_binary,
+  link_depends: linker_script_binary,
+  install: true,
+  install_dir: nm_libexecdir,
+)
diff --git a/src/nm-daemon-helper/nm-daemon-helper.c b/src/nm-daemon-helper/nm-daemon-helper.c
new file mode 100644
index 0000000000..51c7a010dc
--- /dev/null
+++ b/src/nm-daemon-helper/nm-daemon-helper.c
@@ -0,0 +1,193 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+/* Copyright (C) 2021 Red Hat, Inc. */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#if defined(__GLIBC__)
+#include <nss.h>
+#endif
+#include <stdarg.h>
+
+enum {
+    RETURN_SUCCESS      = 0,
+    RETURN_INVALID_CMD  = 1,
+    RETURN_INVALID_ARGS = 2,
+    RETURN_ERROR        = 3,
+};
+
+static char *
+read_arg(void)
+{
+    nm_auto_free char *arg = NULL;
+    size_t             len = 0;
+
+    if (getdelim(&arg, &len, '\0', stdin) < 0)
+        return NULL;
+
+    return nm_steal_pointer(&arg);
+}
+
+static int
+more_args(void)
+{
+    nm_auto_free char *arg = NULL;
+
+    arg = read_arg();
+
+    return !!arg;
+}
+
+static bool
+set_effective_user(const char *user, char *errbuf, size_t errbuf_len)
+{
+    struct passwd *pwentry;
+    int            errsv;
+    char           error[1024];
+
+    errno   = 0;
+    pwentry = getpwnam(user);
+    if (!pwentry) {
+        errsv = errno;
+        if (errsv == 0) {
+            snprintf(errbuf, errbuf_len, "user not found");
+        } else {
+            snprintf(errbuf,
+                     errbuf_len,
+                     "error getting user entry: %d (%s)\n",
+                     errsv,
+                     strerror_r(errsv, error, sizeof(error)));
+        }
+        return false;
+    }
+
+    if (setgid(pwentry->pw_gid) != 0) {
+        errsv = errno;
+        snprintf(errbuf,
+                 errbuf_len,
+                 "failed to change group to %u: %d (%s)\n",
+                 pwentry->pw_gid,
+                 errsv,
+                 strerror_r(errsv, error, sizeof(error)));
+        return false;
+    }
+
+    if (initgroups(user, pwentry->pw_gid) != 0) {
+        errsv = errno;
+        snprintf(errbuf,
+                 errbuf_len,
+                 "failed to reset supplementary group list to %u: %d (%s)\n",
+                 pwentry->pw_gid,
+                 errsv,
+                 strerror_r(errsv, error, sizeof(error)));
+        return false;
+    }
+
+    if (setuid(pwentry->pw_uid) != 0) {
+        errsv = errno;
+        snprintf(errbuf,
+                 errbuf_len,
+                 "failed to change user to %u: %d (%s)\n",
+                 pwentry->pw_uid,
+                 errsv,
+                 strerror_r(errsv, error, sizeof(error)));
+        return false;
+    }
+
+    return true;
+}
+
+/*****************************************************************************/
+
+static bool
+read_file_to_stdout(const char *filename, char *errbuf, size_t errbuf_len)
+{
+    nm_auto_close int fd = -1;
+    char              buffer[4096];
+    char              error[1024];
+    ssize_t           bytes_read;
+    int               errsv;
+
+    fd = open(filename, O_RDONLY);
+    if (fd == -1) {
+        errsv = errno;
+        snprintf(errbuf,
+                 errbuf_len,
+                 "error opening the file: %d (%s)",
+                 errsv,
+                 strerror_r(errsv, error, sizeof(error)));
+        return false;
+    }
+
+    while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0) {
+        if (fwrite(buffer, 1, bytes_read, stdout) != (size_t) bytes_read) {
+            errsv = errno;
+            snprintf(errbuf,
+                     errbuf_len,
+                     "error writing to stdout: %d (%s)",
+                     errsv,
+                     strerror_r(errsv, error, sizeof(error)));
+            return false;
+        }
+    }
+
+    if (bytes_read < 0) {
+        errsv = errno;
+        snprintf(errbuf,
+                 errbuf_len,
+                 "error reading the file: %d (%s)",
+                 errsv,
+                 strerror_r(errsv, error, sizeof(error)));
+        return false;
+    }
+
+    return true;
+}
+
+static int
+cmd_read_file_as_user(void)
+{
+    nm_auto_free char *user     = NULL;
+    nm_auto_free char *filename = NULL;
+    char               error[1024];
+
+    user = read_arg();
+    if (!user)
+        return RETURN_INVALID_ARGS;
+
+    filename = read_arg();
+    if (!filename)
+        return RETURN_INVALID_ARGS;
+
+    if (more_args())
+        return RETURN_INVALID_ARGS;
+
+    if (!set_effective_user(user, error, sizeof(error))) {
+        fprintf(stderr, "Failed to set effective user '%s': %s", user, error);
+        return RETURN_ERROR;
+    }
+
+    if (!read_file_to_stdout(filename, error, sizeof(error))) {
+        fprintf(stderr, "Failed to read file '%s' as user '%s': %s", filename, user, error);
+        return RETURN_ERROR;
+    }
+
+    return RETURN_SUCCESS;
+}
+
+int
+main(int argc, char **argv)
+{
+    nm_auto_free char *cmd = NULL;
+
+    cmd = read_arg();
+    if (!cmd)
+        return RETURN_INVALID_CMD;
+
+    if (nm_streq(cmd, "read-file-as-user"))
+        return cmd_read_file_as_user();
+
+    return RETURN_INVALID_CMD;
+}
diff --git a/src/supplicant/nm-supplicant-config.c b/src/supplicant/nm-supplicant-config.c
index dec4556d1c..9be9fd516b 100644
--- a/src/supplicant/nm-supplicant-config.c
+++ b/src/supplicant/nm-supplicant-config.c
@@ -30,6 +30,7 @@ typedef struct {
 typedef struct {
 	GHashTable *config;
 	GHashTable *blobs;
+	char       *private_user;
 	guint32    ap_scan;
 	gboolean   fast_required;
 	gboolean   dispose_has_run;
@@ -56,7 +57,8 @@ G_DEFINE_TYPE (NMSupplicantConfig, nm_supplicant_config, G_TYPE_OBJECT)
 
 NMSupplicantConfig *
 nm_supplicant_config_new (gboolean support_pmf, gboolean support_fils,
-                          gboolean support_ft, gboolean support_sha384)
+                          gboolean support_ft, gboolean support_sha384,
+			  const char *private_user)
 {
 	NMSupplicantConfigPrivate *priv;
 	NMSupplicantConfig *self;
@@ -68,6 +70,7 @@ nm_supplicant_config_new (gboolean support_pmf, gboolean support_fils,
 	priv->support_fils = support_fils;
 	priv->support_ft = support_ft;
 	priv->support_sha384 = support_sha384;
+	priv->private_user = g_strdup(private_user);
 
 	return self;
 }
@@ -235,19 +238,19 @@ static gboolean
 nm_supplicant_config_add_blob_for_connection (NMSupplicantConfig *self,
                                               GBytes *field,
                                               const char *name,
-                                              const char *con_uid,
+                                              const char *con_uuid,
                                               GError **error)
 {
 	if (field && g_bytes_get_size (field)) {
-		gs_free char *uid = NULL;
+		gs_free char *blob_id = NULL;
 		char *p;
 
-		uid = g_strdup_printf ("%s-%s", con_uid, name);
-		for (p = uid; *p; p++) {
+		blob_id = g_strdup_printf("%s-%s", con_uuid, name);
+        	for (p = blob_id; *p; p++) {
 			if (*p == '/')
 				*p = '-';
 		}
-		if (!nm_supplicant_config_add_blob (self, name, field, uid, error))
+		if (!nm_supplicant_config_add_blob (self, name, field, blob_id, error))
 			return FALSE;
 	}
 	return TRUE;
@@ -260,6 +263,7 @@ nm_supplicant_config_finalize (GObject *object)
 
 	g_hash_table_destroy (priv->config);
 	g_hash_table_destroy (priv->blobs);
+	nm_clear_pointer(&priv->private_user, g_free);
 
 	G_OBJECT_CLASS (nm_supplicant_config_parent_class)->finalize (object);
 }
@@ -754,6 +758,7 @@ nm_supplicant_config_add_setting_wireless_security (NMSupplicantConfig *self,
                                                     guint32 mtu,
                                                     NMSettingWirelessSecurityPmf pmf,
                                                     NMSettingWirelessSecurityFils fils,
+						    GHashTable *files,
                                                     GError **error)
 {
 	NMSupplicantConfigPrivate *priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE (self);
@@ -961,7 +966,7 @@ nm_supplicant_config_add_setting_wireless_security (NMSupplicantConfig *self,
 				             "Cannot set key-mgmt %s with missing 8021x setting", key_mgmt);
 				return FALSE;
 			}
-			if (!nm_supplicant_config_add_setting_8021x (self, setting_8021x, con_uuid, mtu, FALSE, error))
+			if (!nm_supplicant_config_add_setting_8021x (self, setting_8021x, con_uuid, mtu, FALSE, NULL, error))
 				return FALSE;
 		}
 
@@ -1031,6 +1036,7 @@ nm_supplicant_config_add_setting_8021x (NMSupplicantConfig *self,
                                         const char *con_uuid,
                                         guint32 mtu,
                                         gboolean wired,
+					GHashTable *files,
                                         GError **error)
 {
 	NMSupplicantConfigPrivate *priv;
@@ -1242,20 +1248,21 @@ nm_supplicant_config_add_setting_8021x (NMSupplicantConfig *self,
 	}
 
 	/* CA certificate */
+	path  = NULL;
+    	bytes = NULL;
 	if (ca_cert_override) {
-		if (!add_string_val (self, ca_cert_override, "ca_cert", FALSE, NULL, error))
-			return FALSE;
+		/* This is a build-time-configured system-wide file path, no need to pass
+		 * it as a blob */
+        	path = ca_cert_override;
 	} else {
 		switch (nm_setting_802_1x_get_ca_cert_scheme (setting)) {
 		case NM_SETTING_802_1X_CK_SCHEME_BLOB:
 			bytes = nm_setting_802_1x_get_ca_cert_blob (setting);
-			if (!nm_supplicant_config_add_blob_for_connection (self, bytes, "ca_cert", con_uuid, error))
-				return FALSE;
 			break;
 		case NM_SETTING_802_1X_CK_SCHEME_PATH:
 			path = nm_setting_802_1x_get_ca_cert_path (setting);
-			if (!add_string_val (self, path, "ca_cert", FALSE, NULL, error))
-				return FALSE;
+			if (priv->private_user)
+                		bytes = nm_g_hash_table_lookup(files, path);
 			break;
 		case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
 			if (!add_pkcs11_uri_with_pin (self, "ca_cert",
@@ -1270,22 +1277,32 @@ nm_supplicant_config_add_setting_8021x (NMSupplicantConfig *self,
 			break;
 		}
 	}
+	if (bytes) {
+        	if (!nm_supplicant_config_add_blob_for_connection(self, bytes, "ca_cert", con_uuid, error))
+            		return FALSE;
+    	} else if (path) {
+        	/* Private connections cannot use paths other than the system CA store */
+        	g_return_val_if_fail(ca_cert_override || !priv->private_user, FALSE);
+        	if (!add_string_val(self, path, "ca_cert", FALSE, NULL, error))
+            		return FALSE;
+    	}
 
 	/* Phase 2 CA certificate */
+	path  = NULL;
+    	bytes = NULL;
 	if (ca_cert_override) {
-		if (!add_string_val (self, ca_cert_override, "ca_cert2", FALSE, NULL, error))
-			return FALSE;
+		/* This is a build-time-configured system-wide file path, no need to pass
+		 * it as a blob */
+        	path = ca_cert_override;
 	} else {
 		switch (nm_setting_802_1x_get_phase2_ca_cert_scheme (setting)) {
 		case NM_SETTING_802_1X_CK_SCHEME_BLOB:
 			bytes = nm_setting_802_1x_get_phase2_ca_cert_blob (setting);
-			if (!nm_supplicant_config_add_blob_for_connection (self, bytes, "ca_cert2", con_uuid, error))
-				return FALSE;
 			break;
 		case NM_SETTING_802_1X_CK_SCHEME_PATH:
 			path = nm_setting_802_1x_get_phase2_ca_cert_path (setting);
-			if (!add_string_val (self, path, "ca_cert2", FALSE, NULL, error))
-				return FALSE;
+			if (priv->private_user)
+                		bytes = nm_g_hash_table_lookup(files, path);
 			break;
 		case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
 			if (!add_pkcs11_uri_with_pin (self, "ca_cert2",
@@ -1300,6 +1317,15 @@ nm_supplicant_config_add_setting_8021x (NMSupplicantConfig *self,
 			break;
 		}
 	}
+	if (bytes) {
+        	if (!nm_supplicant_config_add_blob_for_connection(self, bytes, "ca_cert2", con_uuid, error))
+            		return FALSE;
+    	} else if (path) {
+        	/* Private connections cannot use paths other than the system CA store */
+        	g_return_val_if_fail(ca_cert_override || !priv->private_user, FALSE);
+        	if (!add_string_val(self, path, "ca_cert2", FALSE, NULL, error))
+            		return FALSE;
+    	}
 
 	/* Subject match */
 	value = nm_setting_802_1x_get_subject_match (setting);
@@ -1325,17 +1351,17 @@ nm_supplicant_config_add_setting_8021x (NMSupplicantConfig *self,
 
 	/* Private key */
 	added = FALSE;
+	path  = NULL;
+	bytes = NULL;
 	switch (nm_setting_802_1x_get_private_key_scheme (setting)) {
 	case NM_SETTING_802_1X_CK_SCHEME_BLOB:
 		bytes = nm_setting_802_1x_get_private_key_blob (setting);
-		if (!nm_supplicant_config_add_blob_for_connection (self, bytes, "private_key", con_uuid, error))
-			return FALSE;
 		added = TRUE;
 		break;
 	case NM_SETTING_802_1X_CK_SCHEME_PATH:
 		path = nm_setting_802_1x_get_private_key_path (setting);
-		if (!add_string_val (self, path, "private_key", FALSE, NULL, error))
-			return FALSE;
+		if (priv->private_user)
+			bytes = nm_g_hash_table_lookup(files, path);
 		added = TRUE;
 		break;
 	case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
@@ -1351,6 +1377,19 @@ nm_supplicant_config_add_setting_8021x (NMSupplicantConfig *self,
 	default:
 		break;
 	}
+	if (bytes) {
+        	if (!nm_supplicant_config_add_blob_for_connection(self,
+                	                                          bytes,
+                        	                                  "private_key",
+                                	                          con_uuid,
+                                        	                  error))
+            		return FALSE;
+    	} else if (path) {
+        	/* Private connections cannot use paths */
+        	g_return_val_if_fail(!priv->private_user, FALSE);
+        	if (!add_string_val(self, path, "private_key", FALSE, NULL, error))
+            		return FALSE;
+    	}
 
 	if (added) {
 		NMSetting8021xCKFormat format;
@@ -1374,16 +1413,16 @@ nm_supplicant_config_add_setting_8021x (NMSupplicantConfig *self,
 			/* Only add the client cert if the private key is not PKCS#12, as
 			 * wpa_supplicant configuration directs us to do.
 			 */
+			path  = NULL;
+			bytes = NULL;
 			switch (nm_setting_802_1x_get_client_cert_scheme (setting)) {
 			case NM_SETTING_802_1X_CK_SCHEME_BLOB:
 				bytes = nm_setting_802_1x_get_client_cert_blob (setting);
-				if (!nm_supplicant_config_add_blob_for_connection (self, bytes, "client_cert", con_uuid, error))
-					return FALSE;
 				break;
 			case NM_SETTING_802_1X_CK_SCHEME_PATH:
 				path = nm_setting_802_1x_get_client_cert_path (setting);
-				if (!add_string_val (self, path, "client_cert", FALSE, NULL, error))
-					return FALSE;
+				if (priv->private_user)
+                    			bytes = nm_g_hash_table_lookup(files, path);
 				break;
 			case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
 				if (!add_pkcs11_uri_with_pin (self, "client_cert",
@@ -1397,22 +1436,35 @@ nm_supplicant_config_add_setting_8021x (NMSupplicantConfig *self,
 			default:
 				break;
 			}
+			if (bytes) {
+                	if (!nm_supplicant_config_add_blob_for_connection(self,
+                        	                                          bytes,
+                                	                                  "client_cert",
+                                        	                          con_uuid,
+                                                	                  error))
+                    		return FALSE;
+            		} else if (path) {
+                		/* Private connections cannot use paths */
+                		g_return_val_if_fail(!priv->private_user, FALSE);
+                		if (!add_string_val(self, path, "client_cert", FALSE, NULL, error))
+                    			return FALSE;
+            		}
 		}
 	}
 
 	/* Phase 2 private key */
 	added = FALSE;
+	path  = NULL;
+	bytes = NULL;
 	switch (nm_setting_802_1x_get_phase2_private_key_scheme (setting)) {
 	case NM_SETTING_802_1X_CK_SCHEME_BLOB:
 		bytes = nm_setting_802_1x_get_phase2_private_key_blob (setting);
-		if (!nm_supplicant_config_add_blob_for_connection (self, bytes, "private_key2", con_uuid, error))
-			return FALSE;
 		added = TRUE;
 		break;
 	case NM_SETTING_802_1X_CK_SCHEME_PATH:
 		path = nm_setting_802_1x_get_phase2_private_key_path (setting);
-		if (!add_string_val (self, path, "private_key2", FALSE, NULL, error))
-			return FALSE;
+		if (priv->private_user)
+            		bytes = nm_g_hash_table_lookup(files, path);
 		added = TRUE;
 		break;
 	case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
@@ -1428,6 +1480,19 @@ nm_supplicant_config_add_setting_8021x (NMSupplicantConfig *self,
 	default:
 		break;
 	}
+	if (bytes) {
+        	if (!nm_supplicant_config_add_blob_for_connection(self,
+                	                                          bytes,
+                        	                                  "private_key2",
+                                	                          con_uuid,
+                                        	                  error))
+            		return FALSE;
+    	} else if (path) {
+        	/* Private connections cannot use paths */
+        	g_return_val_if_fail(!priv->private_user, FALSE);
+        	if (!add_string_val(self, path, "private_key2", FALSE, NULL, error))
+            		return FALSE;
+    	}
 
 	if (added) {
 		NMSetting8021xCKFormat format;
@@ -1451,16 +1516,16 @@ nm_supplicant_config_add_setting_8021x (NMSupplicantConfig *self,
 			/* Only add the client cert if the private key is not PKCS#12, as
 			 * wpa_supplicant configuration directs us to do.
 			 */
+			path  = NULL;
+			bytes = NULL;
 			switch (nm_setting_802_1x_get_phase2_client_cert_scheme (setting)) {
 			case NM_SETTING_802_1X_CK_SCHEME_BLOB:
 				bytes = nm_setting_802_1x_get_phase2_client_cert_blob (setting);
-				if (!nm_supplicant_config_add_blob_for_connection (self, bytes, "client_cert2", con_uuid, error))
-					return FALSE;
 				break;
 			case NM_SETTING_802_1X_CK_SCHEME_PATH:
 				path = nm_setting_802_1x_get_phase2_client_cert_path (setting);
-				if (!add_string_val (self, path, "client_cert2", FALSE, NULL, error))
-					return FALSE;
+				if (priv->private_user)
+                    			bytes = nm_g_hash_table_lookup(files, path);
 				break;
 			case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
 				if (!add_pkcs11_uri_with_pin (self, "client_cert2",
@@ -1474,6 +1539,19 @@ nm_supplicant_config_add_setting_8021x (NMSupplicantConfig *self,
 			default:
 				break;
 			}
+			if (bytes) {
+                		if (!nm_supplicant_config_add_blob_for_connection(self,
+                                		                                  bytes,
+                                                		                  "client_cert2",
+                                                                		  con_uuid,
+                                                                  		  error))
+                    			return FALSE;
+            		} else if (path) {
+                		/* Private connections cannot use paths */
+                		g_return_val_if_fail(!priv->private_user, FALSE);
+                		if (!add_string_val(self, path, "client_cert2", FALSE, NULL, error))
+                    			return FALSE;
+            		}
 		}
 	}
 
diff --git a/src/supplicant/nm-supplicant-config.h b/src/supplicant/nm-supplicant-config.h
index 361f9ac62e..4372b71452 100644
--- a/src/supplicant/nm-supplicant-config.h
+++ b/src/supplicant/nm-supplicant-config.h
@@ -26,7 +26,8 @@ typedef struct _NMSupplicantConfigClass NMSupplicantConfigClass;
 GType nm_supplicant_config_get_type (void);
 
 NMSupplicantConfig *nm_supplicant_config_new (gboolean support_pmf, gboolean support_fils,
-                                              gboolean support_ft, gboolean support_sha384);
+                                              gboolean support_ft, gboolean support_sha384,
+					      const char *private_user);
 
 guint32 nm_supplicant_config_get_ap_scan (NMSupplicantConfig *self);
 
@@ -52,6 +53,7 @@ gboolean nm_supplicant_config_add_setting_wireless_security (NMSupplicantConfig
                                                              guint32 mtu,
                                                              NMSettingWirelessSecurityPmf pmf,
                                                              NMSettingWirelessSecurityFils fils,
+							     GHashTable *files,
                                                              GError **error);
 
 gboolean nm_supplicant_config_add_no_security (NMSupplicantConfig *self,
@@ -62,6 +64,7 @@ gboolean nm_supplicant_config_add_setting_8021x (NMSupplicantConfig *self,
                                                  const char *con_uuid,
                                                  guint32 mtu,
                                                  gboolean wired,
+						 GHashTable *files,
                                                  GError **error);
 
 gboolean nm_supplicant_config_add_setting_macsec (NMSupplicantConfig *self,
diff --git a/src/supplicant/nm-supplicant-interface.c b/src/supplicant/nm-supplicant-interface.c
index c19da11c8a..49da164bae 100644
--- a/src/supplicant/nm-supplicant-interface.c
+++ b/src/supplicant/nm-supplicant-interface.c
@@ -58,6 +58,7 @@ typedef struct {
 	gpointer user_data;
 	guint fail_on_idle_id;
 	guint blobs_left;
+	guint remove_blobs_left;
 	struct _AddNetworkData *add_network_data;
 } AssocData;
 
@@ -2321,12 +2322,171 @@ assoc_add_blob_cb (GDBusProxy *proxy, GAsyncResult *result, gpointer user_data)
 		return;
 	}
 
+	nm_assert(priv->assoc_data->blobs_left > 0);
 	priv->assoc_data->blobs_left--;
 	_LOGT ("assoc[%p]: blob added (%u left)", priv->assoc_data, priv->assoc_data->blobs_left);
 	if (priv->assoc_data->blobs_left == 0)
 		assoc_call_select_network (self);
 }
 
+static void
+assoc_add_blobs(NMSupplicantInterface *self)
+{
+    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
+    GHashTable                   *blobs;
+    GHashTableIter                iter;
+    const char                   *blob_name;
+    GBytes                       *blob_data;
+
+    blobs                        = nm_supplicant_config_get_blobs(priv->assoc_data->cfg);
+    priv->assoc_data->blobs_left = nm_g_hash_table_size(blobs);
+
+    _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: need to add %u blobs",
+          NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+          priv->assoc_data->blobs_left);
+
+    if (priv->assoc_data->blobs_left == 0) {
+        assoc_call_select_network(self);
+        return;
+    }
+
+    g_hash_table_iter_init(&iter, blobs);
+    while (g_hash_table_iter_next(&iter, (gpointer) &blob_name, (gpointer) &blob_data)) {
+        _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: adding blob '%s'",
+              NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+              blob_name);
+	g_dubs_proxy_call (priv->iface_proxy,
+			   "AddBlob",
+			   g_variant_new ("(s@ay)", blob_name, nm_g_bytes_to_variant_ay(blob_data)),
+			   G_DBUS_CALL_FLAGS_NONE,
+			   -1,
+			   priv->assoc_data->cancellable,
+			   (GAsyncReadyCallback) assoc_add_blob_cb,
+			   self);
+        /*_dbus_connection_call(
+            self,
+            NM_WPAS_DBUS_IFACE_INTERFACE,
+            "AddBlob",
+            g_variant_new("(s@ay)", blob_name, nm_g_bytes_to_variant_ay(blob_data)),
+            G_VARIANT_TYPE("()"),
+            G_DBUS_CALL_FLAGS_NONE,
+            DBUS_TIMEOUT_MSEC,
+            priv->assoc_data->cancellable,
+            assoc_add_blob_cb,
+            self);*/
+    }
+}
+
+static void
+assoc_remove_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMSupplicantInterface        *self;
+    NMSupplicantInterfacePrivate *priv;
+    gs_free_error GError         *error = NULL;
+    gs_unref_variant GVariant    *res   = NULL;
+
+    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
+    if (nm_utils_error_is_cancelled(error, FALSE))
+        return;
+
+    self = NM_SUPPLICANT_INTERFACE(user_data);
+    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
+
+    /* We don't consider a failure fatal. The new association might be able
+     * to proceed even with the existing blobs, if they don't conflict with new
+     * ones. */
+
+    nm_assert(priv->assoc_data->remove_blobs_left > 0);
+    priv->assoc_data->remove_blobs_left--;
+
+    if (error) {
+        g_dbus_error_strip_remote_error(error);
+        _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to delete blob: %s",
+              NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+              error->message);
+    } else {
+        _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: blob removed (%u left)",
+              NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+              priv->assoc_data->remove_blobs_left);
+    }
+
+    if (priv->assoc_data->remove_blobs_left == 0)
+        assoc_add_blobs(self);
+}
+
+static void
+assoc_get_blobs_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMSupplicantInterface        *self;
+    NMSupplicantInterfacePrivate *priv;
+    gs_free_error GError         *error = NULL;
+    gs_unref_variant GVariant    *res   = NULL;
+    gs_unref_variant GVariant    *value = NULL;
+    GVariantIter                  iter;
+    const char                   *blob_name;
+    GVariant                     *blob_data;
+
+    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
+    if (nm_utils_error_is_cancelled(error, FALSE))
+        return;
+
+    self = NM_SUPPLICANT_INTERFACE(user_data);
+    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
+
+    if (error) {
+        _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to get blob list: %s",
+              NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+              error->message);
+        assoc_add_blobs(self);
+        return;
+    }
+
+    g_variant_get(res, "(v)", &value);
+
+    /* While the "Blobs" property is documented as type "as", it is actually "a{say}" */
+    if (!value || !g_variant_is_of_type(value, G_VARIANT_TYPE("a{say}"))) {
+        _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to get blob list: wrong return type %s",
+              NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+              value ? g_variant_get_type_string(value) : "NULL");
+        assoc_add_blobs(self);
+        return;
+    }
+
+    g_variant_iter_init(&iter, value);
+    priv->assoc_data->remove_blobs_left = g_variant_iter_n_children(&iter);
+    _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: need to delete %u blobs",
+          NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+          priv->assoc_data->remove_blobs_left);
+
+    if (priv->assoc_data->remove_blobs_left == 0) {
+        assoc_add_blobs(self);
+    } else {
+        while (g_variant_iter_loop(&iter, "{&s@ay}", &blob_name, &blob_data)) {
+            _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: removing blob '%s'",
+                  NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+                  blob_name);
+	    g_dubs_proxy_call (priv->iface_proxy,
+			   "RemoveBlob",
+			   g_variant_new ("s", blob_name),
+			   G_DBUS_CALL_FLAGS_NONE,
+			   -1,
+			   priv->assoc_data->cancellable,
+			   (GAsyncReadyCallback) assoc_remove_blob_cb,
+			   self);
+            /*_dbus_connection_call(self,
+                                  NM_WPAS_DBUS_IFACE_INTERFACE,
+                                  "RemoveBlob",
+                                  g_variant_new("(s)", blob_name),
+                                  G_VARIANT_TYPE("()"),
+                                  G_DBUS_CALL_FLAGS_NONE,
+                                  DBUS_TIMEOUT_MSEC,
+                                  priv->assoc_data->cancellable,
+                                  assoc_remove_blob_cb,
+                                  self);*/
+        }
+    }
+}
+
 static void
 assoc_add_network_cb (GDBusProxy *proxy, GAsyncResult *result, gpointer user_data)
 {
@@ -2382,29 +2542,17 @@ assoc_add_network_cb (GDBusProxy *proxy, GAsyncResult *result, gpointer user_dat
 
 	g_variant_get (reply, "(o)", &priv->net_path);
 
-	/* Send blobs first; otherwise jump to selecting the network */
-	blobs = nm_supplicant_config_get_blobs (priv->assoc_data->cfg);
-	priv->assoc_data->blobs_left = g_hash_table_size (blobs);
-
-	_LOGT ("assoc[%p]: network added (%s) (%u blobs left)", priv->assoc_data, priv->net_path, priv->assoc_data->blobs_left);
-
-	if (priv->assoc_data->blobs_left == 0)
-		assoc_call_select_network (self);
-	else {
-		g_hash_table_iter_init (&iter, blobs);
-		while (g_hash_table_iter_next (&iter, (gpointer) &blob_name, (gpointer) &blob_data)) {
-			g_dbus_proxy_call (priv->iface_proxy,
-			                   "AddBlob",
-			                   g_variant_new ("(s@ay)",
-			                                  blob_name,
-			                                  nm_utils_gbytes_to_variant_ay (blob_data)),
-			                   G_DBUS_CALL_FLAGS_NONE,
-			                   -1,
-			                   priv->assoc_data->cancellable,
-			                   (GAsyncReadyCallback) assoc_add_blob_cb,
-			                   self);
-		}
-	}
+	_LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: network added (%s)", NM_HASH_OBFUSCATE_PTR(priv->assoc_data), priv->net_path);
+
+	/* Delete any existing blobs before adding new ones */
+	g_dubs_proxy_call (priv->iface_proxy,
+			   "org.freedesktop.DBus.Properties.Get",
+			   g_variant_new ("(ss)", WPAS_DBUS_IFACE_INTERFACE, "Blobs"),
+			   G_DBUS_CALL_FLAGS_NONE,
+			   -1,
+			   priv->assoc_data->cancellable,
+			   (GAsyncReadyCallback) assoc_add_blob_cb,
+			   self);
 }
 
 static void
diff --git a/src/supplicant/tests/test-supplicant-config.c b/src/supplicant/tests/test-supplicant-config.c
index 008735b423..66280195a2 100644
--- a/src/supplicant/tests/test-supplicant-config.c
+++ b/src/supplicant/tests/test-supplicant-config.c
@@ -96,7 +96,7 @@ build_supplicant_config (NMConnection *connection,
 	NMSetting8021x *s_8021x;
 	gboolean success;
 
-	config = nm_supplicant_config_new (support_pmf, support_fils, FALSE, FALSE);
+	config = nm_supplicant_config_new (support_pmf, support_fils, FALSE, FALSE, nm_utils_get_connection_first_permissions_user(connection));
 
 	s_wifi = nm_connection_get_setting_wireless (connection);
 	g_assert (s_wifi);
@@ -119,6 +119,7 @@ build_supplicant_config (NMConnection *connection,
 			                                                          mtu,
 			                                                          pmf,
 			                                                          fils,
+										  NULL,
 			                                                          &error);
 	} else {
 		success = nm_supplicant_config_add_no_security (config, &error);
-- 
2.53.0

openSUSE Build Service is sponsored by