* Copyright (C) 2015 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
* Inspired by gnome-keyring:
* Stef Walter <stef@memberwebs.com>
*/
#include "config.h"
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <assert.h>
#include <err.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <syslog.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <security/pam_modules.h>
#include <security/pam_modutil.h>
#include "pam-ssh-add.h"
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;
static unsigned long ssh_agent_pid;
static uid_t ssh_agent_uid;
#define ENVIRON_SIZE 5
#define PATH "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
static const char *agent_vars[] = {
"SSH_AUTH_SOCK",
"SSH_AGENT_PID",
NULL
};
#define STDIN 0
#define STDOUT 1
#define STDERR 2
#define READ_END 0
#define WRITE_END 1
#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 = NULL;
int res;
if (!pam_ssh_add_log_handler)
pam_ssh_add_log_handler = &default_logger;
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);
if (res >= 0)
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);
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 char *
read_string (int fd,
int consume)
{
#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 *override)
{
if (override)
return override;
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 (pam_handle_t *pamh,
const char **args,
char **env,
struct passwd *pwd,
int inp[2],
int outp[2],
int errp[2])
{
assert (pwd);
assert (pwd->pw_dir);
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);
}
pam_modutil_sanitize_helper_fds (pamh,
PAM_MODUTIL_IGNORE_FD,
PAM_MODUTIL_IGNORE_FD,
PAM_MODUTIL_IGNORE_FD);
close (inp[READ_END]);
close (inp[WRITE_END]);
close (outp[READ_END]);
close (outp[WRITE_END]);
close (errp[READ_END]);
close (errp[WRITE_END]);
if (setsid() < 0)
{
error ("failed to detach child process");
exit (EXIT_FAILURE);
}
if (setegid (getgid ()) < 0 || seteuid (getuid ()) < 0)
error ("failed to restore credentials");
if ((getegid() != pwd->pw_gid ? initgroups(pwd->pw_name, pwd->pw_gid) < 0 : 0) ||
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);
}
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)
{
sigaction (SIGCHLD, oldsact, NULL);
sigaction (SIGPIPE, oldpipe, NULL);
}
static pid_t
run_as_user (pam_handle_t *pamh,
const char **args,
char **env,
struct passwd *pwd,
int inp[2],
int outp[2],
int errp[2])
{
pid_t pid = -1;
switch (pid = fork ())
{
case -1:
error ("couldn't fork: %m");
goto done;
case 0:
setup_child (pamh, args, env, pwd, inp, outp, errp);
break;
default:
break;
};
done:
return pid;
}
static int
get_environ_vars_from_agent (char *line,
void *arg)
{
* ssh-agent outputs commands for exporting it's environment
* 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 (pam_handle_t *pamh,
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;
if (pipe (inp) < 0 || pipe (outp) < 0 || pipe (errp) < 0)
{
error ("couldn't create pipes: %m");
goto done;
}
pid = run_as_user (pamh, args, env, pwd,
inp, outp, errp);
if (pid < 1)
goto done;
close (inp[READ_END]);
close (outp[WRITE_END]);
close (errp[WRITE_END]);
inp[READ_END] = outp[WRITE_END] = errp[WRITE_END] = -1;
for (;;)
{
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);
}
while (waitid (P_PID, pid, &result, WEXITED) < 0)
{
if (errno != EINTR)
{
error ("couldn't wait on ssh-add process: %m");
goto done;
}
}
success = result.si_code == CLD_EXITED && result.si_status == 0;
if (!success)
{
if (result.si_code == CLD_EXITED && result.si_status == 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 (pam_handle_t *pamh,
struct passwd *pwd,
const char *xdg_runtime_override,
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_override);
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);
if (pipe (inp) < 0 || pipe (outp) < 0 || pipe (errp) < 0)
{
error ("couldn't create pipes: %m");
goto done;
}
pid = run_as_user (pamh, args, env, pwd,
inp, outp, errp);
if (pid < 1)
goto done;
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;
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;
}
while (waitid (P_PID, pid, &result, WEXITED) < 0)
{
if (errno != EINTR)
{
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);
if (!success)
{
error ("Failed to start ssh-agent");
}
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);
* 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;
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;
len = strlen (password);
memset (password, 0xAA, len);
memset (password, 0xBB, len);
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 char *
strdupx (const char *string)
{
char *copy = strdup (string);
if (copy != NULL)
return copy;
warn ("failed to allocate memory for strdup");
abort ();
}
static int
stash_password_for_session (pam_handle_t *pamh,
const char *password)
{
char *password_copy = strdupx (password);
if (pam_set_data (pamh, STORED_AUTHTOK, password_copy,
cleanup_free_password) != PAM_SUCCESS)
{
free_password (password_copy);
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 (pamh, auth_pwd,
pam_getenv (pamh, "XDG_RUNTIME_DIR"),
&auth_socket,
&auth_pid);
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));
}
if (strncmp (auth_pid, "SSH_AGENT_PID=", 14) == 0)
{
char *endptr;
unsigned long pid;
errno = 0;
pid = strtoul (auth_pid + 14, &endptr, 10);
if (pid > 0 && *endptr == '\0' && !(pid == ULONG_MAX && errno == ERANGE))
{
ssh_agent_pid = pid;
ssh_agent_uid = auth_pwd->pw_uid;
}
else
{
error ("invalid SSH_AGENT_PID value: %s", auth_pid);
}
}
else
{
error ("unexpected agent pid format: %s", auth_pid);
}
}
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;
if (pam_get_data (pamh, STORED_AUTHTOK,
(const void**)&password) != PAM_SUCCESS)
{
password = NULL;
}
success = pam_ssh_add_load (pamh, 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);
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:
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[])
{
parse_args (argc, argv);
if (ssh_agent_pid > 0)
{
debug ("Closing %lu", ssh_agent_pid);
if (setresuid (ssh_agent_uid, ssh_agent_uid, -1) < 0)
{
error ("could not drop privileges for killing ssh agent: %m");
return PAM_SESSION_ERR;
}
if (kill (ssh_agent_pid, SIGTERM) < 0 && errno != ESRCH)
message ("could not kill ssh agent %lu: %m", ssh_agent_pid);
if (setresuid (0, 0, -1) < 0)
{
error ("could not restore privileges after killing ssh agent: %m");
return PAM_SESSION_ERR;
}
}
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);
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);
return PAM_CRED_INSUFFICIENT;
}
PAM_EXTERN int
pam_sm_setcred (pam_handle_t *pamh,
int flags,
int argc,
const char *argv[])
{
return PAM_SUCCESS;
}