From bb0d29a8f10a143c59a8c5ec55a958d73c61a21c Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Fri, 26 Sep 2025 21:04:04 +0200 Subject: [PATCH] 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 1a52bbe7c9dcabc066d8930dfd7b7cfe74dabf78) (cherry picked from commit 3d85bace3dcd8aaf9773db90fa412a7cdc131e4b) (cherry picked from commit 4587832735653ff27eae7afb7c4e3541d6cfc271) --- NEWS | 2 + contrib/fedora/rpm/NetworkManager.spec | 1 + src/libnm-client-impl/libnm.ver | 1 + src/libnm-client-impl/tests/meson.build | 16 +- .../tests/test-copy-cert-as-user.c | 32 +++ src/libnm-client-impl/tests/test-gir.py | 1 + src/libnm-core-impl/nm-utils.c | 255 ++++++++++++++++++ src/libnm-core-public/nm-utils.h | 3 + src/nm-helpers/README.md | 8 + src/nm-helpers/meson.build | 20 ++ src/nm-helpers/nm-libnm-helper.c | 45 ++++ 11 files changed, 378 insertions(+), 6 deletions(-) create mode 100644 src/libnm-client-impl/tests/test-copy-cert-as-user.c create mode 100644 src/nm-helpers/nm-libnm-helper.c diff --git a/NEWS b/NEWS index 211f0fac01..141a78d754 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,8 @@ Overview of changes since NetworkManager-1.52.1 * For private connections (the ones that specify a user in the "connection.permissions" property), verify that the user can access 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 diff --git a/contrib/fedora/rpm/NetworkManager.spec b/contrib/fedora/rpm/NetworkManager.spec index 22a1a49cf8..296a550b5a 100644 --- a/contrib/fedora/rpm/NetworkManager.spec +++ b/contrib/fedora/rpm/NetworkManager.spec @@ -890,6 +890,7 @@ fi %{_libexecdir}/nm-dispatcher %{_libexecdir}/nm-initrd-generator %{_libexecdir}/nm-daemon-helper +%{_libexecdir}/nm-libnm-helper %{_libexecdir}/nm-priv-helper %dir %{_libdir}/%{name} %dir %{nmplugindir} diff --git a/src/libnm-client-impl/libnm.ver b/src/libnm-client-impl/libnm.ver index d65a01a6ca..189920a4b6 100644 --- a/src/libnm-client-impl/libnm.ver +++ b/src/libnm-client-impl/libnm.ver @@ -2051,5 +2051,6 @@ global: libnm_1_52_2 { global: + nm_utils_copy_cert_as_user; nm_vpn_plugin_info_supports_safe_private_file_access; } libnm_1_52_0; diff --git a/src/libnm-client-impl/tests/meson.build b/src/libnm-client-impl/tests/meson.build index 42e9883e77..500504db6e 100644 --- a/src/libnm-client-impl/tests/meson.build +++ b/src/libnm-client-impl/tests/meson.build @@ -5,6 +5,7 @@ test_units = [ 'test-nm-client', 'test-remote-settings-client', 'test-secret-agent', + 'test-copy-cert-as-user' ] foreach test_unit: test_units @@ -37,12 +38,15 @@ foreach test_unit: test_units ], ) - test( - 'src/libnm-client-impl/tests/' + test_unit, - test_script, - timeout: 90, - args: test_args + [exe.full_path()], - ) + # test-copy-cert-as-user is a manual test, don't run it automatically + if test_unit != 'test-copy-cert-as-user' + test( + 'src/libnm-client-impl/tests/' + test_unit, + test_script, + timeout: 90, + args: test_args + [exe.full_path()], + ) + endif endforeach if enable_introspection diff --git a/src/libnm-client-impl/tests/test-copy-cert-as-user.c b/src/libnm-client-impl/tests/test-copy-cert-as-user.c new file mode 100644 index 0000000000..b2ef9de67d --- /dev/null +++ b/src/libnm-client-impl/tests/test-copy-cert-as-user.c @@ -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 \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; +} diff --git a/src/libnm-client-impl/tests/test-gir.py b/src/libnm-client-impl/tests/test-gir.py index af36c7ac31..81518e37c6 100755 --- a/src/libnm-client-impl/tests/test-gir.py +++ b/src/libnm-client-impl/tests/test-gir.py @@ -93,6 +93,7 @@ def syms_from_ver(verfile): # hardcode it. c_syms["nm_ethtool_optname_is_feature"] = "1.20" 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" return c_syms diff --git a/src/libnm-core-impl/nm-utils.c b/src/libnm-core-impl/nm-utils.c index 6528b5fbfa..6069c7eb5b 100644 --- a/src/libnm-core-impl/nm-utils.c +++ b/src/libnm-core-impl/nm-utils.c @@ -17,6 +17,7 @@ #include #include +#include "libnm-glib-aux/nm-io-utils.h" #include "libnm-glib-aux/nm-uuid.h" #include "libnm-glib-aux/nm-json-aux.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++) 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); +} diff --git a/src/libnm-core-public/nm-utils.h b/src/libnm-core-public/nm-utils.h index 35ef580db7..98eef7db63 100644 --- a/src/libnm-core-public/nm-utils.h +++ b/src/libnm-core-public/nm-utils.h @@ -261,6 +261,9 @@ nm_utils_base64secret_decode(const char *base64_key, gsize required_key_len, gui NM_AVAILABLE_IN_1_42 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 #endif /* __NM_UTILS_H__ */ diff --git a/src/nm-helpers/README.md b/src/nm-helpers/README.md index ab0ea02444..66a9429221 100644 --- a/src/nm-helpers/README.md +++ b/src/nm-helpers/README.md @@ -17,6 +17,14 @@ all the threads of the process). 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 -------------- diff --git a/src/nm-helpers/meson.build b/src/nm-helpers/meson.build index 5f330cbc94..7c148079d2 100644 --- a/src/nm-helpers/meson.build +++ b/src/nm-helpers/meson.build @@ -18,6 +18,26 @@ executable( 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 configure_file( diff --git a/src/nm-helpers/nm-libnm-helper.c b/src/nm-helpers/nm-libnm-helper.c new file mode 100644 index 0000000000..bd0ba67d94 --- /dev/null +++ b/src/nm-helpers/nm-libnm-helper.c @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-std-aux/nm-default-std.h" + +#include + +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; +} \ No newline at end of file