From 490aed584944b684026a3fd01f8d81f2881e38d6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 17 Sep 2010 01:26:29 +0200 Subject: [PATCH] ask-password: add minimal framework to allow services query SSL/harddisk passphrases from the user --- .gitignore | 3 + Makefile.am | 49 +++- configure.ac | 2 +- src/.gitignore | 1 + src/ask-password-agent.vala | 250 ++++++++++++++++++++ src/ask-password.c | 352 ++++++++++++++++++++++++++++ src/main.c | 2 + src/org.freedesktop.systemd1.policy | 30 +++ src/reply-password.c | 108 +++++++++ src/systemadm.vala | 2 +- 10 files changed, 794 insertions(+), 5 deletions(-) create mode 100644 src/ask-password-agent.vala create mode 100644 src/ask-password.c create mode 100644 src/org.freedesktop.systemd1.policy create mode 100644 src/reply-password.c diff --git a/.gitignore b/.gitignore index 9e2fb86b..7cecf1c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +systemd-reply-password +systemd-ask-password-agent +systemd-ask-password systemd-kmsg-syslogd systemd-remount-api-vfs test-hostname diff --git a/Makefile.am b/Makefile.am index 7632e553..5f11e4fb 100644 --- a/Makefile.am +++ b/Makefile.am @@ -25,6 +25,7 @@ dbusinterfacedir=@dbusinterfacedir@ udevrulesdir=@udevrulesdir@ pamlibdir=@pamlibdir@ pkgconfigdatadir=$(datadir)/pkgconfig +polkitpolicydir=$(datadir)/polkit-1/actions # Our own, non-special dirs pkgsysconfdir=$(sysconfdir)/systemd @@ -56,14 +57,16 @@ AM_CPPFLAGS = \ rootbin_PROGRAMS = \ systemd \ systemctl \ - systemd-notify + systemd-notify \ + systemd-ask-password bin_PROGRAMS = \ systemd-cgls if HAVE_GTK bin_PROGRAMS += \ - systemadm + systemadm \ + systemd-ask-password-agent endif rootlibexec_PROGRAMS = \ @@ -76,7 +79,8 @@ rootlibexec_PROGRAMS = \ systemd-modules-load \ systemd-remount-api-vfs \ systemd-kmsg-syslogd \ - systemd-vconsole-setup + systemd-vconsole-setup \ + systemd-reply-password noinst_PROGRAMS = \ test-engine \ @@ -282,6 +286,9 @@ dist_doc_DATA = \ pkgconfigdata_DATA = \ systemd.pc +dist_polkitpolicy_DATA = \ + src/org.freedesktop.systemd1.policy + noinst_LTLIBRARIES = \ libsystemd-basic.la \ libsystemd-core.la @@ -665,6 +672,18 @@ systemd_notify_SOURCES = \ systemd_notify_LDADD = \ libsystemd-basic.la +systemd_ask_password_SOURCES = \ + src/ask-password.c + +systemd_ask_password_LDADD = \ + libsystemd-basic.la + +systemd_reply_password_SOURCES = \ + src/reply-password.c + +systemd_reply_password_LDADD = \ + libsystemd-basic.la + systemd_cgls_SOURCES = \ src/cgls.c \ src/cgroup-show.c \ @@ -699,6 +718,30 @@ systemadm_LDADD = \ $(DBUSGLIB_LIBS) \ $(GTK_LIBS) +systemd_ask_password_agent_SOURCES = \ + src/ask-password-agent.vala + +systemd_ask_password_agent_CFLAGS = \ + $(AM_CFLAGS) \ + $(DBUSGLIB_CFLAGS) \ + $(GTK_CFLAGS) \ + -Wno-unused-variable \ + -Wno-unused-function \ + -Wno-shadow \ + -Wno-format-nonliteral + +systemd_ask_password_agent_VALAFLAGS = \ + --pkg=dbus-glib-1 \ + --pkg=posix \ + --pkg=gtk+-2.0 \ + --pkg=linux \ + --pkg=gio-unix-2.0 \ + -g + +systemd_ask_password_agent_LDADD = \ + $(DBUSGLIB_LIBS) \ + $(GTK_LIBS) + pam_systemd_la_SOURCES = \ src/pam-module.c \ src/cgroup-util.c \ diff --git a/configure.ac b/configure.ac index 2ff0c1ff..334b1e26 100644 --- a/configure.ac +++ b/configure.ac @@ -226,7 +226,7 @@ AC_SUBST(AUDIT_LIBS) have_gtk=no AC_ARG_ENABLE(gtk, AS_HELP_STRING([--disable-gtk], [disable GTK tools])) if test "x$enable_gtk" != "xno"; then - PKG_CHECK_MODULES(GTK, [ gtk+-2.0 ], + PKG_CHECK_MODULES(GTK, [ gtk+-2.0 gio-unix-2.0 ], [AC_DEFINE(HAVE_GTK, 1, [Define if GTK is available]) have_gtk=yes], have_gtk=no) AC_SUBST(GTK_CFLAGS) AC_SUBST(GTK_LIBS) diff --git a/src/.gitignore b/src/.gitignore index 1841bf59..389f26da 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1,2 +1,3 @@ +ask-password-agent.c systemd-interfaces.c systemadm.c diff --git a/src/ask-password-agent.vala b/src/ask-password-agent.vala new file mode 100644 index 00000000..5355bb46 --- /dev/null +++ b/src/ask-password-agent.vala @@ -0,0 +1,250 @@ +/*** + This file is part of systemd. + + Copyright 2010 Lennart Poettering + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with systemd; If not, see . +***/ + +using Gtk; +using GLib; +using DBus; +using Linux; +using Posix; + +[CCode (cheader_filename = "time.h")] +extern int clock_gettime(int id, out timespec ts); + +public class PasswordDialog : Dialog { + + public Entry entry; + + public PasswordDialog(string message, string icon) { + set_title("System Password"); + set_has_separator(false); + set_border_width(8); + set_default_response(ResponseType.OK); + set_icon_name(icon); + + add_button(STOCK_CANCEL, ResponseType.CANCEL); + add_button(STOCK_OK, ResponseType.OK); + + Container content = (Container) get_content_area(); + + Box hbox = new HBox(false, 16); + hbox.set_border_width(8); + content.add(hbox); + + Image image = new Image.from_icon_name(icon, IconSize.DIALOG); + hbox.pack_start(image, false, false); + + Box vbox = new VBox(false, 8); + hbox.pack_start(vbox, true, true); + + Label label = new Label(message); + vbox.pack_start(label, false, false); + + entry = new Entry(); + entry.set_visibility(false); + entry.set_activates_default(true); + vbox.pack_start(entry, false, false); + + entry.activate.connect(on_entry_activated); + + show_all(); + } + + public void on_entry_activated() { + response(ResponseType.OK); + } +} + +public class MyStatusIcon : StatusIcon { + + File directory; + File current; + FileMonitor file_monitor; + + string message; + string icon; + string socket; + + PasswordDialog password_dialog; + + public MyStatusIcon() throws GLib.Error { + GLib.Object(icon_name : "dialog-password"); + set_title("System Password Agent"); + + directory = File.new_for_path("/dev/.systemd/ask-password/"); + file_monitor = directory.monitor_directory(0); + file_monitor.changed.connect(file_monitor_changed); + + current = null; + look_for_password(); + + activate.connect(status_icon_activate); + } + + void file_monitor_changed(GLib.File file, GLib.File? other_file, GLib.FileMonitorEvent event_type) throws GLib.Error { + + if (!file.get_basename().has_prefix("ask.")) + return; + + if (event_type == FileMonitorEvent.CREATED || + event_type == FileMonitorEvent.DELETED) + look_for_password(); + } + + void look_for_password() throws GLib.Error { + + if (current != null) { + if (!current.query_exists()) { + current = null; + if (password_dialog != null) + password_dialog.response(ResponseType.REJECT); + } + } + + if (current == null) { + FileEnumerator enumerator = directory.enumerate_children("standard::name", FileQueryInfoFlags.NOFOLLOW_SYMLINKS); + + FileInfo i; + while ((i = enumerator.next_file()) != null) { + if (!i.get_name().has_prefix("ask.")) + continue; + + current = directory.get_child(i.get_name()); + + if (load_password()) + break; + + current = null; + } + } + + if (current == null) + set_visible(false); + + } + + bool load_password() { + + KeyFile key_file = new KeyFile(); + + try { + timespec ts; + + key_file.load_from_file(current.get_path(), KeyFileFlags.NONE); + + string not_after_as_string = key_file.get_string("Ask", "NotAfter"); + + clock_gettime(1, out ts); + uint64 now = (ts.tv_sec * 1000000) + (ts.tv_nsec / 1000); + + uint64 not_after; + if (not_after_as_string.scanf("%llu", out not_after) != 1) + return false; + + if (not_after < now) + return false; + + socket = key_file.get_string("Ask", "Socket"); + } catch (GLib.Error e) { + return false; + } + + try { + message = key_file.get_string("Ask", "Message").compress(); + } catch (GLib.Error e) { + message = "Please Enter System Password!"; + } + set_tooltip_text(message); + + try { + icon = key_file.get_string("Ask", "Icon"); + } catch (GLib.Error e) { + icon = "dialog-password"; + } + set_from_icon_name(icon); + + set_visible(true); + return true; + } + + void status_icon_activate() throws GLib.Error { + + if (current == null) + return; + + if (password_dialog != null) { + password_dialog.present(); + return; + } + + password_dialog = new PasswordDialog(message, icon); + + int result = password_dialog.run(); + string password = password_dialog.entry.get_text(); + + password_dialog.destroy(); + password_dialog = null; + + if (result == ResponseType.REJECT || + result == ResponseType.DELETE_EVENT) + return; + + int to_process; + + Process.spawn_async_with_pipes( + null, + { "/usr/bin/pkexec", "/lib/systemd/systemd-reply-password", result == ResponseType.OK ? "1" : "0", socket }, + null, + 0, + null, + null, + out to_process, + null, + null); + + OutputStream stream = new UnixOutputStream(to_process, true); + + stream.write(password, password.length, null); + } +} + +static const OptionEntry entries[] = { + { null } +}; + +void show_error(string e) { + var m = new MessageDialog(null, 0, MessageType.ERROR, ButtonsType.CLOSE, "%s", e); + m.run(); + m.destroy(); +} + +int main(string[] args) { + try { + Gtk.init_with_args(ref args, "[OPTION...]", entries, "systemd-ask-password-agent"); + + MyStatusIcon i = new MyStatusIcon(); + Gtk.main(); + + } catch (DBus.Error e) { + show_error(e.message); + } catch (GLib.Error e) { + show_error(e.message); + } + + return 0; +} diff --git a/src/ask-password.c b/src/ask-password.c new file mode 100644 index 00000000..2c9b027d --- /dev/null +++ b/src/ask-password.c @@ -0,0 +1,352 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2010 Lennart Poettering + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with systemd; If not, see . +***/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "log.h" +#include "macro.h" +#include "util.h" + +static const char *arg_icon = NULL; +static const char *arg_message = NULL; +static usec_t arg_timeout = 60 * USEC_PER_SEC; + +static int create_socket(char **name) { + int fd; + union { + struct sockaddr sa; + struct sockaddr_un un; + } sa; + int one = 1, r; + char *c; + + assert(name); + + if ((fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0)) < 0) { + log_error("socket() failed: %m"); + return -errno; + } + + zero(sa); + sa.un.sun_family = AF_UNIX; + snprintf(sa.un.sun_path+1, sizeof(sa.un.sun_path)-1, "/org/freedesktop/systemd1/ask-password/%llu", random_ull()); + + if (bind(fd, &sa.sa, sizeof(sa_family_t) + 1 + strlen(sa.un.sun_path+1)) < 0) { + r = -errno; + log_error("bind() failed: %m"); + goto fail; + } + + if (setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &one, sizeof(one)) < 0) { + r = -errno; + log_error("SO_PASSCRED failed: %m"); + goto fail; + } + + if (!(c = strdup(sa.un.sun_path+1))) { + r = -ENOMEM; + log_error("Out of memory"); + goto fail; + } + + *name = c; + return fd; + +fail: + close_nointr_nofail(fd); + + return r; +} + +static int help(void) { + + printf("%s [OPTIONS...] MESSAGE\n\n" + "Query the user for a passphrase.\n\n" + " -h --help Show this help\n" + " --icon=NAME Icon name\n" + " --timeout=USEC Timeout in usec\n", + program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_ICON = 0x100, + ARG_TIMEOUT + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "icon", required_argument, NULL, ARG_ICON }, + { "timeout", required_argument, NULL, ARG_TIMEOUT }, + { NULL, 0, NULL, 0 } + }; + + int c; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) { + + switch (c) { + + case 'h': + help(); + return 0; + + case ARG_ICON: + arg_icon = optarg; + break; + + case ARG_TIMEOUT: + if (parse_usec(optarg, &arg_timeout) < 0 || arg_timeout <= 0) { + log_error("Failed to parse --timeout parameter %s", optarg); + return -EINVAL; + } + break; + + case '?': + return -EINVAL; + + default: + log_error("Unknown option code %c", c); + return -EINVAL; + } + } + + if (optind != argc-1) { + help(); + return -EINVAL; + } + + arg_message = argv[optind]; + return 0; +} + +int main(int argc, char *argv[]) { + char temp[] = "/dev/.systemd/ask-password/tmp.XXXXXX"; + char final[sizeof(temp)] = ""; + int fd = -1, r = EXIT_FAILURE, k; + FILE *f = NULL; + char *socket_name = NULL; + int socket_fd, signal_fd; + sigset_t mask; + usec_t not_after; + + log_parse_environment(); + log_open(); + + if ((k = parse_argv(argc, argv)) < 0) { + r = k < 0 ? EXIT_FAILURE : EXIT_SUCCESS; + goto finish; + } + + if ((fd = mkostemp(temp, O_CLOEXEC|O_CREAT|O_WRONLY)) < 0) { + log_error("Failed to create password file: %m"); + goto finish; + } + + fchmod(fd, 0644); + + if (!(f = fdopen(fd, "w"))) { + log_error("Failed to allocate FILE: %m"); + goto finish; + } + + fd = -1; + + assert_se(sigemptyset(&mask) == 0); + sigset_add_many(&mask, SIGINT, SIGTERM, -1); + assert_se(sigprocmask(SIG_SETMASK, &mask, NULL) == 0); + + if ((signal_fd = signalfd(-1, &mask, SFD_NONBLOCK|SFD_CLOEXEC)) < 0) { + log_error("signalfd(): %m"); + goto finish; + } + + if ((socket_fd = create_socket(&socket_name)) < 0) + goto finish; + + not_after = now(CLOCK_MONOTONIC) + arg_timeout; + + fprintf(f, + "[Ask]\n" + "Socket=%s\n" + "NotAfter=%llu\n", + socket_name, + (unsigned long long) not_after); + + if (arg_message) + fprintf(f, "Message=%s\n", arg_message); + + if (arg_icon) + fprintf(f, "Icon=%s\n", arg_icon); + + fflush(f); + + if (ferror(f)) { + log_error("Failed to write query file: %m"); + goto finish; + } + + memcpy(final, temp, sizeof(temp)); + + final[sizeof(final)-11] = 'a'; + final[sizeof(final)-10] = 's'; + final[sizeof(final)-9] = 'k'; + + if (rename(temp, final) < 0) { + log_error("Failed to rename query file: %m"); + goto finish; + } + + for (;;) { + enum { + FD_SOCKET, + FD_SIGNAL, + _FD_MAX + }; + + char passphrase[LINE_MAX+1]; + struct msghdr msghdr; + struct iovec iovec; + struct ucred *ucred; + union { + struct cmsghdr cmsghdr; + uint8_t buf[CMSG_SPACE(sizeof(struct ucred))]; + } control; + ssize_t n; + struct pollfd pollfd[_FD_MAX]; + + zero(pollfd); + pollfd[FD_SOCKET].fd = socket_fd; + pollfd[FD_SOCKET].events = POLLIN; + pollfd[FD_SIGNAL].fd = signal_fd; + pollfd[FD_SIGNAL].events = POLLIN; + + if ((k = poll(pollfd, 2, arg_timeout/USEC_PER_MSEC)) < 0) { + + if (errno == EINTR) + continue; + + log_error("poll() failed: %s", strerror(-r)); + goto finish; + } + + if (k <= 0) { + log_notice("Timed out"); + goto finish; + } + + if (pollfd[FD_SIGNAL].revents & POLLIN) + break; + + if (pollfd[FD_SOCKET].revents != POLLIN) { + log_error("Unexpected poll() event."); + goto finish; + } + + zero(iovec); + iovec.iov_base = passphrase; + iovec.iov_len = sizeof(passphrase)-1; + + zero(control); + zero(msghdr); + msghdr.msg_iov = &iovec; + msghdr.msg_iovlen = 1; + msghdr.msg_control = &control; + msghdr.msg_controllen = sizeof(control); + + if ((n = recvmsg(socket_fd, &msghdr, 0)) < 0) { + + if (errno == EAGAIN || + errno == EINTR) + continue; + + log_error("recvmsg() failed: %m"); + goto finish; + } + + if (n <= 0) { + log_error("Message too short"); + continue; + } + + if (msghdr.msg_controllen < CMSG_LEN(sizeof(struct ucred)) || + control.cmsghdr.cmsg_level != SOL_SOCKET || + control.cmsghdr.cmsg_type != SCM_CREDENTIALS || + control.cmsghdr.cmsg_len != CMSG_LEN(sizeof(struct ucred))) { + log_warning("Received message without credentials. Ignoring."); + continue; + } + + ucred = (struct ucred*) CMSG_DATA(&control.cmsghdr); + if (ucred->uid != 0) { + log_warning("Got request from unprivileged user. Ignoring."); + continue; + } + + if (passphrase[0] == '+') { + passphrase[n] = 0; + fputs(passphrase+1, stdout); + } else if (passphrase[0] == '-') + goto finish; + else { + log_error("Invalid packet"); + continue; + } + + break; + } + + r = EXIT_SUCCESS; + +finish: + if (fd >= 0) + close_nointr_nofail(fd); + + if (socket_fd >= 0) + close_nointr_nofail(socket_fd); + + if (f) + fclose(f); + + unlink(temp); + + if (final[0]) + unlink(final); + + return r; +} diff --git a/src/main.c b/src/main.c index 671f2bbf..fcb6e8f9 100644 --- a/src/main.c +++ b/src/main.c @@ -1006,6 +1006,8 @@ int main(int argc, char *argv[]) { kmod_setup(); hostname_setup(); loopback_setup(); + + mkdir_p("/dev/.systemd/ask-password/", 0755); } if ((r = manager_new(arg_running_as, &m)) < 0) { diff --git a/src/org.freedesktop.systemd1.policy b/src/org.freedesktop.systemd1.policy new file mode 100644 index 00000000..bb07b827 --- /dev/null +++ b/src/org.freedesktop.systemd1.policy @@ -0,0 +1,30 @@ + + + + + + + + The systemd Project + http://www.freedesktop.org/wiki/Software/systemd + + + Send passphrase back to system + Authentication is required to send the entered passphrase back to the system. + + no + no + auth_admin_keep + + /lib/systemd/systemd-reply-password + + + diff --git a/src/reply-password.c b/src/reply-password.c new file mode 100644 index 00000000..236fdcc9 --- /dev/null +++ b/src/reply-password.c @@ -0,0 +1,108 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2010 Lennart Poettering + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with systemd; If not, see . +***/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "log.h" +#include "macro.h" +#include "util.h" + +static int send_on_socket(int fd, const char *socket_name, const void *packet, size_t size) { + union { + struct sockaddr sa; + struct sockaddr_un un; + } sa; + + assert(fd >= 0); + assert(socket_name); + assert(packet); + + zero(sa); + sa.un.sun_family = AF_UNIX; + strncpy(sa.un.sun_path+1, socket_name, sizeof(sa.un.sun_path)-1); + + if (sendto(fd, packet, size, MSG_NOSIGNAL, &sa.sa, sizeof(sa_family_t) + 1 + strlen(socket_name)) < 0) { + log_error("Failed to send: %m"); + return -1; + } + + return 0; +} + +int main(int argc, char *argv[]) { + int fd = -1, r = EXIT_FAILURE; + char packet[LINE_MAX]; + size_t length; + + log_set_target(LOG_TARGET_SYSLOG_OR_KMSG); + log_parse_environment(); + log_open(); + + if (argc != 3) { + log_error("Wrong number of arguments."); + goto finish; + } + + if (streq(argv[1], "1")) { + + packet[0] = '+'; + if (!fgets(packet+1, sizeof(packet)-1, stdin)) { + log_error("Failed to read password: %m"); + goto finish; + } + + truncate_nl(packet+1); + length = strlen(packet+1) + 1; + } else if (streq(argv[1], "0")) { + packet[0] = '-'; + length = 1; + } else { + log_error("Invalid first argument %s", argv[1]); + goto finish; + } + + if ((fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0)) < 0) { + log_error("socket() failed: %m"); + goto finish; + } + + if (send_on_socket(fd, argv[2], packet, length) < 0) + goto finish; + + r = EXIT_SUCCESS; + +finish: + if (fd >= 0) + close_nointr_nofail(fd); + + return r; +} diff --git a/src/systemadm.vala b/src/systemadm.vala index 4aee1d35..2f3aed20 100644 --- a/src/systemadm.vala +++ b/src/systemadm.vala @@ -978,7 +978,7 @@ void show_error(string e) { m.destroy(); } -int main (string[] args) { +int main(string[] args) { try { Gtk.init_with_args(ref args, "[OPTION...]", entries, "systemadm"); -- 2.39.5