/*
* 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 = PATH_SSH_AGENT;
const char *pam_ssh_agent_arg = NULL;
const char *pam_ssh_add_program = PATH_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 (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
#define BLOCK 256
char *ret = NULL;
int r, len = 0;
for (;;)
{
char *n = realloc (ret, len + BLOCK);
if (!n)
{
free (ret);
errno = ENOMEM;
return NULL;
}
memset (n + len, 0, BLOCK);
ret = n;
r = read (fd, ret + len, BLOCK-1);
if (r < 0)
{
if (errno == EAGAIN || errno == EINTR)
continue;
free (ret);
return NULL;
}
else
{
len = len + 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;
}