libnm: add function to copy a certificate or key as user

Add a new public function nm_utils_copy_cert_as_user() to libnm. It
reads a certificate or key file on behalf of the given user and writes
it to a directory in /run/NetworkManager. It is useful for VPN plugins
that run as root and need to verify that the user owning the
connection (the one listed in the connection.permissions property) can
access the file.

(cherry picked from commit 1a52bbe7c9)
(cherry picked from commit 3d85bace3d)
(cherry picked from commit 4587832735)
This commit is contained in:
Beniamino Galvani 2025-09-26 21:04:04 +02:00 committed by Íñigo Huguet
parent 29d190ef95
commit bb0d29a8f1
11 changed files with 378 additions and 6 deletions

2
NEWS
View file

@ -8,6 +8,8 @@ Overview of changes since NetworkManager-1.52.1
* For private connections (the ones that specify a user in the * For private connections (the ones that specify a user in the
"connection.permissions" property), verify that the user can access "connection.permissions" property), verify that the user can access
the 802.1X certificates and keys set in the connection. the 802.1X certificates and keys set in the connection.
* Introduce a libnm function that can be used by VPN plugins to check
user permissions on certificate and keys.
============================================= =============================================
NetworkManager-1.52.1 NetworkManager-1.52.1

View file

@ -890,6 +890,7 @@ fi
%{_libexecdir}/nm-dispatcher %{_libexecdir}/nm-dispatcher
%{_libexecdir}/nm-initrd-generator %{_libexecdir}/nm-initrd-generator
%{_libexecdir}/nm-daemon-helper %{_libexecdir}/nm-daemon-helper
%{_libexecdir}/nm-libnm-helper
%{_libexecdir}/nm-priv-helper %{_libexecdir}/nm-priv-helper
%dir %{_libdir}/%{name} %dir %{_libdir}/%{name}
%dir %{nmplugindir} %dir %{nmplugindir}

View file

@ -2051,5 +2051,6 @@ global:
libnm_1_52_2 { libnm_1_52_2 {
global: global:
nm_utils_copy_cert_as_user;
nm_vpn_plugin_info_supports_safe_private_file_access; nm_vpn_plugin_info_supports_safe_private_file_access;
} libnm_1_52_0; } libnm_1_52_0;

View file

@ -5,6 +5,7 @@ test_units = [
'test-nm-client', 'test-nm-client',
'test-remote-settings-client', 'test-remote-settings-client',
'test-secret-agent', 'test-secret-agent',
'test-copy-cert-as-user'
] ]
foreach test_unit: test_units foreach test_unit: test_units
@ -37,12 +38,15 @@ foreach test_unit: test_units
], ],
) )
# test-copy-cert-as-user is a manual test, don't run it automatically
if test_unit != 'test-copy-cert-as-user'
test( test(
'src/libnm-client-impl/tests/' + test_unit, 'src/libnm-client-impl/tests/' + test_unit,
test_script, test_script,
timeout: 90, timeout: 90,
args: test_args + [exe.full_path()], args: test_args + [exe.full_path()],
) )
endif
endforeach endforeach
if enable_introspection if enable_introspection

View file

@ -0,0 +1,32 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* This is a program to manually test the
* nm_utils_copy_cert_as_user() libnm function.
*/
#include "libnm-client-impl/nm-default-libnm.h"
#include "nm-utils.h"
int
main(int argc, char **argv)
{
gs_free_error GError *error = NULL;
gs_free char *filename = NULL;
if (argc != 3) {
g_printerr("Usage: %s <FILE> <USER>\n", argv[0]);
return 1;
}
filename = nm_utils_copy_cert_as_user(argv[1], argv[2], &error);
if (!filename) {
g_printerr("Error: %s\n", error->message);
return 1;
}
g_print("%s\n", filename);
return 0;
}

View file

@ -93,6 +93,7 @@ def syms_from_ver(verfile):
# hardcode it. # hardcode it.
c_syms["nm_ethtool_optname_is_feature"] = "1.20" c_syms["nm_ethtool_optname_is_feature"] = "1.20"
c_syms["nm_setting_bond_port_get_prio"] = "1.44" c_syms["nm_setting_bond_port_get_prio"] = "1.44"
c_syms["nm_utils_copy_cert_as_user"] = "1.56"
c_syms["nm_vpn_plugin_info_supports_safe_private_file_access"] = "1.56" c_syms["nm_vpn_plugin_info_supports_safe_private_file_access"] = "1.56"
return c_syms return c_syms

View file

@ -17,6 +17,7 @@
#include <linux/pkt_sched.h> #include <linux/pkt_sched.h>
#include <linux/if_infiniband.h> #include <linux/if_infiniband.h>
#include "libnm-glib-aux/nm-io-utils.h"
#include "libnm-glib-aux/nm-uuid.h" #include "libnm-glib-aux/nm-uuid.h"
#include "libnm-glib-aux/nm-json-aux.h" #include "libnm-glib-aux/nm-json-aux.h"
#include "libnm-glib-aux/nm-str-buf.h" #include "libnm-glib-aux/nm-str-buf.h"
@ -6195,3 +6196,257 @@ nm_utils_ensure_gtypes(void)
for (meta_type = 0; meta_type < _NM_META_SETTING_TYPE_NUM; meta_type++) for (meta_type = 0; meta_type < _NM_META_SETTING_TYPE_NUM; meta_type++)
nm_meta_setting_infos[meta_type].get_setting_gtype(); nm_meta_setting_infos[meta_type].get_setting_gtype();
} }
/*****************************************************************************/
typedef struct {
GPid pid;
GSource *child_watch_source;
GMainLoop *loop;
GError *error;
int child_stdout;
int child_stderr;
GSource *output_source;
GSource *error_source;
NMStrBuf output_buffer;
NMStrBuf error_buffer;
} HelperInfo;
static void
helper_complete(HelperInfo *info, GError *error_take)
{
if (error_take) {
if (!info->error)
info->error = error_take;
else
g_error_free(error_take);
}
if (info->output_source || info->error_source || info->pid != -1) {
/* Wait that the pipe is closed and process has terminated */
return;
}
if (info->error && info->error_buffer.len > 0) {
/* Prefer the message from stderr as it's more informative */
g_error_free(info->error);
info->error = g_error_new(NM_CONNECTION_ERROR,
NM_CONNECTION_ERROR_FAILED,
"%s",
nm_str_buf_get_str(&info->error_buffer));
}
g_main_loop_quit(info->loop);
}
static gboolean
helper_have_err_data(int fd, GIOCondition condition, gpointer user_data)
{
HelperInfo *info = user_data;
gssize n_read;
GError *error = NULL;
n_read = nm_utils_fd_read(fd, &info->error_buffer);
if (n_read > 0)
return G_SOURCE_CONTINUE;
nm_clear_g_source_inst(&info->error_source);
nm_clear_fd(&info->child_stderr);
if (n_read < 0) {
error = g_error_new(NM_UTILS_ERROR,
NM_UTILS_ERROR_UNKNOWN,
"read from process returned %d (%s)",
(int) -n_read,
nm_strerror_native((int) -n_read));
}
helper_complete(info, error);
return G_SOURCE_CONTINUE;
}
static gboolean
helper_have_data(int fd, GIOCondition condition, gpointer user_data)
{
HelperInfo *info = user_data;
gssize n_read;
GError *error = NULL;
n_read = nm_utils_fd_read(fd, &info->output_buffer);
if (n_read > 0)
return G_SOURCE_CONTINUE;
nm_clear_g_source_inst(&info->output_source);
nm_clear_fd(&info->child_stdout);
if (n_read < 0) {
error = g_error_new(NM_UTILS_ERROR,
NM_UTILS_ERROR_UNKNOWN,
"read from process returned %d (%s)",
(int) -n_read,
nm_strerror_native((int) -n_read));
}
helper_complete(info, error);
return G_SOURCE_CONTINUE;
}
static void
helper_child_terminated(GPid pid, int status, gpointer user_data)
{
HelperInfo *info = user_data;
gs_free char *status_desc = NULL;
GError *error = NULL;
info->pid = -1;
nm_clear_g_source_inst(&info->child_watch_source);
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
if (!status_desc)
status_desc = nm_utils_get_process_exit_status_desc(status);
error =
g_error_new(NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "helper process %s", status_desc);
}
helper_complete(info, error);
}
#define RUN_CERT_DIR NMRUNDIR "/cert"
/**
* nm_utils_copy_cert_as_user:
* @filename: the file name of the certificate or key to copy
* @user: the user to impersonate when reading the file
* @error: (nullable): return location for a #GError, or %NULL
*
* Reads @filename on behalf of user @user and writes the
* content to a new file in /run/NetworkManager/cert/.
* The new file has permission 600 and is owned by root.
*
* This function is useful for VPN plugins that run as root and need
* to verify that the user owning the connection (the one listed in the
* connection.permissions property) can access the file.
*
* Returns: (transfer full): the name of the new temporary file. Or %NULL
* if an error occurred, including when the given user can't access the
* file.
*
* Since: 1.56, 1.52.2
*/
char *
nm_utils_copy_cert_as_user(const char *filename, const char *user, GError **error)
{
gs_unref_bytes GBytes *bytes = NULL;
char dst_path[] = RUN_CERT_DIR "/XXXXXX";
HelperInfo info = {
.child_stdout = -1,
.child_stderr = -1,
};
GMainContext *context;
int fd = -1;
g_return_val_if_fail(filename, NULL);
g_return_val_if_fail(user, NULL);
g_return_val_if_fail(!error || !*error, NULL);
if (geteuid() != 0) {
g_set_error_literal(error,
NM_CONNECTION_ERROR,
NM_CONNECTION_ERROR_INVALID_PROPERTY,
_("This function needs to be called by root"));
return NULL;
}
if (!g_spawn_async_with_pipes(
"/",
(char **)
NM_MAKE_STRV(LIBEXECDIR "/nm-libnm-helper", "read-file-as-user", filename, user),
(char **) NM_MAKE_STRV(),
G_SPAWN_CLOEXEC_PIPES | G_SPAWN_DO_NOT_REAP_CHILD,
NULL,
NULL,
&info.pid,
NULL,
&info.child_stdout,
&info.child_stderr,
error)) {
return NULL;
}
context = g_main_context_new();
info.loop = g_main_loop_new(context, FALSE);
/* Watch process */
info.child_watch_source = nm_g_child_watch_source_new(info.pid,
G_PRIORITY_DEFAULT,
helper_child_terminated,
&info,
NULL);
g_source_attach(info.child_watch_source, context);
/* Watch stdout */
info.output_buffer = NM_STR_BUF_INIT(0, FALSE);
info.output_source = nm_g_unix_fd_source_new(info.child_stdout,
G_IO_IN | G_IO_ERR | G_IO_HUP,
G_PRIORITY_DEFAULT,
helper_have_data,
&info,
NULL);
g_source_attach(info.output_source, context);
/* Watch stderr */
info.error_buffer = NM_STR_BUF_INIT(0, FALSE);
info.error_source = nm_g_unix_fd_source_new(info.child_stderr,
G_IO_IN | G_IO_ERR | G_IO_HUP,
G_PRIORITY_DEFAULT,
helper_have_err_data,
&info,
NULL);
g_source_attach(info.error_source, context);
/* Wait termination */
g_main_loop_run(info.loop);
g_clear_pointer(&info.loop, g_main_loop_unref);
g_clear_pointer(&context, g_main_context_unref);
if (info.error) {
nm_str_buf_destroy(&info.output_buffer);
nm_str_buf_destroy(&info.error_buffer);
g_propagate_error(error, g_steal_pointer(&info.error));
return NULL;
}
/* Write the data to a new file */
bytes = g_bytes_new(nm_str_buf_get_str_unsafe(&info.output_buffer), info.output_buffer.len);
nm_str_buf_destroy(&info.output_buffer);
nm_str_buf_destroy(&info.error_buffer);
mkdir(RUN_CERT_DIR, 0600);
fd = mkstemp(dst_path);
if (fd < 0) {
g_set_error_literal(error,
NM_CONNECTION_ERROR,
NM_CONNECTION_ERROR_INVALID_PROPERTY,
_("Failure creating the temporary file"));
return NULL;
}
nm_close(fd);
if (!nm_utils_file_set_contents(dst_path,
g_bytes_get_data(bytes, NULL),
g_bytes_get_size(bytes),
0600,
NULL,
NULL,
error)) {
return NULL;
}
return g_strdup(dst_path);
}

View file

@ -261,6 +261,9 @@ nm_utils_base64secret_decode(const char *base64_key, gsize required_key_len, gui
NM_AVAILABLE_IN_1_42 NM_AVAILABLE_IN_1_42
void nm_utils_ensure_gtypes(void); void nm_utils_ensure_gtypes(void);
NM_AVAILABLE_IN_1_52_2
char *nm_utils_copy_cert_as_user(const char *filename, const char *user, GError **error);
G_END_DECLS G_END_DECLS
#endif /* __NM_UTILS_H__ */ #endif /* __NM_UTILS_H__ */

View file

@ -17,6 +17,14 @@ all the threads of the process).
This is not directly useful to the user. This is not directly useful to the user.
nm-libnm-helper
---------------
A internal helper application that is spawned by libnm to perform
certain actions without impacting the calling process.
This is not directly useful to the user.
nm-priv-helper nm-priv-helper
-------------- --------------

View file

@ -18,6 +18,26 @@ executable(
install_dir: nm_libexecdir, install_dir: nm_libexecdir,
) )
# nm-libnm-helper
executable(
'nm-libnm-helper',
['nm-libnm-helper.c'],
include_directories : [
src_inc,
top_inc,
],
dependencies: [
glib_dep,
],
link_with: [
libnm_glib_aux,
libnm_std_aux,
],
install: true,
install_dir: nm_libexecdir,
)
# nm-priv-helper # nm-priv-helper
configure_file( configure_file(

View file

@ -0,0 +1,45 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "libnm-std-aux/nm-default-std.h"
#include <stdio.h>
enum {
RETURN_SUCCESS = 0,
RETURN_INVALID_CMD = 1,
RETURN_INVALID_ARGS = 2,
RETURN_ERROR = 3,
};
static int
read_file_as_user(const char *filename, const char *user)
{
char error[1024];
if (!nm_utils_set_effective_user(user, error, sizeof(error))) {
fprintf(stderr, "Failed to set effective user '%s': %s", user, error);
return RETURN_ERROR;
}
if (!nm_utils_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)
{
if (argc <= 1)
return RETURN_INVALID_CMD;
if (nm_streq(argv[1], "read-file-as-user")) {
if (argc != 4)
return RETURN_INVALID_ARGS;
return read_file_as_user(argv[2], argv[3]);
}
return RETURN_INVALID_CMD;
}