diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 035872d..18b5cb0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,9 +54,7 @@ jobs: - name: Build NetworkManager-ssh run: | - if [ ! -f configure ]; then - autoreconf -vfi - fi + autoreconf -vfi ./configure --disable-static \ $(if [ -d gtk4 ]; then echo "--with-gtk4"; fi) \ --enable-more-warnings=yes \ diff --git a/Makefile.am b/Makefile.am index bf499f6..ad71bc4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -160,7 +160,8 @@ libexec_PROGRAMS += nm-ssh-service nm_ssh_service_SOURCES = \ src/nm-ssh-service.c \ src/nm-ssh-service.h \ - shared/nm-service-defines.h + shared/nm-service-defines.h \ + shared/nm-utils/nm-shared-utils.c nm_ssh_service_LDADD = \ $(LIBNM_LIBS) $(GIO_LIBS) diff --git a/configure.ac b/configure.ac index 83685ba..0f2fd6c 100644 --- a/configure.ac +++ b/configure.ac @@ -103,6 +103,16 @@ 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" +saved_LIBS="$LIBS" +saved_CFLAGS="$CFLAGS" +LIBS="$LIBNM_LIBS $LIBS" +CFLAGS="$LIBNM_CFLAGS $CFLAGS" +AC_CHECK_FUNC([nm_utils_copy_cert_as_user], + [AC_DEFINE([HAVE_NM_UTILS_COPY_CERT_AS_USER], [1], + [Define if nm_utils_copy_cert_as_user is available in libnm])]) +LIBS="$saved_LIBS" +CFLAGS="$saved_CFLAGS" + NM_VPN_SERVICE_DIR=`$PKG_CONFIG --define-variable prefix='\${prefix}' --variable vpnservicedir libnm` AC_SUBST(NM_VPN_SERVICE_DIR) diff --git a/nm-ssh-service.name.in b/nm-ssh-service.name.in index 6ed7600..b6afb27 100644 --- a/nm-ssh-service.name.in +++ b/nm-ssh-service.name.in @@ -3,6 +3,7 @@ name=ssh service=org.freedesktop.NetworkManager.ssh program=@LIBEXECDIR@/nm-ssh-service supports-multiple-connections=true +supports-safe-private-file-access=true [libnm] plugin=@PLUGINDIR@/libnm-vpn-plugin-ssh.so diff --git a/shared/nm-utils/nm-shared-utils.c b/shared/nm-utils/nm-shared-utils.c index 38bb818..eb2fdec 100644 --- a/shared/nm-utils/nm-shared-utils.c +++ b/shared/nm-utils/nm-shared-utils.c @@ -24,6 +24,12 @@ #include "nm-shared-utils.h" #include +#include +#include +#include +#include +#include +#include /*****************************************************************************/ @@ -316,3 +322,263 @@ nm_g_object_set_property (GObject *object, } /*****************************************************************************/ + +static void +_str_append_escape (GString *s, char ch) +{ + g_string_append_c (s, '\\'); + g_string_append_c (s, '0' + ((((guchar) ch) >> 6) & 07)); + g_string_append_c (s, '0' + ((((guchar) ch) >> 3) & 07)); + g_string_append_c (s, '0' + ( ((guchar) ch) & 07)); +} + +/** + * nm_utils_str_utf8safe_escape: + * @str: NUL terminated input string, possibly in utf-8 encoding + * @flags: #NMUtilsStrUtf8SafeFlags flags + * @to_free: (out): return the pointer location of the string + * if a copying was necessary. + * + * Returns the possible non-UTF-8 NUL terminated string @str + * and uses backslash escaping (C escaping, like g_strescape()) + * to sanitize non UTF-8 characters. The result is valid + * UTF-8. + * + * The operation can be reverted with g_strcompress() or + * nm_utils_str_utf8safe_unescape(). + * + * Depending on @flags, valid UTF-8 characters are not escaped at all + * (except the escape character '\\'). This is the difference to g_strescape(), + * which escapes all non-ASCII characters. This allows to pass on + * valid UTF-8 characters as-is and can be directly shown to the user + * as UTF-8 -- with exception of the backslash escape character, + * invalid UTF-8 sequences, and other (depending on @flags). + * + * Returns: the escaped input string, as valid UTF-8. If no escaping + * is necessary, it returns the input @str. Otherwise, an allocated + * string @to_free is returned which must be freed by the caller + * with g_free. The escaping can be reverted by g_strcompress(). + **/ +const char * +nm_utils_str_utf8safe_escape (const char *str, NMUtilsStrUtf8SafeFlags flags, char **to_free) +{ + const char *p = NULL; + GString *s; + + g_return_val_if_fail (to_free, NULL); + + *to_free = NULL; + if (!str || !str[0]) + return str; + + if ( g_utf8_validate (str, -1, &p) + && !NM_STRCHAR_ANY (str, ch, + ( ch == '\\' \ + || ( NM_FLAGS_HAS (flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL) \ + && ch < ' ') \ + || ( NM_FLAGS_HAS (flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII) \ + && ((guchar) ch) >= 127)))) + return str; + + s = g_string_sized_new ((p - str) + strlen (p) + 5); + + do { + for (; str < p; str++) { + char ch = str[0]; + + if (ch == '\\') + g_string_append (s, "\\\\"); + else if ( ( NM_FLAGS_HAS (flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL) \ + && ch < ' ') \ + || ( NM_FLAGS_HAS (flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII) \ + && ((guchar) ch) >= 127)) + _str_append_escape (s, ch); + else + g_string_append_c (s, ch); + } + + if (p[0] == '\0') + break; + _str_append_escape (s, p[0]); + + str = &p[1]; + g_utf8_validate (str, -1, &p); + } while (TRUE); + + *to_free = g_string_free (s, FALSE); + return *to_free; +} + +const char * +nm_utils_str_utf8safe_unescape (const char *str, char **to_free) +{ + g_return_val_if_fail (to_free, NULL); + + if (!str || !strchr (str, '\\')) { + *to_free = NULL; + return str; + } + return (*to_free = g_strcompress (str)); +} + +/** + * nm_utils_str_utf8safe_escape_cp: + * @str: NUL terminated input string, possibly in utf-8 encoding + * @flags: #NMUtilsStrUtf8SafeFlags flags + * + * Like nm_utils_str_utf8safe_escape(), except the returned value + * is always a copy of the input and must be freed by the caller. + * + * Returns: the escaped input string in UTF-8 encoding. The returned + * value should be freed with g_free(). + * The escaping can be reverted by g_strcompress(). + **/ +char * +nm_utils_str_utf8safe_escape_cp (const char *str, NMUtilsStrUtf8SafeFlags flags) +{ + char *s; + + nm_utils_str_utf8safe_escape (str, flags, &s); + return s ?: g_strdup (str); +} + +char * +nm_utils_str_utf8safe_unescape_cp (const char *str) +{ + return str ? g_strcompress (str) : NULL; +} + +char * +nm_utils_str_utf8safe_escape_take (char *str, NMUtilsStrUtf8SafeFlags flags) +{ + char *str_to_free; + + nm_utils_str_utf8safe_escape (str, flags, &str_to_free); + if (str_to_free) { + g_free (str); + return str_to_free; + } + return str; +} + +/*****************************************************************************/ + +#ifndef HAVE_NM_UTILS_COPY_CERT_AS_USER +/* + * Fallback implementation of nm_utils_copy_cert_as_user() for systems + * with NetworkManager < 1.52.2 (e.g. EL9, older Ubuntu LTS). + * + * Forks a child that drops privileges to @user, reads @filename, and + * pipes the content back to the parent, which writes it to a root-owned + * temporary file under /run/NetworkManager/cert/ (mode 0600). + */ +char * +nm_utils_copy_cert_as_user (const char *filename, const char *user, GError **error) +{ + struct passwd *pw; + int pipefd[2]; + pid_t pid; + char *tmp_path = NULL; + int tmp_fd = -1; + char buf[4096]; + ssize_t n; + gboolean write_error = FALSE; + int status; + + g_return_val_if_fail (filename, NULL); + g_return_val_if_fail (user, NULL); + g_return_val_if_fail (!error || !*error, NULL); + + pw = getpwnam (user); + if (!pw) { + g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, + "Unknown user '%s'", user); + return NULL; + } + + if (pipe (pipefd) < 0) { + g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, + "Failed to create pipe: %s", g_strerror (errno)); + return NULL; + } + + pid = fork (); + if (pid < 0) { + close (pipefd[0]); + close (pipefd[1]); + g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, + "Failed to fork: %s", g_strerror (errno)); + return NULL; + } + + if (pid == 0) { + /* Child: drop privileges to @user and stream the file to the pipe */ + int fd; + + close (pipefd[0]); + + if (setgid (pw->pw_gid) < 0 + || initgroups (pw->pw_name, pw->pw_gid) < 0 + || setuid (pw->pw_uid) < 0) { + close (pipefd[1]); + _exit (1); + } + + fd = open (filename, O_RDONLY); + if (fd < 0) { + close (pipefd[1]); + _exit (1); + } + + while ((n = read (fd, buf, sizeof (buf))) > 0) { + if (write (pipefd[1], buf, n) != n) { + close (fd); + close (pipefd[1]); + _exit (1); + } + } + close (fd); + close (pipefd[1]); + _exit (0); + } + + /* Parent: write pipe output to a root-only temp file */ + close (pipefd[1]); + + g_mkdir_with_parents ("/run/NetworkManager/cert", 0700); + + tmp_path = g_strdup ("/run/NetworkManager/cert/nm-ssh-XXXXXX"); + tmp_fd = mkstemp (tmp_path); + if (tmp_fd < 0) { + g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, + "Failed to create temporary file: %s", g_strerror (errno)); + g_free (tmp_path); + close (pipefd[0]); + waitpid (pid, NULL, 0); + return NULL; + } + + fchmod (tmp_fd, 0600); + + while ((n = read (pipefd[0], buf, sizeof (buf))) > 0) { + if (write (tmp_fd, buf, n) != n) { + write_error = TRUE; + break; + } + } + + close (tmp_fd); + close (pipefd[0]); + waitpid (pid, &status, 0); + + if (write_error || !WIFEXITED (status) || WEXITSTATUS (status) != 0) { + unlink (tmp_path); + g_free (tmp_path); + g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, + "Failed to read '%s' as user '%s'", filename, user); + return NULL; + } + + return tmp_path; +} +#endif /* HAVE_NM_UTILS_COPY_CERT_AS_USER */ diff --git a/shared/nm-utils/nm-shared-utils.h b/shared/nm-utils/nm-shared-utils.h index 5d8a3a8..c6c2d94 100644 --- a/shared/nm-utils/nm-shared-utils.h +++ b/shared/nm-utils/nm-shared-utils.h @@ -82,4 +82,25 @@ gboolean nm_g_object_set_property (GObject *object, /*****************************************************************************/ +typedef enum { + NM_UTILS_STR_UTF8_SAFE_FLAG_NONE = 0, + NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL = 0x0001, + NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII = 0x0002, +} NMUtilsStrUtf8SafeFlags; + +const char *nm_utils_str_utf8safe_escape (const char *str, NMUtilsStrUtf8SafeFlags flags, char **to_free); +const char *nm_utils_str_utf8safe_unescape (const char *str, char **to_free); + +char *nm_utils_str_utf8safe_escape_cp (const char *str, NMUtilsStrUtf8SafeFlags flags); +char *nm_utils_str_utf8safe_unescape_cp (const char *str); + +char *nm_utils_str_utf8safe_escape_take (char *str, NMUtilsStrUtf8SafeFlags flags); + + +/*****************************************************************************/ + +#ifndef HAVE_NM_UTILS_COPY_CERT_AS_USER +char *nm_utils_copy_cert_as_user (const char *filename, const char *user, GError **error); +#endif + #endif /* __NM_SHARED_UTILS_H__ */ diff --git a/src/nm-ssh-service.c b/src/nm-ssh-service.c index 694fb61..61f0b9d 100644 --- a/src/nm-ssh-service.c +++ b/src/nm-ssh-service.c @@ -49,6 +49,8 @@ #include "nm-ssh-service.h" #include "nm-utils.h" +#include "nm-utils/nm-macros-internal.h" +#include "nm-utils/nm-shared-utils.h" #if !defined(DIST_VERSION) # define DIST_VERSION VERSION @@ -56,6 +58,7 @@ static gboolean debug = FALSE; static GMainLoop *loop = NULL; +static GPtrArray *tmp_file_paths; G_DEFINE_TYPE (NMSshPlugin, nm_ssh_plugin, NM_TYPE_VPN_SERVICE_PLUGIN) @@ -825,6 +828,51 @@ get_ssh_arg_int (const char *arg, long int *retval) return TRUE; } +/* ++ * 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 to a temporary file readable only by root. If ++ * requested, the file name is unescaped. ++ * ++ * The returned file name must be freed by the caller. ++ */ +static char * +access_file (const char *filename, + const char *user, + gboolean do_unescape, + GError **error) + { + char *tmp = NULL; + char *to_free = NULL; + + g_return_val_if_fail (filename, NULL); + g_return_val_if_fail (!error || !*error, NULL); + + if (!user) { + if (do_unescape) { + return nm_utils_str_utf8safe_unescape_cp (filename); + } else + return g_strdup (filename); + } + + /* Private connection */ + + if (do_unescape) { + filename = nm_utils_str_utf8safe_unescape (filename, &to_free); + } + + tmp = nm_utils_copy_cert_as_user (filename, user, error); + g_free (to_free); + if (!tmp) + return NULL; + + g_ptr_array_add (tmp_file_paths, g_strdup (tmp)); + + return tmp; +} + static char* get_known_hosts_file(const char *username, const char* ssh_agent_socket) @@ -857,6 +905,35 @@ get_known_hosts_file(const char *username, return ssh_known_hosts; } +static const char * +get_connection_permission_user (NMConnection *connection) +{ + NMSettingConnection *s_con; + const char *ptype; + const char *pitem; + const char *pdetail; + guint num_perms; + guint i; + + s_con = nm_connection_get_setting_connection (connection); + if (!s_con) + return NULL; + + num_perms = nm_setting_connection_get_num_permissions (s_con); + if (num_perms == 0) + return NULL; + + for (i = 0; i < num_perms; i++) { + if (!nm_setting_connection_get_permission (s_con, i, &ptype, &pitem, &pdetail)) + continue; + + if (nm_streq0 (ptype, "user")) + return pitem; + } + + return NULL; +} + static gboolean nm_ssh_start_ssh_binary (NMSshPlugin *plugin, NMSettingVpn *s_vpn, @@ -870,6 +947,9 @@ nm_ssh_start_ssh_binary (NMSshPlugin *plugin, const char *ssh_binary = NULL, *sshpass_binary = NULL, *tmp = NULL; const char *remote = NULL, *port = NULL, *mtu = NULL, *ssh_agent_socket = NULL, *auth_type = NULL; char *known_hosts_file = NULL; + char *known_hosts_file_tmp = NULL; + const char *ssh_private_key = NULL; + char *ssh_private_key_tmp = NULL; char *tmp_arg = NULL; char *ip_addr_cmd_4 = NULL, *ip_addr_cmd_6 = NULL, *ip_link_cmd = NULL; char *envp[16]; @@ -987,11 +1067,17 @@ nm_ssh_start_ssh_binary (NMSshPlugin *plugin, /* Passing a id_dsa/id_rsa key as an argument to ssh */ if (!strncmp (auth_type, NM_SSH_AUTH_TYPE_KEY, strlen(NM_SSH_AUTH_TYPE_KEY))) { - tmp = nm_setting_vpn_get_data_item (s_vpn, NM_SSH_KEY_KEY_FILE); - if (tmp && strlen (tmp)) { + ssh_private_key = nm_setting_vpn_get_data_item (s_vpn, NM_SSH_KEY_KEY_FILE); + + // Add private key in a safe manner + ssh_private_key_tmp = access_file (ssh_private_key, default_username, TRUE, error); + + if (ssh_private_key_tmp && strlen (ssh_private_key_tmp)) { /* Specify key file */ add_ssh_arg (args, "-i"); - add_ssh_arg (args, tmp); + add_ssh_arg (args, ssh_private_key_tmp); + g_free (ssh_private_key_tmp); + ssh_private_key_tmp = NULL; } else { /* No key specified? Exit! */ g_set_error (error, @@ -1047,14 +1133,22 @@ nm_ssh_start_ssh_binary (NMSshPlugin *plugin, * So we'll probe the user owning SSH_AUTH_SOCK and then use * -o UserKnownHostsFile=$HOME/.ssh/known_hosts */ known_hosts_file = get_known_hosts_file(default_username, ssh_agent_socket); + if (!(known_hosts_file && strlen (known_hosts_file))) { g_warning("Using root's .ssh/known_hosts"); } else { + // Verify access to known_hosts_file and add safely + known_hosts_file_tmp = access_file (known_hosts_file, default_username, TRUE, error); + if (!(known_hosts_file_tmp && strlen (known_hosts_file_tmp))) + return FALSE; + if (debug) - g_message("Using known_hosts at: '%s'", known_hosts_file); + g_message("Using known_hosts at: '%s' (%s)", known_hosts_file_tmp, known_hosts_file); + add_ssh_arg (args, "-o"); - add_ssh_arg (args, g_strdup_printf("UserKnownHostsFile=%s", known_hosts_file) ); + add_ssh_arg (args, g_strdup_printf("UserKnownHostsFile=%s", known_hosts_file_tmp) ); g_free(known_hosts_file); + g_free(known_hosts_file_tmp); } add_ssh_arg (args, "-o"); @@ -1522,7 +1616,7 @@ real_connect (NMVpnServicePlugin *plugin, return FALSE; } - user_name = nm_setting_vpn_get_user_name (s_vpn); + user_name = get_connection_permission_user (connection); /* Validate the properties */ if (!nm_ssh_properties_validate (s_vpn, error)) @@ -1726,6 +1820,7 @@ main (int argc, char *argv[]) gboolean persist = FALSE; GOptionContext *opt_ctx = NULL; gchar *bus_name = NM_DBUS_SERVICE_SSH; + guint i; GOptionEntry options[] = { { "persist", 0, 0, G_OPTION_ARG_NONE, &persist, N_("Don't quit when VPN connection terminates"), NULL }, @@ -1774,6 +1869,8 @@ main (int argc, char *argv[]) if (!plugin) exit (EXIT_FAILURE); + tmp_file_paths = g_ptr_array_new_with_free_func(g_free); + loop = g_main_loop_new (NULL, FALSE); if (!persist) @@ -1782,6 +1879,11 @@ main (int argc, char *argv[]) setup_signals (); g_main_loop_run (loop); + for (i = 0; i < tmp_file_paths->len; i++) { + unlink ((const char *) tmp_file_paths->pdata[i]); + } + g_ptr_array_unref (tmp_file_paths); + g_main_loop_unref (loop); g_object_unref (plugin);