From 67159523e720bee6e25b900534d9f779b48c22e2 Mon Sep 17 00:00:00 2001 From: Stef Walter Date: Mon, 24 Aug 2015 11:42:26 +0200 Subject: [PATCH 1/8] retest: Put retest C only testing code into its own directory So it can be used by multiple components --- Makefile.am | 1 + src/reauthorize/Makefile-reauthorize.am | 6 ------ src/reauthorize/test-reauthorize.c | 2 +- src/retest/Makefile.am | 5 +++++ src/{reauthorize => retest}/retest.c | 0 src/{reauthorize => retest}/retest.h | 0 6 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 src/retest/Makefile.am rename src/{reauthorize => retest}/retest.c (100%) rename src/{reauthorize => retest}/retest.h (100%) diff --git a/Makefile.am b/Makefile.am index 66c62551251..a329c5e7109 100644 --- a/Makefile.am +++ b/Makefile.am @@ -252,6 +252,7 @@ include src/ws/Makefile-ws.am include src/static/Makefile-static.am include src/remotectl/Makefile-remotectl.am include src/reauthorize/Makefile-reauthorize.am +include src/retest/Makefile.am include src/selinux/Makefile-selinux.am include branding/default/Makefile.am include branding/fedora/Makefile.am diff --git a/src/reauthorize/Makefile-reauthorize.am b/src/reauthorize/Makefile-reauthorize.am index ceb6fe2ebbc..76f57100b09 100644 --- a/src/reauthorize/Makefile-reauthorize.am +++ b/src/reauthorize/Makefile-reauthorize.am @@ -47,12 +47,6 @@ REAUTHORIZE_CHECKS = \ test-reauthorize \ $(NULL) -noinst_LIBRARIES += libretest.a -libretest_a_SOURCES = \ - src/reauthorize/retest.c \ - src/reauthorize/retest.h \ - $(NULL) - test_reauthorize_CFLAGS = $(libreauthorize_a_CFLAGS) test_reauthorize_SOURCES = src/reauthorize/test-reauthorize.c test_reauthorize_LDADD = libretest.a $(libreauthorize_a_LIBS) diff --git a/src/reauthorize/test-reauthorize.c b/src/reauthorize/test-reauthorize.c index 21dfaecb6fb..b08dd43ef91 100644 --- a/src/reauthorize/test-reauthorize.c +++ b/src/reauthorize/test-reauthorize.c @@ -34,7 +34,7 @@ #define _GNU_SOURCE -#include "retest.h" +#include "retest/retest.h" #include "reauthorize.h" diff --git a/src/retest/Makefile.am b/src/retest/Makefile.am new file mode 100644 index 00000000000..e193ffb9537 --- /dev/null +++ b/src/retest/Makefile.am @@ -0,0 +1,5 @@ +noinst_LIBRARIES += libretest.a +libretest_a_SOURCES = \ + src/retest/retest.c \ + src/retest/retest.h \ + $(NULL) diff --git a/src/reauthorize/retest.c b/src/retest/retest.c similarity index 100% rename from src/reauthorize/retest.c rename to src/retest/retest.c diff --git a/src/reauthorize/retest.h b/src/retest/retest.h similarity index 100% rename from src/reauthorize/retest.h rename to src/retest/retest.h From ccd8d0c772f5d5c35657c0e975c3d866a9d4eab0 Mon Sep 17 00:00:00 2001 From: petervo Date: Mon, 24 Aug 2015 15:43:54 -0700 Subject: [PATCH 2/8] pam-ssh-add: PAM module for loading keys via ssh-add --- Makefile.am | 1 + configure.ac | 8 +- doc/guide/Makefile-guide.am | 1 + doc/guide/authentication.xml | 34 + doc/guide/cockpit-guide.xml | 1 + doc/key-auth.md | 26 + doc/man/Makefile-man.am | 2 + doc/man/pam_ssh_add.xml | 91 ++ src/pam-ssh-add/HACKING | 16 + src/pam-ssh-add/Makefile.am | 51 ++ src/pam-ssh-add/mock-environment | 12 + src/pam-ssh-add/mock-ssh-add | 40 + src/pam-ssh-add/mock-ssh-agent | 17 + src/pam-ssh-add/pam-ssh-add.c | 1078 +++++++++++++++++++++++ src/pam-ssh-add/pam-ssh-add.h | 45 + src/pam-ssh-add/test-ssh-add.c | 433 +++++++++ src/reauthorize/Makefile-reauthorize.am | 2 +- src/selinux/cockpit.te | 1 + test/check-multi-machine-key | 196 +++++ test/check-verify | 1 + test/cockpit.pam | 2 + test/ssh/id_dsa | 15 + test/ssh/id_dsa.pub | 1 + test/ssh/id_ed25519 | 8 + test/ssh/id_ed25519.pub | 1 + test/ssh/id_rsa | 30 + test/ssh/id_rsa.pub | 1 + test/ssh/identity | 27 + test/ssh/identity.pub | 1 + tools/cockpit.spec | 2 + 30 files changed, 2140 insertions(+), 4 deletions(-) create mode 100644 doc/guide/authentication.xml create mode 100644 doc/key-auth.md create mode 100644 doc/man/pam_ssh_add.xml create mode 100644 src/pam-ssh-add/HACKING create mode 100644 src/pam-ssh-add/Makefile.am create mode 100755 src/pam-ssh-add/mock-environment create mode 100755 src/pam-ssh-add/mock-ssh-add create mode 100755 src/pam-ssh-add/mock-ssh-agent create mode 100644 src/pam-ssh-add/pam-ssh-add.c create mode 100644 src/pam-ssh-add/pam-ssh-add.h create mode 100644 src/pam-ssh-add/test-ssh-add.c create mode 100755 test/check-multi-machine-key create mode 100644 test/ssh/id_dsa create mode 100644 test/ssh/id_dsa.pub create mode 100644 test/ssh/id_ed25519 create mode 100644 test/ssh/id_ed25519.pub create mode 100644 test/ssh/id_rsa create mode 100644 test/ssh/id_rsa.pub create mode 100644 test/ssh/identity create mode 100644 test/ssh/identity.pub diff --git a/Makefile.am b/Makefile.am index a329c5e7109..39a44206745 100644 --- a/Makefile.am +++ b/Makefile.am @@ -254,6 +254,7 @@ include src/remotectl/Makefile-remotectl.am include src/reauthorize/Makefile-reauthorize.am include src/retest/Makefile.am include src/selinux/Makefile-selinux.am +include src/pam-ssh-add/Makefile.am include branding/default/Makefile.am include branding/fedora/Makefile.am include branding/centos/Makefile.am diff --git a/configure.ac b/configure.ac index 66d29d53ca1..d4fd7b93eee 100644 --- a/configure.ac +++ b/configure.ac @@ -159,9 +159,10 @@ AC_CHECK_HEADER([security/pam_appl.h], , AC_CHECK_LIB(pam, pam_open_session, [ true ], [AC_MSG_ERROR([Couldn't find PAM library. Try installing pam-devel])] ) -COCKPIT_SESSION_LIBS="$COCKPIT_SESSION_LIBS -lpam" -COCKPIT_WS_LIBS="$COCKPIT_WS_LIBS -lpam" -REAUTHORIZE_LIBS="$REAUTHORIZE_LIBS -lpam" +PAM_LIBS="-lpam" +COCKPIT_SESSION_LIBS="$COCKPIT_SESSION_LIBS $PAM_LIBS" +COCKPIT_WS_LIBS="$COCKPIT_WS_LIBS $PAM_LIBS" +REAUTHORIZE_LIBS="$REAUTHORIZE_LIBS $PAM_LIBS" # pam module directory AC_ARG_WITH([pamdir], @@ -433,6 +434,7 @@ AC_MSG_RESULT($enable_strict) # AC_SUBST(REAUTHORIZE_LIBS) +AC_SUBST(PAM_LIBS) AC_OUTPUT([ Makefile diff --git a/doc/guide/Makefile-guide.am b/doc/guide/Makefile-guide.am index 535b1ea489e..9d387e80dd1 100644 --- a/doc/guide/Makefile-guide.am +++ b/doc/guide/Makefile-guide.am @@ -6,6 +6,7 @@ GUIDE_INCLUDES = \ doc/guide/api-container.xml \ doc/guide/api-base1.xml \ doc/guide/api-server.xml \ + doc/guide/authentication.xml \ doc/guide/embedding.xml \ doc/guide/packages.xml \ doc/guide/https.xml \ diff --git a/doc/guide/authentication.xml b/doc/guide/authentication.xml new file mode 100644 index 00000000000..965bb7d2ad3 --- /dev/null +++ b/doc/guide/authentication.xml @@ -0,0 +1,34 @@ + + + + Cockpit Authentication + + Cockpit allows you to monitor and administer several servers at the same time. + While you will need to connect to the primary server with a password + or kerberos ticket, cockpit does support accessing secondary machines via + public key based authentication. + + Once a user is successfully logged in, an ssh-agent is started. + The following keys are then preloaded into the ssh-agent provided + they are supported by your ssh version, present, with the correct + permission, and either unencrypted or encrypted with the same + password that was used to login. + + + ~/.ssh/identity + ~/.ssh/id_rsa + ~/.ssh/id_dsa + ~/.ssh/id_ed25519 + ~/.ssh/id_ecdsa + + + Cockpit will provide an interface for loading other keys into the agent + that could not be automatically loaded. + + Note that when a user is authenticated in this way the authentication + happens without a password, as such the standard cockpit reauthorization + mechanisms do not work. The user will only be able to obtain additional + priviledges if they do not require a password. + + diff --git a/doc/guide/cockpit-guide.xml b/doc/guide/cockpit-guide.xml index 1975e89358f..72e91511183 100644 --- a/doc/guide/cockpit-guide.xml +++ b/doc/guide/cockpit-guide.xml @@ -24,6 +24,7 @@ + diff --git a/doc/key-auth.md b/doc/key-auth.md new file mode 100644 index 00000000000..21cab8b3464 --- /dev/null +++ b/doc/key-auth.md @@ -0,0 +1,26 @@ + +Cockpit Key Based Authentication +================================ + +Cockpit allows you to monitor and administer several servers at the same time. +While you will need to connect to the primary server with a password +or kerberos ticket, cockpit does support accessing secondary machines via +public key based authentication. + +Note that when a user is authenticated in this way the authentication happens +without a password, as such the standard cockpit reauthorization mechanisms do +not work. The user will only be able to obtain additional priviledges if they do not require a password. + +In order to support key based authentication cockpit adds pam_ssh_add.so +to it's pam stack. Once a user is successfully logged in a new ssh-agent +is started and ssh-add is run to load the default keys, if a password is +requested the one the user logged in with is provided. + +When cockpit-ws attempts to establish an ssh connection to a new server, +it will request a "stream" channel with the internal name "ssh-agent" +from the bridge. That channel proxies the running ssh-agent +allowing the new ssh connection to use it as a standard ssh agent and +offer any loaded public keys to the remote server as authentication options. + +When the user logs out or has their session terminated, the ssh-agent is also +terminated. diff --git a/doc/man/Makefile-man.am b/doc/man/Makefile-man.am index 5eda5f06e9b..b1feddc9062 100644 --- a/doc/man/Makefile-man.am +++ b/doc/man/Makefile-man.am @@ -11,12 +11,14 @@ if WITH_COCKPIT_WS man_MANS += \ doc/man/cockpit-ws.8 \ doc/man/cockpit.conf.5 \ + doc/man/pam_ssh_add.8 \ doc/man/remotectl.8 \ $(NULL) EXTRA_DIST += \ doc/man/cockpit-ws.xml \ doc/man/cockpit.conf.xml \ + doc/man/pam_ssh_add.xml \ doc/man/remotectl.xml \ $(NULL) diff --git a/doc/man/pam_ssh_add.xml b/doc/man/pam_ssh_add.xml new file mode 100644 index 00000000000..9b74b46a731 --- /dev/null +++ b/doc/man/pam_ssh_add.xml @@ -0,0 +1,91 @@ + + + + + + pam_ssh_add + pam_ssh_add + + + + pam_ssh_add + 8 + + + + pam_ssh_add + PAM module to auto load ssh keys into an agent + + + DESCRIPTION + + pam_ssh_add provides authentication and session modules that + allow users to start their session with a running ssh-agent with as + many ssh keys loaded as possible. + + + If used, the authentication module simply stores the authentication + token for later use by the session module. Because this module performs + no actual authentication it returns PAM_CRED_INSUFFICIENT on success and + should always be accompanied by an actual authentication module in your + pam configuration. + + + By default the session module will start a new ssh-agent and run + ssh-add, loading any keys that exist in the default path for the + newly logged in user. If any keys prompt for a password, and a authenication + token was successfully stored, that token will be provided as the password. + + + + + + Options + + + + + This option will turn on debug logging to syslog. + + + + + + + Examples + + + auth required pam_unix.so + auth optional pam_ssh_add.so + session optional pam_ssh_add.so + + + + + + + BUGS + + Please send bug reports to either the distribution bug tracker or the + upstream bug tracker. + + + + diff --git a/src/pam-ssh-add/HACKING b/src/pam-ssh-add/HACKING new file mode 100644 index 00000000000..b77f8afaa97 --- /dev/null +++ b/src/pam-ssh-add/HACKING @@ -0,0 +1,16 @@ + +Working on pam_ssh_add.so +------------------------- + +For now this is part of Cockpit, but may eventually be split out. So only +basic glibc and linux dependencies are allowed: + + * glibc + * pam + +Tests +----- +Right now the bulk of the code is unit tested using mock versions of ssh-add +and ssh-agent. However the pam module itself is only tested with integration +tests. Hopefully the cwrap project will get the ability to mock pam. When that +happens we should add unit tests for the actual pam module as well. diff --git a/src/pam-ssh-add/Makefile.am b/src/pam-ssh-add/Makefile.am new file mode 100644 index 00000000000..73c7046d5a0 --- /dev/null +++ b/src/pam-ssh-add/Makefile.am @@ -0,0 +1,51 @@ +noinst_LIBRARIES += libpam_ssh_add.a + +libpam_ssh_add_a_SOURCES = \ + src/pam-ssh-add/pam-ssh-add.c \ + src/pam-ssh-add/pam-ssh-add.h \ + $(NULL) + +libpam_ssh_add_a_CFLAGS = $(NULL) + +libpam_ssh_add_a_LIBS = \ + libpam_ssh_add.a \ + $(PAM_LIBS) + +pam_ssh_add_FILES = src/pam-ssh-add/pam-ssh-add.c src/pam-ssh-add/pam-ssh-add.c +pam_ssh_add.so: libpam_ssh_add.a $(pam_ssh_add_FILES) + $(AM_V_CCLD) $(CC) -fPIC -shared $(CFLAGS) $(libpam_ssh_add_a_CFLAGS) -I$(builddir) \ + -o $@ $^ $(libpam_ssh_add_a_LIBS) $(LDFLAGS) + +all-local:: pam_ssh_add.so + +EXTRA_DIST += \ + $(pam_ssh_add_FILES) \ + $(NULL) + +CLEANFILES += pam_ssh_add.so + +install-exec-local:: + $(MKDIR_P) $(DESTDIR)$(pamdir) + $(INSTALL) pam_ssh_add.so $(DESTDIR)$(pamdir) +uninstall-local:: + $(RM) -f $(DESTDIR)$(pamdir)/pam_ssh_add.so + +# ----------------------------------------------------------------------------- +# Tests + +PAM_SSH_ADD_CHECKS = \ + test-ssh-add \ + $(NULL) + +test_ssh_add_SOURCES = src/pam-ssh-add/test-ssh-add.c +test_ssh_add_CFLAGS = $(libpam_ssh_add_a_CFLAGS) +test_ssh_add_LDADD = $(libpam_ssh_add_a_LIBS) libretest.a + +noinst_PROGRAMS += $(PAM_SSH_ADD_CHECKS) +TESTS += $(PAM_SSH_ADD_CHECKS) + +EXTRA_DIST += \ + src/pam-ssh-add/mock-ssh-agent \ + src/pam-ssh-add/mock-ssh-add \ + src/pam-ssh-add/mock-environment \ + $(NULL) diff --git a/src/pam-ssh-add/mock-environment b/src/pam-ssh-add/mock-environment new file mode 100755 index 00000000000..43e6f701013 --- /dev/null +++ b/src/pam-ssh-add/mock-environment @@ -0,0 +1,12 @@ +#!/bin/sh +for key in XDG_RUNTIME_DIR HOME PATH LC_ALL OTHER SSH_AUTH_SOCK +do + value=$(/usr/bin/printenv "$key") + result=$? + if [ $result -eq 0 ]; then + echo "$key=$value" 1>&2 + else + echo "NO $key" 1>&2 + fi +done +exit 1; diff --git a/src/pam-ssh-add/mock-ssh-add b/src/pam-ssh-add/mock-ssh-add new file mode 100755 index 00000000000..1c2c841ba92 --- /dev/null +++ b/src/pam-ssh-add/mock-ssh-add @@ -0,0 +1,40 @@ +#!/bin/sh +PASSWORD="foobar" + +password_good=0 +password_bad=0 +password_blanks=0 + +case "$1" in + "no-socket") + exit 2;; + *) + for dummy_key in 0 1 2 + do + echo "Enter passphrase for $dummy_key" >&2 + while true + do + read answer + case $answer in + "$PASSWORD") + password_good=$(expr "$password_good" + 1) + break;; + "") + password_blanks=$(expr "$password_blanks" + 1) + break;; + *) + password_bad=$(expr "$password_bad" + 1) + echo "Bad passphrase, try again for $dummy_key" >&2 + continue;; + esac + done + done + + echo "Correct password $password_good, bad password $password_bad, password_blanks $password_blanks" >&2 + + if [ $password_good -eq 3 ]; then + exit 0; + else + exit 1; + fi +esac diff --git a/src/pam-ssh-add/mock-ssh-agent b/src/pam-ssh-add/mock-ssh-agent new file mode 100755 index 00000000000..bbe1ec8d562 --- /dev/null +++ b/src/pam-ssh-add/mock-ssh-agent @@ -0,0 +1,17 @@ +#!/bin/sh +case "$1" in + "good-vars") + echo "EXTRA=var; export EXTRA" + echo "SSH_AUTH_SOCKET=socket; export SSH_AUTH_SOCKET" + echo "SSH_AGENT_PID=100; export SSH_AGENT_PID" + echo "EXTRA2=100; export EXTRA2" + exit 0;; + "bad-vars") + echo "Just a bunch of nothing" + echo "TEST=var; export TEST" + exit 0;; + *) + echo "Normal output" + echo "Bad things" 1>&2 + exit 1;; +esac diff --git a/src/pam-ssh-add/pam-ssh-add.c b/src/pam-ssh-add/pam-ssh-add.c new file mode 100644 index 00000000000..af8f673dabf --- /dev/null +++ b/src/pam-ssh-add/pam-ssh-add.c @@ -0,0 +1,1078 @@ +/* + * This file is part of Cockpit. + * + * Copyright (C) 2015 Red Hat, Inc. + * + * Cockpit is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Cockpit 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cockpit; If not, see . + */ + +/* + * Inspired by gnome-keyring: + * Stef Walter + */ + +#define _GNU_SOURCE 1 + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "pam-ssh-add.h" + +/* programs that can be overwidden in tests */ +const char *pam_ssh_agent_program = "/usr/bin/ssh-agent"; +const char *pam_ssh_agent_arg = NULL; + +const char *pam_ssh_add_program = "/usr/bin/ssh-add"; +const char *pam_ssh_add_arg = NULL; + +/* Environment */ +#define ENVIRON_SIZE 5 +#define PATH "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +/* ssh-agent output variables we care about */ +static const char *agent_vars[] = { + "SSH_AUTH_SOCK", + "SSH_AGENT_PID", + NULL +}; + +/* pre-set file descriptors */ +#define STDIN 0 +#define STDOUT 1 +#define STDERR 2 + +/* read & write ends of a pipe */ +#define READ_END 0 +#define WRITE_END 1 + +/* pre-set file descriptors */ +#define STDIN 0 +#define STDOUT 1 +#define STDERR 2 + +/* attribute for stored auth */ +#define STORED_AUTHTOK "pam_ssh_add_authtok" + +#ifndef debug +#define debug(format, ...) \ + do { if (pam_ssh_add_verbose_mode) \ + syslog (LOG_INFO | LOG_AUTHPRIV, "pam_ssh_add: " format, ##__VA_ARGS__); \ + } while (0) +#endif + +#ifndef error +#define error(format, ...) \ + do { message_handler (LOG_ERR, "pam_ssh_add: " format, ##__VA_ARGS__); \ + } while (0) +#endif + +#ifndef message +#define message(format, ...) \ + do { message_handler (LOG_WARNING, "pam_ssh_add: " format, ##__VA_ARGS__); \ + } while (0) +#endif + +typedef int (* line_cb) (char *line, void *arg); +int pam_ssh_add_verbose_mode = 0; +pam_ssh_add_logger pam_ssh_add_log_handler = NULL; + +#ifndef message_handler +#if __GNUC__ > 2 +static void +message_handler (int level, const char *format, ...) +__attribute__((__format__(__printf__, 2, 3))); +#endif + +static void +default_logger (int level, const char *str) +{ + if (level == LOG_INFO) + debug ("%s", str); + else if (level == LOG_ERR) + syslog (LOG_ERR, "%s", str); + else + syslog (LOG_WARNING, "%s", str); +} + +static void +message_handler (int level, + const char *format, ...) +{ + va_list va; + char *data; + int res; + + if (!pam_ssh_add_log_handler) + pam_ssh_add_log_handler = &default_logger; + + /* Fast path for simple messages */ + if (!strchr (format, '%')) + { + pam_ssh_add_log_handler (level, format); + return; + } + + va_start (va, format); + res = vasprintf (&data, format, va); + va_end (va); + + if (res < 0) + { + pam_ssh_add_log_handler (LOG_ERR, "out of memory printing message"); + return; + } + + pam_ssh_add_log_handler (level, data); + free (data); +} +#endif + +static void +close_safe (int fd) +{ + if (fd != -1) + close (fd); +} + +static char * +strbtrim (char *data) +{ + assert (data); + while (*data && isspace (*data)) + ++data; + return (char*)data; +} + +static int +foreach_line (char *lines, + line_cb cb, + void *arg) +{ + char *line, *ctx; + int ret = 1; + + assert (lines); + + /* Call cb for each line in the text block */ + for (line = strtok_r (lines, "\n", &ctx); line != NULL; + line = strtok_r (NULL, "\n", &ctx)) + { + ret = (cb) (line, arg); + if (!ret) + return ret; + } + return ret; +} + +static int +closefd (void *data, + int fd) +{ + int *from = data; + if (fd >= *from) + { + while (close (fd) < 0) + { + if (errno == EAGAIN || errno == EINTR) + continue; + if (errno == EBADF || errno == EINVAL) + break; + message ("couldn't close fd in child process: %m"); + return -1; + } + } + + return 0; +} + +#ifndef HAVE_FDWALK + +static int +fdwalk (int (*cb)(void *data, int fd), + void *data) +{ + int open_max; + int fd; + int res = 0; + + struct rlimit rl; + +#ifdef __linux__ + DIR *d; + + if ((d = opendir ("/proc/self/fd"))) { + struct dirent *de; + + while ((de = readdir (d))) { + long l; + char *e = NULL; + + if (de->d_name[0] == '.') + continue; + + errno = 0; + l = strtol (de->d_name, &e, 10); + if (errno != 0 || !e || *e) + continue; + + fd = (int) l; + + if ((long) fd != l) + continue; + + if (fd == dirfd (d)) + continue; + + if ((res = cb (data, fd)) != 0) + break; + } + + closedir (d); + return res; + } + + /* If /proc is not mounted or not accessible we fall back to the old + * rlimit trick */ + +#endif + + if (getrlimit (RLIMIT_NOFILE, &rl) == 0 && rl.rlim_max != RLIM_INFINITY) + open_max = rl.rlim_max; + else + open_max = sysconf (_SC_OPEN_MAX); + + for (fd = 0; fd < open_max; fd++) + if ((res = cb (data, fd)) != 0) + break; + + return res; +} + +#endif /* HAVE_FDWALK */ + +static char * +read_string (int fd, + int consume) +{ + /* We only accept a max of 8K */ + #define MAX_LENGTH 8192 + + char buf[256]; + char *ret = NULL; + int r, len = 0; + + for (;;) + { + r = read (fd, buf, sizeof (buf)); + if (r < 0) + { + if (errno == EAGAIN || errno == EINTR) + continue; + + free (ret); + return NULL; + } + else + { + char *n = realloc (ret, len + r + 1); + if (!n) + { + free (ret); + errno = ENOMEM; + return NULL; + } + + memset (n + len, 0, r + 1); + ret = n; + len = len + r; + strncat (ret, buf, r); + } + + if (r == 0 || len > MAX_LENGTH || consume == 0) + break; + } + + return ret; +} + +static int +write_string (int fd, + const char *buf) +{ + size_t bytes = 0; + int res, len = strlen (buf); + + while (bytes < len) + { + res = write (fd, buf + bytes, len - bytes); + if (res < 0) + { + if (errno != EINTR && errno != EAGAIN) + return -1; + } + else + { + bytes += res; + } + } + + return 0; +} + +static int +log_problem (char *line, + void *arg) +{ + /* + * Called for each stderr output line from the daemon. + * Send it all to the log. + */ + + int *success; + + assert (line); + assert (arg); + + success = (int*)arg; + if (*success) + message ("%s", line); + else + error ("%s", line); + + return 1; +} + +static const char * +get_optional_env (const char *name, + const char *overide) +{ + if (overide) + return overide; + + return getenv (name); +} + +static int +build_environment (char **env, + const char *first_key, ...) +{ + int i = 0; + int res = 0; + const char *key = first_key; + va_list va; + + va_start (va, first_key); + + while (key != NULL) + { + const char *value = va_arg (va, char*); + if (value != NULL) + { + if (asprintf (env + (i++), "%s=%s", key, value) < 0) + { + error ("couldn't allocate environment"); + goto out; + } + } + key = va_arg (va, char*); + } + res = 1; + +out: + va_end (va); + return res; +} + +static void +setup_child (const char **args, + char **env, + struct passwd *pwd, + int inp[2], + int outp[2], + int errp[2]) +{ + int from; + + assert (pwd); + assert (pwd->pw_dir); + + /* Fix up our end of the pipes */ + if (dup2 (inp[READ_END], STDIN) < 0 || + dup2 (outp[WRITE_END], STDOUT) < 0 || + dup2 (errp[WRITE_END], STDERR) < 0) + { + error ("couldn't setup pipes: %m"); + exit (EXIT_FAILURE); + } + + from = STDERR + 1; + if (fdwalk (closefd, &from) < 0) + { + error ("couldn't close all file descirptors"); + exit (EXIT_FAILURE); + } + + /* Close unnecessary file descriptors */ + close (inp[READ_END]); + close (inp[WRITE_END]); + close (outp[READ_END]); + close (outp[WRITE_END]); + close (errp[READ_END]); + close (errp[WRITE_END]); + + /* Start a new session, to detach from tty */ + if (setsid() < 0) + { + error ("failed to detach child process"); + exit (EXIT_FAILURE); + } + + /* We may be running effective as another user, revert that */ + if (setegid (getgid ()) < 0 || seteuid (getuid ()) < 0) + error ("failed to restore credentials"); + + /* Setup process credentials */ + if (setgid (pwd->pw_gid) < 0 || setuid (pwd->pw_uid) < 0 || + setegid (pwd->pw_gid) < 0 || seteuid (pwd->pw_uid) < 0) + { + error ("couldn't setup credentials: %m"); + exit (EXIT_FAILURE); + } + + /* Now actually execute the process */ + execve (args[0], (char **) args, env); + error ("couldn't run %s: %m", args[0]); + exit (EXIT_FAILURE); +} + +static void +ignore_signals (struct sigaction *defsact, + struct sigaction *oldsact, + struct sigaction *ignpipe, + struct sigaction *oldpipe) +{ + /* + * Make sure that SIGCHLD occurs. Otherwise our waitpid below + * doesn't work properly. We need to wait on the process to + * get the daemon exit status. + */ + memset (defsact, 0, sizeof (*defsact)); + memset (oldsact, 0, sizeof (*oldsact)); + defsact->sa_handler = SIG_DFL; + sigaction (SIGCHLD, defsact, oldsact); + + /* + * Make sure we don't exit with a SIGPIPE while doing this, that + * would be very annoying to a user trying to log in. + */ + memset (ignpipe, 0, sizeof (*ignpipe)); + memset (oldpipe, 0, sizeof (*oldpipe)); + ignpipe->sa_handler = SIG_IGN; + sigaction (SIGPIPE, ignpipe, oldpipe); +} + +static void +restore_signals (struct sigaction *oldsact, + struct sigaction *oldpipe) +{ + /* Restore old handler */ + sigaction (SIGCHLD, oldsact, NULL); + sigaction (SIGPIPE, oldpipe, NULL); +} + +static pid_t +run_as_user (const char **args, + char **env, + struct passwd *pwd, + int inp[2], + int outp[2], + int errp[2]) +{ + pid_t pid = -1; + + /* Start up daemon child process */ + switch (pid = fork ()) + { + case -1: + error ("couldn't fork: %m"); + goto done; + + /* This is the child */ + case 0: + setup_child (args, env, pwd, inp, outp, errp); + /* Should never be reached */ + break; + + /* This is the parent */ + default: + break; + }; + +done: + return pid; +} + +static int +get_environ_vars_from_agent (char *line, + void *arg) +{ + /* + * ssh-agent outputs commands for exporting it's envirnment + * variables. We want to return these variables so parse + * them out and store them. + */ + + char *c = NULL; + int i; + int ret = 1; + const char sep[] = "; export"; + + char **ret_array = (char**)arg; + + assert (line); + assert (arg); + + line = strbtrim (line); + debug ("got line: %s", line); + c = strstr (line, sep); + if (c) + { + *c = '\0'; + debug ("name/value is: %s", line); + for (i = 0; agent_vars[i] != NULL; i++) + { + if (strstr(line, agent_vars[i])) + { + if (asprintf (ret_array + (i), "%s", line) < 0) + { + error ("Error allocating output variable"); + ret = 0; + } + break; + } + } + } + + return ret; +} + +int +pam_ssh_add_load (struct passwd *pwd, + const char *agent_socket, + const char *password) +{ + struct sigaction defsact, oldsact, ignpipe, oldpipe; + int i; + int inp[2] = { -1, -1 }; + int outp[2] = { -1, -1 }; + int errp[2] = { -1, -1 }; + + char *env[ENVIRON_SIZE] = { NULL }; + const char *args[] = { "/bin/sh", "-c", "$0 $1", + pam_ssh_add_program, + pam_ssh_add_arg, + NULL }; + + pid_t pid; + int success = 0; + int force_stderr_debug = 1; + + siginfo_t result; + + ignore_signals (&defsact, &oldsact, &ignpipe, &oldpipe); + + assert (pwd); + if (!agent_socket) + { + message ("ssh-add requires an agent socket"); + goto done; + } + + if (!build_environment (env, + "PATH", PATH, + "LC_ALL", "C", + "HOME", pwd->pw_dir, + "SSH_AUTH_SOCK", agent_socket, + NULL)) + goto done; + + /* Create the necessary pipes */ + if (pipe (inp) < 0 || pipe (outp) < 0 || pipe (errp) < 0) + { + error ("couldn't create pipes: %m"); + goto done; + } + + pid = run_as_user (args, env, pwd, + inp, outp, errp); + if (pid < 1) + goto done; + + /* in the parent, close our unneeded ends of the pipes */ + close (inp[READ_END]); + close (outp[WRITE_END]); + close (errp[WRITE_END]); + inp[READ_END] = outp[WRITE_END] = errp[WRITE_END] = -1; + for (;;) + { + /* ssh-add asks for password on stderr */ + char *outerr = read_string (errp[READ_END], 0); + if (outerr == NULL || outerr[0] == '\0') + { + free (outerr); + break; + } + + if (strstr (outerr, "Enter passphrase") != NULL) + { + debug ("Got password request"); + if (password != NULL) + write_string (inp[WRITE_END], password); + write_string (inp[WRITE_END], "\n"); + } + else if (strstr (outerr, "Bad passphrase")) + { + debug ("sent bad password"); + write_string (inp[WRITE_END], "\n"); + } + else + { + foreach_line (outerr, log_problem, + &force_stderr_debug); + } + + free (outerr); + } + + /* Wait for the initial process to exit */ + if (waitid (P_PID, pid, &result, WEXITED) < 0) + { + error ("couldn't wait on ssh-add process: %m"); + goto done; + } + + success = result.si_code == CLD_EXITED && result.si_status == 0; + /* Failure from process */ + if (!success) + { + /* key loading failed, don't report as an error */ + if (result.si_code == 1) + { + success = 1; + message ("Failed adding some keys"); + } + else + { + message ("Failed adding keys: %d", result.si_status); + } + } + +done: + restore_signals (&oldsact, &oldpipe); + + close_safe (inp[0]); + close_safe (inp[1]); + close_safe (outp[0]); + close_safe (outp[1]); + close_safe (errp[0]); + close_safe (errp[1]); + + for (i = 0; env[i] != NULL; i++) + free (env[i]); + + return success; +} + +int +pam_ssh_add_start_agent (struct passwd *pwd, + const char *xdg_runtime_overide, + char **out_auth_sock_var, + char **out_agent_pid_var) +{ + char *env[ENVIRON_SIZE] = { NULL }; + const char *xdg_runtime; + + struct sigaction defsact, oldsact, ignpipe, oldpipe; + siginfo_t result; + + int inp[2] = { -1, -1 }; + int outp[2] = { -1, -1 }; + int errp[2] = { -1, -1 }; + pid_t pid; + + const char *args[] = { "/bin/sh", "-c", "$0 $1", + pam_ssh_agent_program, + pam_ssh_agent_arg, + NULL }; + + char *output = NULL; + char *outerr = NULL; + int success = 0; + int i = 0; + + char *save_vars[N_ELEMENTS (agent_vars)] = { NULL, }; + + assert (pwd); + xdg_runtime = get_optional_env ("XDG_RUNTIME_DIR", + xdg_runtime_overide); + if (!build_environment (env, + "PATH", PATH, + "LC_ALL", "C", + "HOME", pwd->pw_dir, + "XDG_RUNTIME_DIR", xdg_runtime, + NULL)) + goto done; + + ignore_signals (&defsact, &oldsact, &ignpipe, &oldpipe); + /* Create the necessary pipes */ + if (pipe (inp) < 0 || pipe (outp) < 0 || pipe (errp) < 0) + { + error ("couldn't create pipes: %m"); + goto done; + } + + pid = run_as_user (args, env, pwd, + inp, outp, errp); + if (pid < 1) + goto done; + + /* in the parent, close our unneeded ends of the pipes */ + close (inp[READ_END]); + close (outp[WRITE_END]); + close (errp[WRITE_END]); + close (inp[WRITE_END]); + + inp[READ_END] = outp[WRITE_END] = errp[WRITE_END] = -1; + + /* Read any stdout and stderr data */ + output = read_string (outp[READ_END], 1); + outerr = read_string (errp[READ_END], 0); + if (!output || !outerr) + { + error ("couldn't read data from ssh-agent: %m"); + goto done; + } + + /* Wait for the initial process to exit */ + if (waitid (P_PID, pid, &result, WEXITED) < 0) + { + error ("couldn't wait on ssh-agent process: %m"); + goto done; + } + + success = result.si_code == CLD_EXITED && result.si_status == 0; + + if (outerr && outerr[0]) + foreach_line (outerr, log_problem, &success); + + foreach_line (output, get_environ_vars_from_agent, save_vars); + + /* Failure from process */ + if (!success) + { + error ("Failed to start ssh-agent"); + } + /* Failure to find vars */ + else if (!save_vars[0] || !save_vars[1]) + { + message ("Expected agent environment variables not found"); + success = 0; + } + + if (out_auth_sock_var && save_vars[0]) + *out_auth_sock_var = strdup (save_vars[0]); + + if (out_agent_pid_var && save_vars[1]) + *out_agent_pid_var = strdup (save_vars[1]); + +done: + restore_signals (&oldsact, &oldpipe); + + close_safe (inp[0]); + close_safe (inp[1]); + close_safe (outp[0]); + close_safe (outp[1]); + close_safe (errp[0]); + close_safe (errp[1]); + + free (output); + free (outerr); + + /* save_vars may contain NULL + * values use agent_vars as the + * marker instead + */ + for (i = 0; agent_vars[i] != NULL; i++) + free (save_vars[i]); + + for (i = 0; env[i] != NULL; i++) + free (env[i]); + + return success; +} + +/* -------------------------------------------------------------------------------- + * PAM Module + */ + +static void +parse_args (int argc, + const char **argv) +{ + int i; + + pam_ssh_add_verbose_mode = 0; + + /* Parse the arguments */ + for (i = 0; i < argc; i++) + { + if (strcmp (argv[i], "debug") == 0) + { + pam_ssh_add_verbose_mode = 1; + } + else + { + message ("invalid option: %s", argv[i]); + continue; + } + } +} + +static void +free_password (char *password) +{ + volatile char *vp; + size_t len; + + if (!password) + return; + + /* Defeats some optimizations */ + len = strlen (password); + memset (password, 0xAA, len); + memset (password, 0xBB, len); + + /* Defeats others */ + vp = (volatile char*)password; + while (*vp) + *(vp++) = 0xAA; + + free (password); +} + +static void +cleanup_free_password (pam_handle_t *pamh, + void *data, + int pam_end_status) +{ + free_password (data); +} + +static int +stash_password_for_session (pam_handle_t *pamh, + const char *password) +{ + if (pam_set_data (pamh, STORED_AUTHTOK, strdup (password), + cleanup_free_password) != PAM_SUCCESS) + { + message ("error stashing password for session"); + return PAM_AUTHTOK_RECOVER_ERR; + } + + return PAM_SUCCESS; +} + +static int +start_agent (pam_handle_t *pamh, + struct passwd *auth_pwd) +{ + char *auth_socket = NULL; + char *auth_pid = NULL; + int success = 0; + int res; + + success = pam_ssh_add_start_agent (auth_pwd, + pam_getenv (pamh, "XDG_RUNTIME_DIR"), + &auth_socket, + &auth_pid); + + /* Store pid and socket enviroment vars */ + if (!success || !auth_socket || !auth_pid) + { + res = PAM_SERVICE_ERR; + } + else + { + res = pam_putenv (pamh, auth_socket); + if (res == PAM_SUCCESS) + res = pam_putenv (pamh, auth_pid); + + if (res != PAM_SUCCESS) + { + error ("couldn't set agent environment: %s", + pam_strerror (pamh, res)); + } + } + + free (auth_socket); + free (auth_pid); + + return res; +} + +static int +load_keys (pam_handle_t *pamh, + struct passwd *auth_pwd) +{ + const char *password; + int success = 0; + + /* Get the stored authtok here */ + if (pam_get_data (pamh, STORED_AUTHTOK, + (const void**)&password) != PAM_SUCCESS) + { + password = NULL; + } + + success = pam_ssh_add_load (auth_pwd, + pam_getenv (pamh, "SSH_AUTH_SOCK"), + password); + + return success ? PAM_SUCCESS : PAM_SERVICE_ERR; +} + +PAM_EXTERN int +pam_sm_open_session (pam_handle_t *pamh, + int flags, + int argc, + const char *argv[]) +{ + int res; + int o_res; + + struct passwd *auth_pwd; + const char *user; + + parse_args (argc, argv); + + /* Lookup the user */ + res = pam_get_user (pamh, &user, NULL); + if (res != PAM_SUCCESS) + { + message ("couldn't get pam user: %s", pam_strerror (pamh, res)); + goto out; + } + + auth_pwd = getpwnam (user); + if (!auth_pwd) + { + error ("error looking up user information"); + res = PAM_SERVICE_ERR; + goto out; + } + + res = start_agent (pamh, auth_pwd); + + if (res == PAM_SUCCESS) + res = load_keys (pamh, auth_pwd); + +out: + /* Delete the stored password, + unless we are not in start mode + then we might still need it. + */ + o_res = pam_set_data (pamh, STORED_AUTHTOK, + NULL, cleanup_free_password); + if (o_res != PAM_SUCCESS) + { + message ("couldn't delete stored authtok: %s", + pam_strerror (pamh, o_res)); + } + + return res; +} + +PAM_EXTERN int +pam_sm_close_session (pam_handle_t *pamh, + int flags, + int argc, + const char *argv[]) +{ + const char *s_pid; + int pid = 0; + parse_args (argc, argv); + + /* Kill the ssh agent we started */ + s_pid = pam_getenv (pamh, "SSH_AGENT_PID"); + if (s_pid) + pid = atoi (s_pid); + + if (pid > 0) + { + debug ("Closing %d", pid); + kill (pid, SIGTERM); + } + return PAM_SUCCESS; +} + +PAM_EXTERN int +pam_sm_authenticate (pam_handle_t *pamh, + int unused, + int argc, + const char **argv) +{ + const char *password; + int ret; + + parse_args (argc, argv); + + /* Look up the password and store it for later */ + ret = pam_get_item (pamh, PAM_AUTHTOK, + (const void**)&password); + if (ret != PAM_SUCCESS) + message ("no password is available: %s", + pam_strerror (pamh, ret)); + + if (password != NULL) + stash_password_for_session (pamh, password); + + /* We're not an authentication module */ + return PAM_CRED_INSUFFICIENT; +} + +PAM_EXTERN int +pam_sm_setcred (pam_handle_t *pamh, + int flags, + int argc, + const char *argv[]) +{ + return PAM_SUCCESS; +} diff --git a/src/pam-ssh-add/pam-ssh-add.h b/src/pam-ssh-add/pam-ssh-add.h new file mode 100644 index 00000000000..74495fc0452 --- /dev/null +++ b/src/pam-ssh-add/pam-ssh-add.h @@ -0,0 +1,45 @@ +/* + * This file is part of Cockpit. + * + * Copyright (C) 2015 Red Hat, Inc. + * + * Cockpit is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Cockpit 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cockpit; If not, see . + */ + +#ifndef PAM_SSH_ADD_H__ +#define PAM_SSH_ADD_H__ + +#include "pwd.h" + +#define N_ELEMENTS(x) (sizeof(x) / sizeof (x)[0]) + +extern const char *pam_ssh_agent_program; +extern const char *pam_ssh_agent_arg; +extern const char *pam_ssh_add_program; +extern const char *pam_ssh_add_arg; +extern int pam_ssh_add_verbose_mode; + +typedef void (*pam_ssh_add_logger) (int level, const char *data); +extern pam_ssh_add_logger pam_ssh_add_log_handler; + +int pam_ssh_add_start_agent (struct passwd *pwd, + const char *xdg_runtime_overide, + char **out_auth_sock_var, + char **out_agent_pid_var); + +int pam_ssh_add_load (struct passwd *pwd, + const char *agent_socket, + const char *password); + +#endif /* PAM_SSH_ADD_H__ */ diff --git a/src/pam-ssh-add/test-ssh-add.c b/src/pam-ssh-add/test-ssh-add.c new file mode 100644 index 00000000000..0520f820c5c --- /dev/null +++ b/src/pam-ssh-add/test-ssh-add.c @@ -0,0 +1,433 @@ +/* + * This file is part of Cockpit. + * + * Copyright (C) 2015 Red Hat, Inc. + * + * Cockpit is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Cockpit 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cockpit; If not, see . + */ + +#define _GNU_SOURCE + +#include "pam-ssh-add.h" + +#include "retest/retest.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +static int unexpected_message; + +/* Enviroment variables we set */ +static const char *env_names[] = { + "XDG_RUNTIME_DIR", + "HOME", + "PATH", + "LC_ALL", + "SSH_AUTH_SOCK", + NULL +}; + + +/* Holds environment values to set in pam context */ +static char *env_saved[N_ELEMENTS (env_names)] = { NULL, }; + +typedef struct { + const char *ssh_add; + const char *ssh_add_arg; + const char *ssh_agent; + const char *ssh_agent_arg; + const char *password; + struct passwd *pw; +} Fixture; + +struct _ExpectedMessage { + const char *line; + TAILQ_ENTRY (_ExpectedMessage) messages; +}; + +TAILQ_HEAD (ExpectedList, _ExpectedMessage) el_head; + +typedef struct _ExpectedMessage ExpectedMessage; + +static void +free_expected (ExpectedMessage *em) +{ + free (em); + em = NULL; +} + +static void +expect_message (const char *msg) +{ + ExpectedMessage *em = NULL; + em = (ExpectedMessage *) malloc(sizeof(ExpectedMessage)); + if (em == NULL) + assert_not_reached ("expected message allocation failed"); + em->line = msg; + TAILQ_INSERT_TAIL (&el_head, em, messages); +} + +static void +test_logger (int level, const char *msg) +{ + assert (msg != NULL); + if (el_head.tqh_first != NULL) + { + ExpectedMessage *em = el_head.tqh_first; + assert_str_contains (msg, em->line); + TAILQ_REMOVE (&el_head, el_head.tqh_first, messages); + free_expected (em); + } + else + { + warnx ("%s", msg); + unexpected_message = 1; + } +} + +static void +save_environment (void) +{ + int i; + + for (i = 0; env_names[i] != NULL; i++) + env_saved[i] = getenv (env_names[i]); +} + +static void +restore_environment (void) +{ + int i; + for (i = 0; env_names[i] != NULL; i++) + { + if (env_saved[i]) + setenv (env_names[i], env_saved[i], 1); + else + unsetenv (env_names[i]); + } +} + +static void +setup (void *arg) +{ + Fixture *fix = arg; + unexpected_message = 0; + if (!fix->ssh_add) + fix->ssh_add = SRCDIR "/src/pam-ssh-add/mock-ssh-add"; + + if (!fix->ssh_agent) + fix->ssh_agent = SRCDIR "/src/pam-ssh-add/mock-ssh-agent"; + + pam_ssh_add_program = fix->ssh_add; + pam_ssh_add_arg = fix->ssh_add_arg; + pam_ssh_agent_program = fix->ssh_agent; + pam_ssh_agent_arg = fix->ssh_agent_arg; + fix->pw = getpwuid (getuid ()); +} + +static void +teardown (void *arg) +{ + int missed = 0; + + // restore original environment + restore_environment (); + + while (el_head.tqh_first != NULL) + { + ExpectedMessage *em = el_head.tqh_first; + warnx ("message didn't get logged: %s", em->line); + TAILQ_REMOVE (&el_head, el_head.tqh_first, messages); + free_expected (em); + missed = 1; + } + + if (missed) + assert_not_reached ("expected messages didn't get logged"); + + if (unexpected_message) + assert_not_reached ("got unexpected messages"); + +} + +static Fixture default_fixture = { +}; + +static Fixture environment_fixture = { + .ssh_agent = SRCDIR "/src/pam-ssh-add/mock-environment", + .ssh_agent_arg = NULL +}; + +static void +run_test_agent_environment (void *data, + const char *xdg_runtime, + const char *xdg_runtime_expect) +{ + Fixture *fix = data; + int ret; + char *xdg_expect = NULL; + char *home_expect = NULL; + + if (xdg_runtime_expect) + { + if (asprintf (&xdg_expect, "XDG_RUNTIME_DIR=%s", + xdg_runtime_expect) < 0) + warnx ("Couldn't allocate XDG_RUNTIME_DIR expect variable"); + } + else + { + xdg_expect = strdup ("NO XDG_RUNTIME_DIR"); + } + + if (asprintf (&home_expect, "HOME=%s", fix->pw->pw_dir) < 0) + warnx ("Couldn't allocate HOME expect variable"); + + expect_message (xdg_expect); + expect_message (home_expect); + + expect_message ("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"); + expect_message ("LC_ALL=C"); + expect_message ("NO OTHER"); + + expect_message ("NO SSH_AUTH_SOCK"); + expect_message ("Failed to start ssh-agent"); + + ret = pam_ssh_add_start_agent (fix->pw, xdg_runtime, NULL, NULL); + + assert_num_eq (0, ret); + + free (xdg_expect); + free (home_expect); +} + +static void +test_environment (void *data) +{ + run_test_agent_environment (data, NULL, getenv ("XDG_RUNTIME_DIR")); +} + +static void +test_environment_env_overides (void *data) +{ + setenv ("PATH", "bad", 1); + setenv ("LC_ALL", "bad", 1); + setenv ("HOME", "bad", 1); + setenv ("XDG_RUNTIME_DIR", "", 1); + setenv ("SSH_AUTH_SOCK", "bad", 1); + setenv ("OTHER", "bad", 1); + + run_test_agent_environment (data, NULL, ""); +} + +static void +test_environment_overides (void *data) +{ + setenv ("XDG_RUNTIME_DIR", "bad", 1); + run_test_agent_environment (data, "xdgover", "xdgover"); +} + +static void +test_failed_agent (void *data) +{ + Fixture *fix = data; + char *sock = NULL; + char *pid = NULL; + int ret; + + expect_message ("Bad things"); + expect_message ("Failed to start ssh-agent"); + ret = pam_ssh_add_start_agent (fix->pw, NULL, &sock, &pid); + + assert_num_eq (0, ret); + assert_ptr_eq (sock, NULL); + assert_ptr_eq (pid, NULL); + + free (sock); + free (pid); +} + +static Fixture bad_agent_fixture = { + .ssh_agent_arg = "bad-vars", +}; + +static void +test_bad_agent_vars (void *data) +{ + Fixture *fix = data; + char *sock = NULL; + char *pid = NULL; + int ret; + + expect_message ("Expected agent environment variables not found"); + ret = pam_ssh_add_start_agent (fix->pw, NULL, &sock, &pid); + + assert_num_eq (0, ret); + assert_ptr_eq (sock, NULL); + assert_ptr_eq (pid, NULL); + + free (sock); + free (pid); +} + +static Fixture good_agent_fixture = { + .ssh_agent_arg = "good-vars", +}; + +static void +test_good_agent_vars (void *data) +{ + Fixture *fix = data; + char *sock = NULL; + char *pid = NULL; + int ret; + + ret = pam_ssh_add_start_agent (fix->pw, NULL, &sock, &pid); + + assert_num_eq (1, ret); + assert_str_cmp (sock, ==, "SSH_AUTH_SOCKET=socket"); + assert_str_cmp (pid, ==, "SSH_AGENT_PID=100"); + + free (sock); + free (pid); +} + +static Fixture keys_password_fixture = { + .ssh_add_arg = NULL, + .password = "foobar", +}; + +static Fixture keys_no_password_fixture = { + .ssh_add_arg = NULL, + .password = NULL, +}; + +static Fixture keys_bad_password_fixture = { + .ssh_add_arg = NULL, + .password = "bad", +}; + +static void +test_keys (void *data) +{ + int ret; + int expect = 1; + Fixture *fix = data; + const char *key_add_result; + + if (fix->password == NULL) + { + key_add_result = "Correct password 0, bad password 0, password_blanks 3"; + } + else if (strcmp (fix->password, "foobar") == 0) + { + expect = 0; + key_add_result = "Correct password 3, bad password 0, password_blanks 0"; + } + else + { + key_add_result = "Correct password 0, bad password 3, password_blanks 3"; + } + + expect_message (key_add_result); + if (expect) + expect_message ("Failed adding some keys"); + + ret = pam_ssh_add_load (fix->pw, "mock-socket", fix->password); + + assert_num_eq (1, ret); +} + +static Fixture keys_environment_fixture = { + .ssh_add = SRCDIR "/src/pam-ssh-add/mock-environment", + .ssh_add_arg = NULL +}; + +static void +test_key_environment (void *data) +{ + Fixture *fix = data; + int ret; + char *home_expect = NULL; + + expect_message ("ssh-add requires an agent socket"); + ret = pam_ssh_add_load (fix->pw, NULL, NULL); + assert_num_eq (0, ret); + + if (asprintf (&home_expect, "HOME=%s", fix->pw->pw_dir) < 0) + warnx ("Couldn't allocate HOME expect variable"); + + expect_message ("NO XDG_RUNTIME_DIR"); + expect_message (home_expect); + + expect_message ("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"); + expect_message ("LC_ALL=C"); + expect_message ("NO OTHER"); + + expect_message ("SSH_AUTH_SOCK=mock-socket"); + expect_message ("Failed adding some keys"); + + ret = pam_ssh_add_load (fix->pw, "mock-socket", NULL); + + assert_num_eq (1, ret); + + free (home_expect); +} + +int +main (int argc, + char *argv[]) +{ + signal (SIGPIPE, SIG_IGN); + + TAILQ_INIT(&el_head); + + save_environment (); + + re_fixture (setup, teardown); + + pam_ssh_add_log_handler = &test_logger; + pam_ssh_add_verbose_mode = 0; + + re_testx (test_key_environment, &keys_environment_fixture, + "/pam-ssh-add/add-key-environment"); + re_testx (test_keys, &keys_no_password_fixture, + "/pam-ssh-add/add-key-no-password"); + re_testx (test_keys, &keys_bad_password_fixture, + "/pam-ssh-add/add-key-bad-password"); + re_testx (test_keys, &keys_password_fixture, + "/pam-ssh-add/add-key-password"); + + re_testx (test_environment, &environment_fixture, + "/pam-ssh-add/environment"); + re_testx (test_environment_env_overides, &environment_fixture, + "/pam-ssh-add/environment-env-overides"); + re_testx (test_environment_overides, &environment_fixture, + "/pam-ssh-add/environment-overides"); + re_testx (test_good_agent_vars, &good_agent_fixture, + "/pam-ssh-add/good-agent-vars"); + re_testx (test_bad_agent_vars, &bad_agent_fixture, + "/pam-ssh-add/bad-agent-vars"); + re_testx (test_failed_agent, &default_fixture, + "/pam-ssh-add/test-failed-agent"); + + return re_test_run (argc, argv); +} diff --git a/src/reauthorize/Makefile-reauthorize.am b/src/reauthorize/Makefile-reauthorize.am index 76f57100b09..a9b2c2c6d5e 100644 --- a/src/reauthorize/Makefile-reauthorize.am +++ b/src/reauthorize/Makefile-reauthorize.am @@ -23,7 +23,7 @@ EXTRA_DIST += \ CLEANFILES += $(pam_MODULES) -install-exec-local: +install-exec-local:: $(MKDIR_P) $(DESTDIR)$(pamdir) $(INSTALL) pam_reauthorize.so $(DESTDIR)$(pamdir) uninstall-local:: diff --git a/src/selinux/cockpit.te b/src/selinux/cockpit.te index 07ad7994ebb..f9dc216dd46 100644 --- a/src/selinux/cockpit.te +++ b/src/selinux/cockpit.te @@ -88,6 +88,7 @@ allow cockpit_session_t init_t:unix_stream_socket sendto; # cockpit-session does these when authenticating via GSSAPI allow cockpit_session_t tmp_t:file { create write open unlink }; +allow cockpit_session_t tmp_t:dir { create rmdir }; # cockpit-session can execute cockpit-bridge as the user userdom_spec_domtrans_all_users(cockpit_session_t) diff --git a/test/check-multi-machine-key b/test/check-multi-machine-key new file mode 100755 index 00000000000..268c40b5918 --- /dev/null +++ b/test/check-multi-machine-key @@ -0,0 +1,196 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Cockpit. +# +# Copyright (C) 2013 Red Hat, Inc. +# +# Cockpit is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# Cockpit 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Cockpit; If not, see . + +from testlib import * + +import socket +import time +import unittest +import os +import subprocess + +def inject_extras(browser): + browser.eval_js(""" + dashboard_addresses = function () { + var addresses = $('#dashboard-hosts .list-group-item').map(function(i,e) { return $(e).data("address"); }).get(); + return addresses; + } + """) + +def get_webapp_machines(browser): + wm = { } + cockpit = browser.eval_js("return dashboard_addresses();") + for i in range (0, len(cockpit)): + wm[cockpit[i]] = i; + return wm + +def wait_dashboard_addresses(b, expected): + b.wait_js_func( + """(function (expected) { + return expected.sort().toString() == dashboard_addresses().sort().toString(); + })""", expected) + +def add_machine(b, address): + b.click('#dashboard-add') + b.wait_popup('dashboard_setup_server_dialog') + b.set_val('#dashboard_setup_address', address) + b.wait_text('#dashboard_setup_next', "Next") + b.click('#dashboard_setup_next') + b.wait_text('#dashboard_setup_next', "Add host") + b.click('#dashboard_setup_next') + b.wait_text('#dashboard_setup_next', "Close") + b.click('#dashboard_setup_next') + b.wait_popdown('dashboard_setup_server_dialog') + +def kill_user_admin(machine): + # logind from systemd 208 is buggy, so we use systemd directly if it fails + # https://bugs.freedesktop.org/show_bug.cgi?id=71092 + machine.execute("loginctl kill-user admin || systemctl kill user-1000.slice") + +LOAD_KEYS = [ + "id_rsa", # password: foobar + "identity", # no password + "id_dsa", # password: badbad + "id_ed25519", # password: foobar +] + +KEY_IDS = [ + +] +def test_keys(b, keys): + b.call_js_func("""(function () { + window.test_result = null; + cockpit.spawn([ "ssh-add", "-l" ]). + done(function (data) { + window.test_result = data; + }). + fail(function (error) { + console.log(error); + }); + })""",) + + b.wait_js_func("(function (expected) { return window.test_result == expected; })", "\n".join(keys) + "\n") + +@unittest.skipIf("atomic" in os.environ.get("TEST_OS", "") or "rhel" in os.environ.get("TEST_OS", ""), "key auth: not supported") +class TestMultiMachineKeyAuth(MachineCase): + + def setUp(self): + MachineCase.setUp(self) + self.machine2 = self.new_machine() + self.machine2.start() + self.machine2.wait_boot() + + def tearDown(self): + if self.runner and self.runner.wasSuccessful(): + self.check_journal_messages(self.machine2) + MachineCase.tearDown(self) + + def testBasic(self): + b = self.browser + m1 = self.machine + m2 = self.machine2 + + self.login_and_go("dashboard", href="dashboard/list", host=None) + inject_extras(b) + wait_dashboard_addresses (b, [ "localhost" ]) + + add_machine(b, m2.address) + wait_dashboard_addresses (b, [ "localhost", m2.address ]) + + wm = get_webapp_machines(b) + m1_index = wm["localhost"]+1 + m2_index = wm[m2.address]+1 + + # Logout + b.logout() + b.wait_visible("#login") + + # Change the admin password on m2 so password login + # no longer works + m2.execute("echo abcdefg | passwd --stdin admin") + m1.execute("mkdir -p /home/admin/.ssh") + + m1.upload(["ssh/{0}".format(k) for k in LOAD_KEYS], + "/home/admin/.ssh/") + m1.upload(["ssh/{0}.pub".format(k) for k in LOAD_KEYS], + "/home/admin/.ssh/") + m1.execute("chmod 400 /home/admin/.ssh/*") + m1.execute("chown -R admin:admin /home/admin/.ssh") + + # Server is marked as down. + b.login_and_go("dashboard", href="dashboard/list", host=None) + inject_extras(b) + wait_dashboard_addresses (b, [ "localhost", m2.address ]) + b.wait_attr('#dashboard-hosts a:nth-child(%d) img' % m2_index, 'src', 'images/server-error.png') + + try: + m1.execute("ps xa | grep ssh-agent | grep -v grep") + except subprocess.CalledProcessError: + assert False, "No running ssh-agent found" + + # Check our keys were loaded. + test_keys(b, [ + "2048 SHA256:SRvBhCmkCEVnJ6ascVH0AoVEbS3nPbowZkNevJnXtgw /home/admin/.ssh/id_rsa (RSA)", + "256 SHA256:Uht4NX54Gjz62cNA8+LrHo63HiFW/i5aWg/cl/A3X+c /home/admin/.ssh/id_ed25519 (ED25519)", + "2048 SHA256:gwSyNJuAvsZXlsv+zQNten2XaCe4cw6Gbqw2tmFTBRA /home/admin/.ssh/identity (RSA)", + ]) + + # add key + m2.execute("mkdir -p /home/admin/.ssh") + m2.upload(["ssh/id_rsa.pub"], "/home/admin/.ssh/authorized_keys") + m2.execute("chown -R admin:admin /home/admin/.ssh/") + m2.execute("chmod 600 /home/admin/.ssh/authorized_keys") + m2.execute("chmod 700 /home/admin/.ssh") + + # Server is up. + b.click('#dashboard-hosts a:nth-child(%d)' % m2_index) + b.switch_to_top() + b.wait_js_cond('window.location.hash != "#/dashboard/list"') + b.enter_page("server", m2.address) + b.wait_text_not("#system_information_hostname_button", "") + b.switch_to_top() + b.go("/dashboard/list") + b.enter_page("dashboard") + b.wait_attr('#dashboard-hosts a:nth-child(%d) img' % m2_index, 'src', 'images/server-small.png') + + # Logout + b.logout() + b.wait_visible("#login") + try: + m1.execute("ps xa | grep ssh-agent | grep -v grep") + assert False, "Logout did not stop ssh agent." + except subprocess.CalledProcessError: + pass + + self.allow_restart_journal_messages() + self.allow_journal_messages(".*: .* host key for server is not known: .*", + # Might happen when killing the bridge. + "localhost: dropping message while waiting for child to exit", + "Received message for unknown channel: .*", + ".*: error reading from ssh", + ".*: bridge program failed: Child process exited with code .*", + # Since there is not password, + # reauthorize doesn't work on m2 + ".*: user admin reauthorization failed", + "Error executing command as another user: Not authorized", + "This incident has been reported.", + "sudo: a password is required") + +test_main() diff --git a/test/check-verify b/test/check-verify index 433f95b9ce2..921b4359525 100755 --- a/test/check-verify +++ b/test/check-verify @@ -65,6 +65,7 @@ $RUNNER <> shell.list %files bridge %doc %{_mandir}/man1/cockpit-bridge.1.gz +%doc %{_mandir}/man8/pam_ssh_add.8.gz %{_bindir}/cockpit-bridge %attr(4755, -, -) %{_libexecdir}/cockpit-polkit %{_libexecdir}/cockpit-wrapper %{_libdir}/security/pam_reauthorize.so +%{_libdir}/security/pam_ssh_add.so %{_datadir}/dbus-1/services/com.redhat.Cockpit.service %files doc From de18a6195784f8b8b8e5a2d49ac768eb1871ce2d Mon Sep 17 00:00:00 2001 From: Stef Walter Date: Mon, 24 Aug 2015 13:31:04 +0200 Subject: [PATCH 3/8] pam-ssh-add: No use printing messages when out of memory If out of memory, we won't be able to print the message --- src/pam-ssh-add/pam-ssh-add.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pam-ssh-add/pam-ssh-add.c b/src/pam-ssh-add/pam-ssh-add.c index af8f673dabf..179a5ffe2d7 100644 --- a/src/pam-ssh-add/pam-ssh-add.c +++ b/src/pam-ssh-add/pam-ssh-add.c @@ -144,13 +144,9 @@ message_handler (int level, res = vasprintf (&data, format, va); va_end (va); - if (res < 0) - { - pam_ssh_add_log_handler (LOG_ERR, "out of memory printing message"); - return; - } + if (res > 0) + pam_ssh_add_log_handler (level, data); - pam_ssh_add_log_handler (level, data); free (data); } #endif From c127a40f2cf8aa43a29924998da49fcb5accafb9 Mon Sep 17 00:00:00 2001 From: Stef Walter Date: Mon, 24 Aug 2015 13:31:28 +0200 Subject: [PATCH 4/8] pam-ssh-add: Don't call exit() when in forked child This will otherwise call the atexit() functions. --- src/pam-ssh-add/pam-ssh-add.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pam-ssh-add/pam-ssh-add.c b/src/pam-ssh-add/pam-ssh-add.c index 179a5ffe2d7..51d6d5803a2 100644 --- a/src/pam-ssh-add/pam-ssh-add.c +++ b/src/pam-ssh-add/pam-ssh-add.c @@ -466,7 +466,7 @@ setup_child (const char **args, /* Now actually execute the process */ execve (args[0], (char **) args, env); error ("couldn't run %s: %m", args[0]); - exit (EXIT_FAILURE); + _exit (EXIT_FAILURE); } static void From 3cd28a3c7662db5ebf08ba340aeff5b2030c3a5e Mon Sep 17 00:00:00 2001 From: Stef Walter Date: Mon, 24 Aug 2015 13:48:30 +0200 Subject: [PATCH 5/8] bridge: Actually use the usermod path we lookup in configure.ac --- src/bridge/cockpitdbussetup.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bridge/cockpitdbussetup.c b/src/bridge/cockpitdbussetup.c index 29f99b841aa..5a4ebaad3e0 100644 --- a/src/bridge/cockpitdbussetup.c +++ b/src/bridge/cockpitdbussetup.c @@ -38,7 +38,7 @@ const gchar *cockpit_bridge_path_shadow = "/etc/shadow"; const gchar *cockpit_bridge_path_newusers = "/usr/sbin/newusers"; const gchar *cockpit_bridge_path_chpasswd = "/usr/sbin/chpasswd"; -const gchar *cockpit_bridge_path_usermod = "/usr/sbin/usermod"; +const gchar *cockpit_bridge_path_usermod = PATH_USERMOD; static GVariant * setup_get_property (GDBusConnection *connection, From 78011d71d664c75ff26ade574e82fbdf338e4102 Mon Sep 17 00:00:00 2001 From: Stef Walter Date: Mon, 24 Aug 2015 13:50:34 +0200 Subject: [PATCH 6/8] configure: Report the paths of commands we've found --- configure.ac | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/configure.ac b/configure.ac index d4fd7b93eee..d3e1a8ac03e 100644 --- a/configure.ac +++ b/configure.ac @@ -476,4 +476,8 @@ echo " With address sanitizer: ${asan_status} Branding: ${BRAND} Supports key auth: ${key_auth} + + pkexec: ${PKEXEC} + sudo: ${SUDO} + usermod: ${USERMOD} " From 603d243f19db3338c2ae1c9335f4c36783385818 Mon Sep 17 00:00:00 2001 From: Stef Walter Date: Mon, 24 Aug 2015 13:52:11 +0200 Subject: [PATCH 7/8] pam-ssh-add: Lookup path to ssh-add and ssh-agent in configure --- configure.ac | 11 ++++++++++- src/pam-ssh-add/pam-ssh-add.c | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/configure.ac b/configure.ac index d3e1a8ac03e..11bff87f046 100644 --- a/configure.ac +++ b/configure.ac @@ -256,8 +256,15 @@ AC_DEFINE_UNQUOTED([PATH_PKEXEC], ["$PKEXEC"], [Location of pkexec binary]) AC_PATH_PROG([SUDO], [sudo], [/usr/bin/sudo], [$PATH:/usr/local/sbin:/usr/sbin:/sbin]) AC_DEFINE_UNQUOTED([PATH_SUDO], ["$SUDO"], [Location of sudo binary]) -# Config +# ssh-add +AC_PATH_PROG([SSH_ADD], [ssh-add], [/usr/bin/ssh-add], [$PATH:/usr/local/sbin:/usr/sbin:/sbin]) +AC_DEFINE_UNQUOTED([PATH_SSH_ADD], ["$SSH_ADD"], [Location of ssh-add binary]) + +# ssh-agent +AC_PATH_PROG([SSH_AGENT], [ssh-agent], [/usr/bin/ssh-agent], [$PATH:/usr/local/bin:/usr/bin:/bin]) +AC_DEFINE_UNQUOTED([PATH_SSH_AGENT], ["$SSH_AGENT"], [Location of ssh-agent binary]) +# Config AC_DEFINE_UNQUOTED([COCKPIT_CONFIG_FILE],["$sysconfdir/cockpit/cockpit.conf"],[Configuration file]) changequote(,)dnl @@ -478,6 +485,8 @@ echo " Supports key auth: ${key_auth} pkexec: ${PKEXEC} + ssh-add: ${SSH_ADD} + ssh-agent: ${SSH_AGENT} sudo: ${SUDO} usermod: ${USERMOD} " diff --git a/src/pam-ssh-add/pam-ssh-add.c b/src/pam-ssh-add/pam-ssh-add.c index 51d6d5803a2..0c276c5c098 100644 --- a/src/pam-ssh-add/pam-ssh-add.c +++ b/src/pam-ssh-add/pam-ssh-add.c @@ -47,10 +47,10 @@ #include "pam-ssh-add.h" /* programs that can be overwidden in tests */ -const char *pam_ssh_agent_program = "/usr/bin/ssh-agent"; +const char *pam_ssh_agent_program = PATH_SSH_AGENT; const char *pam_ssh_agent_arg = NULL; -const char *pam_ssh_add_program = "/usr/bin/ssh-add"; +const char *pam_ssh_add_program = PATH_SSH_ADD; const char *pam_ssh_add_arg = NULL; /* Environment */ From 916d026378af93cd9f2383fa4d992fdebd125956 Mon Sep 17 00:00:00 2001 From: petervo Date: Tue, 25 Aug 2015 09:52:11 -0700 Subject: [PATCH 8/8] pam-ssh-add: Remove tmp dir permissions --- src/selinux/cockpit.te | 1 - 1 file changed, 1 deletion(-) diff --git a/src/selinux/cockpit.te b/src/selinux/cockpit.te index f9dc216dd46..07ad7994ebb 100644 --- a/src/selinux/cockpit.te +++ b/src/selinux/cockpit.te @@ -88,7 +88,6 @@ allow cockpit_session_t init_t:unix_stream_socket sendto; # cockpit-session does these when authenticating via GSSAPI allow cockpit_session_t tmp_t:file { create write open unlink }; -allow cockpit_session_t tmp_t:dir { create rmdir }; # cockpit-session can execute cockpit-bridge as the user userdom_spec_domtrans_all_users(cockpit_session_t)