summary refs log tree commit diff
path: root/src/core
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2025-12-13 13:48:57 +0100
committerMichael Biebl <biebl@debian.org>2025-12-13 13:48:57 +0100
commit6de29285e533f4fec22a219013f3687edb6b7399 (patch)
tree4b1627413f2e8fc8e3ccfa1496a79a04b8a71bde /src/core
parent01609e485803fa250413385ae209660fb7b30a2e (diff)
New upstream version 1.54.3 upstream/1.54.3
Diffstat (limited to 'src/core')
-rw-r--r--src/core/devices/nm-device-ethernet.c11
-rw-r--r--src/core/devices/nm-device-macsec.c11
-rw-r--r--src/core/devices/nm-device-private.h2
-rw-r--r--src/core/devices/nm-device-utils.c3
-rw-r--r--src/core/devices/nm-device.c127
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c4
-rw-r--r--src/core/nm-core-utils.c221
-rw-r--r--src/core/nm-core-utils.h19
-rw-r--r--src/core/supplicant/nm-supplicant-config.c170
-rw-r--r--src/core/supplicant/nm-supplicant-config.h4
-rw-r--r--src/core/supplicant/nm-supplicant-interface.c191
-rw-r--r--src/core/supplicant/tests/test-supplicant-config.c4
12 files changed, 662 insertions, 105 deletions
diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c
index 4034fdaa..db1245b3 100644
--- a/src/core/devices/nm-device-ethernet.c
+++ b/src/core/devices/nm-device-ethernet.c
@@ -630,10 +630,17 @@ build_supplicant_config(NMDeviceEthernet *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(NM_SUPPL_CAP_MASK_NONE);
+    config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE,
+                                      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/core/devices/nm-device-macsec.c b/src/core/devices/nm-device-macsec.c
index 2ff1eeb3..1659ea05 100644
--- a/src/core/devices/nm-device-macsec.c
+++ b/src/core/devices/nm-device-macsec.c
@@ -201,7 +201,8 @@ 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(NM_SUPPL_CAP_MASK_NONE);
+    config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE,
+                                      nm_utils_get_connection_first_permissions_user(connection));
 
     s_macsec = nm_device_get_applied_setting(NM_DEVICE(self), NM_TYPE_SETTING_MACSEC);
 
@@ -227,7 +228,13 @@ 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/core/devices/nm-device-private.h b/src/core/devices/nm-device-private.h
index 2f73a01b..2568d9f1 100644
--- a/src/core/devices/nm-device-private.h
+++ b/src/core/devices/nm-device-private.h
@@ -179,4 +179,6 @@ void nm_device_auth_request(NMDevice                      *self,
 
 void nm_device_link_properties_set(NMDevice *self, gboolean reapply);
 
+GHashTable *nm_device_get_private_files(NMDevice *self);
+
 #endif /* NM_DEVICE_PRIVATE_H */
diff --git a/src/core/devices/nm-device-utils.c b/src/core/devices/nm-device-utils.c
index a78499fb..3e86e2e8 100644
--- a/src/core/devices/nm-device-utils.c
+++ b/src/core/devices/nm-device-utils.c
@@ -237,7 +237,7 @@ resolve_addr_helper_cb(GObject *source, GAsyncResult *result, gpointer user_data
     gs_free_error GError *error  = NULL;
     gs_free char         *output = NULL;
 
-    output = nm_utils_spawn_helper_finish(result, &error);
+    output = nm_utils_spawn_helper_finish_string(result, &error);
     if (nm_utils_error_is_cancelled(error))
         return;
 
@@ -276,6 +276,7 @@ resolve_addr_spawn_helper(ResolveAddrInfo *info, ResolveAddrService services)
     nm_inet_ntop(info->addr_family, &info->address, addr_str);
     _LOG2D(info, "start lookup via nm-daemon-helper using services: %s", str);
     nm_utils_spawn_helper(NM_MAKE_STRV("resolve-address", addr_str, str),
+                          FALSE,
                           g_task_get_cancellable(info->task),
                           resolve_addr_helper_cb,
                           info);
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index 6bba982a..b0a3f780 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -335,6 +335,12 @@ typedef struct {
     int           addr_family;
 } HostnameResolver;
 
+typedef enum {
+    PRIVATE_FILES_STATE_UNKNOWN = 0,
+    PRIVATE_FILES_STATE_READING,
+    PRIVATE_FILES_STATE_DONE,
+} PrivateFilesState;
+
 /*****************************************************************************/
 
 enum {
@@ -771,6 +777,13 @@ typedef struct _NMDevicePrivate {
         guint64  rx_bytes;
     } stats;
 
+    struct {
+        GHashTable       *table;
+        GCancellable     *cancellable;
+        char             *user;
+        PrivateFilesState state;
+    } private_files;
+
     bool mtu_force_set_done : 1;
 
     bool needs_ip6_subnet : 1;
@@ -10807,6 +10820,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))
+        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, FALSE);
+}
+
+GHashTable *
+nm_device_get_private_files(NMDevice *self)
+{
+    return NM_DEVICE_GET_PRIVATE(self)->private_files.table;
+}
+
 /*
  * activate_stage2_device_config
  *
@@ -10819,6 +10875,7 @@ activate_stage2_device_config(NMDevice *self)
 {
     NMDevicePrivate *priv  = NM_DEVICE_GET_PRIVATE(self);
     NMDeviceClass   *klass = NM_DEVICE_GET_CLASS(self);
+    NMConnection    *applied;
     NMActStageReturn ret;
     NMSettingWired  *s_wired;
     gboolean         no_firmware = FALSE;
@@ -10827,6 +10884,68 @@ activate_stage2_device_config(NMDevice *self)
 
     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_managed_type_is_external(self)) {
         _ethtool_state_set(self);
         nm_device_link_properties_set(self, FALSE);
@@ -10843,7 +10962,7 @@ activate_stage2_device_config(NMDevice *self)
         priv->tc_committed = TRUE;
     }
 
-    nm_routing_rules_sync(nm_device_get_applied_connection(self),
+    nm_routing_rules_sync(applied,
                           NM_TERNARY_TRUE,
                           klass->get_extra_rules,
                           self,
@@ -17154,6 +17273,12 @@ nm_device_cleanup(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanu
     if (klass->deactivate)
         klass->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/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index b890b110..148caa11 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -2946,7 +2946,8 @@ build_supplicant_config(NMDeviceWifi         *self,
     s_wireless = nm_connection_get_setting_wireless(connection);
     g_return_val_if_fail(s_wireless != NULL, NULL);
 
-    config = nm_supplicant_config_new(nm_supplicant_interface_get_capabilities(priv->sup_iface));
+    config = nm_supplicant_config_new(nm_supplicant_interface_get_capabilities(priv->sup_iface),
+                                      nm_utils_get_connection_first_permissions_user(connection));
 
     /* Warn if AP mode may not be supported */
     if (nm_streq0(nm_setting_wireless_get_mode(s_wireless), NM_SETTING_WIRELESS_MODE_AP)
@@ -3022,6 +3023,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/core/nm-core-utils.c b/src/core/nm-core-utils.c
index d3c2eb21..4a7f760d 100644
--- a/src/core/nm-core-utils.c
+++ b/src/core/nm-core-utils.c
@@ -5011,6 +5011,7 @@ typedef struct {
     int      child_stdin;
     int      child_stdout;
     int      child_stderr;
+    gboolean binary_output;
     GSource *input_source;
     GSource *output_source;
     GSource *error_source;
@@ -5090,9 +5091,17 @@ helper_complete(HelperInfo *info, GError *error)
     }
 
     nm_clear_g_cancellable_disconnect(g_task_get_cancellable(info->task), &info->cancellable_id);
-    g_task_return_pointer(info->task,
-                          nm_str_buf_finalize(&info->in_buffer, NULL) ?: g_new0(char, 1),
-                          g_free);
+
+    if (info->binary_output) {
+        g_task_return_pointer(
+            info->task,
+            g_bytes_new(nm_str_buf_get_str_unsafe(&info->in_buffer), info->in_buffer.len),
+            (GDestroyNotify) (g_bytes_unref));
+    } else {
+        g_task_return_pointer(info->task,
+                              nm_str_buf_finalize(&info->in_buffer, NULL) ?: g_new0(char, 1),
+                              g_free);
+    }
     helper_info_free(info);
 }
 
@@ -5235,6 +5244,7 @@ helper_cancelled(GObject *object, gpointer user_data)
 
 void
 nm_utils_spawn_helper(const char *const  *args,
+                      gboolean            binary_output,
                       GCancellable       *cancellable,
                       GAsyncReadyCallback callback,
                       gpointer            cb_data)
@@ -5250,9 +5260,14 @@ nm_utils_spawn_helper(const char *const  *args,
 
     info  = g_new(HelperInfo, 1);
     *info = (HelperInfo) {
-        .task = nm_g_task_new(NULL, cancellable, nm_utils_spawn_helper, callback, cb_data),
+        .task          = nm_g_task_new(NULL, cancellable, nm_utils_spawn_helper, callback, cb_data),
+        .binary_output = binary_output,
     };
 
+    /* Store if the caller requested binary output so that we can check later
+     * that the right result function is called. */
+    g_task_set_task_data(info->task, GINT_TO_POINTER(binary_output), NULL);
+
     if (!g_spawn_async_with_pipes("/",
                                   (char **) NM_MAKE_STRV(LIBEXECDIR "/nm-daemon-helper"),
                                   (char **) NM_MAKE_STRV(),
@@ -5363,11 +5378,25 @@ nm_utils_spawn_helper(const char *const  *args,
 }
 
 char *
-nm_utils_spawn_helper_finish(GAsyncResult *result, GError **error)
+nm_utils_spawn_helper_finish_string(GAsyncResult *result, GError **error)
+{
+    GTask *task = G_TASK(result);
+
+    nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper));
+    /* Check binary_output */
+    nm_assert(GPOINTER_TO_INT(g_task_get_task_data(task)) == FALSE);
+
+    return g_task_propagate_pointer(task, error);
+}
+
+GBytes *
+nm_utils_spawn_helper_finish_binary(GAsyncResult *result, GError **error)
 {
     GTask *task = G_TASK(result);
 
     nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper));
+    /* Check binary_output */
+    nm_assert(GPOINTER_TO_INT(g_task_get_task_data(task)) == TRUE);
 
     return g_task_propagate_pointer(task, error);
 }
@@ -5474,3 +5503,185 @@ nm_utils_shorten_hostname(const char *hostname, char **shortened)
     *shortened = g_steal_pointer(&s);
     return TRUE;
 }
+
+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)) {
+        /* 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/core/nm-core-utils.h b/src/core/nm-core-utils.h
index fdfed5f6..224018c6 100644
--- a/src/core/nm-core-utils.h
+++ b/src/core/nm-core-utils.h
@@ -478,11 +478,13 @@ guint8 nm_wifi_utils_level_to_quality(int val);
 /*****************************************************************************/
 
 void nm_utils_spawn_helper(const char *const  *args,
+                           gboolean            binary_output,
                            GCancellable       *cancellable,
                            GAsyncReadyCallback callback,
                            gpointer            cb_data);
 
-char *nm_utils_spawn_helper_finish(GAsyncResult *result, GError **error);
+char   *nm_utils_spawn_helper_finish_string(GAsyncResult *result, GError **error);
+GBytes *nm_utils_spawn_helper_finish_binary(GAsyncResult *result, GError **error);
 
 /*****************************************************************************/
 
@@ -490,4 +492,19 @@ uid_t nm_utils_get_nm_uid(void);
 
 gid_t nm_utils_get_nm_gid(void);
 
+/*****************************************************************************/
+
+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/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c
index 9e066395..56cc832b 100644
--- a/src/core/supplicant/nm-supplicant-config.c
+++ b/src/core/supplicant/nm-supplicant-config.c
@@ -30,6 +30,7 @@ typedef struct {
 typedef struct {
     GHashTable    *config;
     GHashTable    *blobs;
+    char          *private_user;
     NMSupplCapMask capabilities;
     guint32        ap_scan;
     bool           fast_required : 1;
@@ -60,7 +61,7 @@ _get_capability(NMSupplicantConfigPrivate *priv, NMSupplCapType type)
 }
 
 NMSupplicantConfig *
-nm_supplicant_config_new(NMSupplCapMask capabilities)
+nm_supplicant_config_new(NMSupplCapMask capabilities, const char *private_user)
 {
     NMSupplicantConfigPrivate *priv;
     NMSupplicantConfig        *self;
@@ -69,6 +70,7 @@ nm_supplicant_config_new(NMSupplCapMask capabilities)
     priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
 
     priv->capabilities = capabilities;
+    priv->private_user = g_strdup(private_user);
 
     return self;
 }
@@ -258,19 +260,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;
@@ -283,6 +285,7 @@ nm_supplicant_config_finalize(GObject *object)
 
     g_hash_table_destroy(priv->config);
     nm_clear_pointer(&priv->blobs, g_hash_table_destroy);
+    nm_clear_pointer(&priv->private_user, g_free);
 
     G_OBJECT_CLASS(nm_supplicant_config_parent_class)->finalize(object);
 }
@@ -930,6 +933,7 @@ nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig
                                                    guint32                       mtu,
                                                    NMSettingWirelessSecurityPmf  pmf,
                                                    NMSettingWirelessSecurityFils fils,
+                                                   GHashTable                   *files,
                                                    GError                      **error)
 {
     NMSupplicantConfigPrivate    *priv          = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
@@ -1284,6 +1288,7 @@ nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig
                                                         con_uuid,
                                                         mtu,
                                                         FALSE,
+                                                        files,
                                                         error))
                 return FALSE;
         }
@@ -1365,6 +1370,7 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
                                        const char         *con_uuid,
                                        guint32             mtu,
                                        gboolean            wired,
+                                       GHashTable         *files,
                                        GError            **error)
 {
     NMSupplicantConfigPrivate    *priv;
@@ -1594,24 +1600,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,
@@ -1627,26 +1630,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(
@@ -1663,6 +1672,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);
@@ -1714,21 +1732,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:
@@ -1745,6 +1759,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;
@@ -1768,20 +1795,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(
@@ -1797,26 +1820,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:
@@ -1834,6 +1866,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;
@@ -1857,20 +1902,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(
@@ -1886,6 +1927,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/core/supplicant/nm-supplicant-config.h b/src/core/supplicant/nm-supplicant-config.h
index c52b756e..96460b86 100644
--- a/src/core/supplicant/nm-supplicant-config.h
+++ b/src/core/supplicant/nm-supplicant-config.h
@@ -29,7 +29,7 @@ typedef struct _NMSupplicantConfigClass NMSupplicantConfigClass;
 
 GType nm_supplicant_config_get_type(void);
 
-NMSupplicantConfig *nm_supplicant_config_new(NMSupplCapMask capabilities);
+NMSupplicantConfig *nm_supplicant_config_new(NMSupplCapMask capabilities, const char *private_user);
 
 guint32 nm_supplicant_config_get_ap_scan(NMSupplicantConfig *self);
 
@@ -57,6 +57,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, GError **error);
@@ -66,6 +67,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/core/supplicant/nm-supplicant-interface.c b/src/core/supplicant/nm-supplicant-interface.c
index 514b7c0d..ef72447a 100644
--- a/src/core/supplicant/nm-supplicant-interface.c
+++ b/src/core/supplicant/nm-supplicant-interface.c
@@ -46,6 +46,7 @@ typedef struct {
     gpointer                     user_data;
     guint                        fail_on_idle_id;
     guint                        blobs_left;
+    guint                        remove_blobs_left;
     guint                        calls_left;
     struct _AddNetworkData      *add_network_data;
 } AssocData;
@@ -2264,6 +2265,7 @@ assoc_add_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data)
         return;
     }
 
+    nm_assert(priv->assoc_data->blobs_left > 0);
     priv->assoc_data->blobs_left--;
     _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: blob added (%u left)",
           NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
@@ -2273,18 +2275,156 @@ assoc_add_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 }
 
 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);
+        _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))
+        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))
+        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);
+            _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(GObject *source, GAsyncResult *result, gpointer user_data)
 {
     AddNetworkData                 *add_network_data = user_data;
     AssocData                      *assoc_data;
     NMSupplicantInterface          *self;
     NMSupplicantInterfacePrivate   *priv;
-    gs_unref_variant GVariant      *res   = NULL;
-    gs_free_error GError           *error = NULL;
-    GHashTable                     *blobs;
-    GHashTableIter                  iter;
-    const char                     *blob_name;
-    GBytes                         *blob_data;
+    gs_unref_variant GVariant      *res         = NULL;
+    gs_free_error GError           *error       = NULL;
     nm_auto_ref_string NMRefString *name_owner  = NULL;
     nm_auto_ref_string NMRefString *object_path = NULL;
 
@@ -2336,34 +2476,21 @@ assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data)
     nm_assert(!priv->net_path);
     g_variant_get(res, "(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 = blobs ? g_hash_table_size(blobs) : 0u;
-
-    _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: network added (%s) (%u blobs left)",
+    _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: network added (%s)",
           NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
-          priv->net_path,
-          priv->assoc_data->blobs_left);
-
-    if (priv->assoc_data->blobs_left == 0) {
-        assoc_call_select_network(self);
-        return;
-    }
+          priv->net_path);
 
-    g_hash_table_iter_init(&iter, blobs);
-    while (g_hash_table_iter_next(&iter, (gpointer) &blob_name, (gpointer) &blob_data)) {
-        _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);
-    }
+    /* Delete any existing blobs before adding new ones */
+    _dbus_connection_call(self,
+                          DBUS_INTERFACE_PROPERTIES,
+                          "Get",
+                          g_variant_new("(ss)", NM_WPAS_DBUS_IFACE_INTERFACE, "Blobs"),
+                          G_VARIANT_TYPE("(v)"),
+                          G_DBUS_CALL_FLAGS_NONE,
+                          DBUS_TIMEOUT_MSEC,
+                          priv->assoc_data->cancellable,
+                          assoc_get_blobs_cb,
+                          self);
 }
 
 static void
diff --git a/src/core/supplicant/tests/test-supplicant-config.c b/src/core/supplicant/tests/test-supplicant-config.c
index 1ca5b26e..416fe005 100644
--- a/src/core/supplicant/tests/test-supplicant-config.c
+++ b/src/core/supplicant/tests/test-supplicant-config.c
@@ -98,7 +98,8 @@ build_supplicant_config(NMConnection  *connection,
     NMSetting8021x                     *s_8021x;
     gboolean                            success;
 
-    config = nm_supplicant_config_new(capabilities);
+    config = nm_supplicant_config_new(capabilities,
+                                      nm_utils_get_connection_first_permissions_user(connection));
 
     s_wifi = nm_connection_get_setting_wireless(connection);
     g_assert(s_wifi);
@@ -120,6 +121,7 @@ build_supplicant_config(NMConnection  *connection,
                                                                mtu,
                                                                pmf,
                                                                fils,
+                                                               NULL,
                                                                &error);
     } else {
         success = nm_supplicant_config_add_no_security(config, &error);