Commit a768f57286 for strongswan.org
commit a768f572864d7c9d4ef1a3651b48c4290a38cdd3
Author: Tobias Brunner <tobias@strongswan.org>
Date: Wed Nov 26 13:55:54 2025 +0100
nm: Create safe copies of files for user-specific connections
This ensures that only certificates/private keys accessible by the
configured user are accessed and prevents attackers from misusing
other user's credentials.
Also removed setting NM_VERSION_MIN_REQUIRED, which suppresses deprecation
warnings that were added with newer API versions, and
NM_VERSION_MAX_ALLOWED, which warns if using functions added in newer
API versions, so we always build against the latest API available.
But we check explicitly for the required function so this works with
older NM versions and automatically will use it if the function is
backported.
Note that we can't use BUILD_FROM_FILE to read the temporary files as that
uses mmap() which SELinux policies prevent us from using at the location
these files are stored ([/var]/run/NetworkManager/cert/).
Fixes: CVE-2025-9615
diff --git a/configure.ac b/configure.ac
index c020d92f86..c751bd8a6d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1255,8 +1255,10 @@ fi
if test x$nm = xtrue; then
PKG_CHECK_MODULES(nm, [gthread-2.0 libnm])
- AC_SUBST(nm_CFLAGS)
- AC_SUBST(nm_LIBS)
+ saved_LIBS=$LIBS
+ LIBS="$nm_LIBS"
+ AC_CHECK_FUNCS(nm_utils_copy_cert_as_user)
+ LIBS=$saved_LIBS
AC_MSG_CHECKING([for D-Bus policy directory])
if test -n "$dbuspolicydir" -a "x$dbuspolicydir" != xno; then
diff --git a/src/charon-nm/nm/nm_service.c b/src/charon-nm/nm/nm_service.c
index 7c010dae21..d24e7b29e4 100644
--- a/src/charon-nm/nm/nm_service.c
+++ b/src/charon-nm/nm/nm_service.c
@@ -15,6 +15,7 @@
*/
#include <stdio.h>
+#include <fcntl.h>
#include <inttypes.h>
#include <net/if.h>
@@ -54,6 +55,8 @@ typedef struct {
tun_device_t *tun;
/* name of the connection */
char *name;
+ /* temporary files for safe access */
+ GPtrArray *safe_files;
} NMStrongswanPluginPrivate;
G_DEFINE_TYPE_WITH_PRIVATE(NMStrongswanPlugin, nm_strongswan_plugin, NM_TYPE_VPN_SERVICE_PLUGIN)
@@ -421,6 +424,100 @@ METHOD(listener_t, child_updown, bool,
return TRUE;
}
+/**
+ * Determine the user configured in the given connection (if any).
+ */
+static const char *get_connection_permission_user(NMConnection *connection)
+{
+ NMSettingConnection *s_con;
+ const char *ptype, *pitem, *pdetail;
+ guint num_perms, i;
+
+ s_con = nm_connection_get_setting_connection(connection);
+ if (s_con)
+ {
+ num_perms = nm_setting_connection_get_num_permissions(s_con);
+ if (num_perms > 0)
+ {
+ for (i = 0; i < num_perms; i++)
+ {
+ if (nm_setting_connection_get_permission(s_con, i, &ptype,
+ &pitem, &pdetail) &&
+ streq(ptype, "user"))
+ {
+ return pitem;
+ }
+ }
+ }
+ }
+ return NULL;
+}
+
+/**
+ * Given a file name and an (optional) user name owning the connection, returns
+ * the file name to be used in the configuration.
+ *
+ * If the user is set, the file is accessed on behalf of the user, and copied
+ * safely to a temporary file readable only by root.
+ *
+ * The returned file name must be freed by the caller.
+ */
+static char *access_file(NMStrongswanPluginPrivate *priv, const char *filename,
+ const char *user, GError **err)
+{
+#ifdef HAVE_NM_UTILS_COPY_CERT_AS_USER
+ if (user)
+ {
+ char *tmp = nm_utils_copy_cert_as_user(filename, user, err);
+ if (tmp)
+ {
+ g_ptr_array_add(priv->safe_files, g_strdup(tmp));
+ }
+ return tmp;
+ }
+#endif
+ return g_strdup(filename);
+}
+
+/**
+ * Read the contents of the given file. If an (optional) user name owning
+ * the connection is given, the file is accessed safely on behalf of that user.
+ *
+ * We have to do this explicitly via open() because SELinux policies prevent us
+ * from using mmap(), which BUILD_FROM_FILE would use.
+ */
+static chunk_t read_safe_file(NMStrongswanPluginPrivate *priv,
+ const char *filename, const char *user,
+ GError **err)
+{
+ chunk_t chunk = chunk_empty;
+ char *safe_path;
+ int fd;
+
+ safe_path = access_file(priv, filename, user, err);
+ if (safe_path)
+ {
+ fd = open(safe_path, O_RDONLY);
+ if (fd != -1)
+ {
+ if (!chunk_from_fd(fd, &chunk))
+ {
+ g_set_error(err, NM_VPN_PLUGIN_ERROR,
+ NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
+ "Unable to read '%s': %s", safe_path, strerror(errno));
+ }
+ close(fd);
+ }
+ else
+ {
+ g_set_error(err, NM_VPN_PLUGIN_ERROR,
+ NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
+ "Unable to open '%s': %s", safe_path, strerror(errno));
+ }
+ }
+ return chunk;
+}
+
/**
* Find a certificate for which we have a private key on a smartcard
*/
@@ -479,12 +576,13 @@ static identification_t *find_smartcard_key(NMStrongswanPluginPrivate *priv,
*/
static bool add_auth_cfg_cert(NMStrongswanPluginPrivate *priv,
NMSettingVpn *vpn, peer_cfg_t *peer_cfg,
- GError **err)
+ const char *user, GError **err)
{
identification_t *id = NULL;
certificate_t *cert = NULL;
auth_cfg_t *auth;
const char *str, *method, *cert_source;
+ chunk_t safe_file;
method = nm_setting_vpn_get_data_item(vpn, "method");
cert_source = nm_setting_vpn_get_data_item(vpn, "cert-source") ?: method;
@@ -514,8 +612,14 @@ static bool add_auth_cfg_cert(NMStrongswanPluginPrivate *priv,
bool agent = streq(cert_source, "agent");
+ safe_file = read_safe_file(priv, str, user, err);
+ if (!safe_file.ptr)
+ {
+ return FALSE;
+ }
cert = lib->creds->create(lib->creds, CRED_CERTIFICATE, CERT_X509,
- BUILD_FROM_FILE, str, BUILD_END);
+ BUILD_BLOB, safe_file, BUILD_END);
+ chunk_clear(&safe_file);
if (!cert)
{
g_set_error(err, NM_VPN_PLUGIN_ERROR,
@@ -555,8 +659,14 @@ static bool add_auth_cfg_cert(NMStrongswanPluginPrivate *priv,
{
priv->creds->set_key_password(priv->creds, secret);
}
+ safe_file = read_safe_file(priv, str, user, err);
+ if (!safe_file.ptr)
+ {
+ return FALSE;
+ }
private = lib->creds->create(lib->creds, CRED_PRIVATE_KEY,
- KEY_ANY, BUILD_FROM_FILE, str, BUILD_END);
+ KEY_ANY, BUILD_BLOB, safe_file, BUILD_END);
+ chunk_clear(&safe_file);
if (!private)
{
g_set_error(err, NM_VPN_PLUGIN_ERROR,
@@ -735,7 +845,8 @@ static gboolean connect_(NMVpnServicePlugin *plugin, NMConnection *connection,
NMSettingVpn *vpn;
enumerator_t *enumerator;
identification_t *gateway = NULL;
- const char *str, *method;
+ const char *str, *method, *user;
+ chunk_t safe_file;
bool virtual, proposal;
proposal_t *prop;
ike_cfg_t *ike_cfg;
@@ -790,6 +901,8 @@ static gboolean connect_(NMVpnServicePlugin *plugin, NMConnection *connection,
DBG4(DBG_CFG, "%s",
nm_setting_to_string(NM_SETTING(vpn)));
+ user = get_connection_permission_user(connection);
+
ike.remote = (char*)nm_setting_vpn_get_data_item(vpn, "address");
if (!ike.remote || !*ike.remote)
{
@@ -818,8 +931,14 @@ static gboolean connect_(NMVpnServicePlugin *plugin, NMConnection *connection,
str = nm_setting_vpn_get_data_item(vpn, "certificate");
if (str)
{
+ safe_file = read_safe_file(priv, str, user, err);
+ if (!safe_file.ptr)
+ {
+ return FALSE;
+ }
cert = lib->creds->create(lib->creds, CRED_CERTIFICATE, CERT_X509,
- BUILD_FROM_FILE, str, BUILD_END);
+ BUILD_BLOB, safe_file, BUILD_END);
+ chunk_clear(&safe_file);
if (!cert)
{
g_set_error(err, NM_VPN_PLUGIN_ERROR,
@@ -907,7 +1026,7 @@ static gboolean connect_(NMVpnServicePlugin *plugin, NMConnection *connection,
streq(method, "agent") ||
streq(method, "smartcard"))
{
- if (!add_auth_cfg_cert (priv, vpn, peer_cfg, err))
+ if (!add_auth_cfg_cert(priv, vpn, peer_cfg, user, err))
{
peer_cfg->destroy(peer_cfg);
ike_cfg->destroy(ike_cfg);
@@ -1053,10 +1172,12 @@ static gboolean connect_(NMVpnServicePlugin *plugin, NMConnection *connection,
static gboolean need_secrets(NMVpnServicePlugin *plugin, NMConnection *connection,
const char **setting_name, GError **error)
{
+ NMStrongswanPluginPrivate *priv;
NMSettingVpn *settings;
const char *method, *cert_source, *path;
bool need_secret = FALSE;
+ priv = NM_STRONGSWAN_PLUGIN_GET_PRIVATE((NMStrongswanPlugin*)plugin);
settings = NM_SETTING_VPN(nm_connection_get_setting(connection,
NM_TYPE_SETTING_VPN));
method = nm_setting_vpn_get_data_item(settings, "method");
@@ -1088,18 +1209,27 @@ static gboolean need_secrets(NMVpnServicePlugin *plugin, NMConnection *connectio
if (path)
{
private_key_t *key;
+ const char *user;
+ chunk_t safe_file;
- /* try to load/decrypt the private key */
- key = lib->creds->create(lib->creds, CRED_PRIVATE_KEY,
- KEY_ANY, BUILD_FROM_FILE, path, BUILD_END);
- if (key)
- {
- key->destroy(key);
- need_secret = FALSE;
- }
- else if (nm_setting_vpn_get_secret(settings, "password"))
+ user = get_connection_permission_user(connection);
+ safe_file = read_safe_file(priv, path, user, error);
+ if (safe_file.ptr)
{
- need_secret = FALSE;
+ /* try to load/decrypt the private key */
+ key = lib->creds->create(lib->creds, CRED_PRIVATE_KEY,
+ KEY_ANY, BUILD_BLOB, safe_file,
+ BUILD_END);
+ chunk_clear(&safe_file);
+ if (key)
+ {
+ key->destroy(key);
+ need_secret = FALSE;
+ }
+ else if (nm_setting_vpn_get_secret(settings, "password"))
+ {
+ need_secret = FALSE;
+ }
}
}
}
@@ -1185,6 +1315,7 @@ static void nm_strongswan_plugin_init(NMStrongswanPlugin *plugin)
priv->listener.ike_reestablish_post = _ike_reestablish_post;
charon->bus->add_listener(charon->bus, &priv->listener);
priv->xfrmi_manager = lib->get(lib, KERNEL_NETLINK_XFRMI_MANAGER);
+ priv->safe_files = g_ptr_array_new_with_free_func(g_free);
}
/**
@@ -1194,11 +1325,17 @@ static void nm_strongswan_plugin_dispose(GObject *obj)
{
NMStrongswanPlugin *plugin;
NMStrongswanPluginPrivate *priv;
+ int i;
plugin = NM_STRONGSWAN_PLUGIN(obj);
priv = NM_STRONGSWAN_PLUGIN_GET_PRIVATE(plugin);
delete_interface(priv);
free(priv->name);
+ for (i = 0; i < priv->safe_files->len; i++)
+ {
+ unlink((const char *)priv->safe_files->pdata[i]);
+ }
+ g_ptr_array_unref(priv->safe_files);
G_OBJECT_CLASS (nm_strongswan_plugin_parent_class)->dispose (obj);
}
diff --git a/src/frontends/gnome/configure.ac b/src/frontends/gnome/configure.ac
index 7b9f9eff5a..0a3da4c344 100644
--- a/src/frontends/gnome/configure.ac
+++ b/src/frontends/gnome/configure.ac
@@ -78,8 +78,6 @@ AC_ARG_WITH(
AM_CONDITIONAL(WITH_GTK4, test "$with_gtk4" != no)
PKG_CHECK_MODULES(LIBNM, libnm >= 1.1.0)
-LIBNM_CFLAGS="$LIBNM_CFLAGS -DNM_VERSION_MIN_REQUIRED=NM_VERSION_1_2"
-LIBNM_CFLAGS="$LIBNM_CFLAGS -DNM_VERSION_MAX_ALLOWED=NM_VERSION_1_2"
PKG_CHECK_MODULES(LIBNMA, libnma >= 1.1.0)
if test x"$with_gtk4" != xno; then
diff --git a/src/frontends/gnome/nm-strongswan-service.name.in b/src/frontends/gnome/nm-strongswan-service.name.in
index 373262a41a..6f6ae00f9d 100644
--- a/src/frontends/gnome/nm-strongswan-service.name.in
+++ b/src/frontends/gnome/nm-strongswan-service.name.in
@@ -2,6 +2,7 @@
name=strongswan
service=org.freedesktop.NetworkManager.strongswan
program=@CHARON@
+supports-safe-private-file-access=true
[libnm]
plugin=@NM_PLUGINDIR@/libnm-vpn-plugin-strongswan.so