/*
* Run a PAM interaction script for testing.
*
* Provides an interface that loads a PAM interaction script from a file and
* runs through that script, calling the internal PAM module functions and
* checking their results. This allows automation of PAM testing through
* external data files instead of coding everything in C.
*
* The canonical version of this file is maintained in the rra-c-util package,
* which can be found at .
*
* Written by Russ Allbery
* Copyright 2017-2018, 2020 Russ Allbery
* Copyright 2011-2012, 2014
* The Board of Trustees of the Leland Stanford Junior University
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* SPDX-License-Identifier: MIT
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* Used for enumerating arrays. */
#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
/* Mapping of strings to PAM function pointers and group numbers. */
static const struct {
const char *name;
pam_call call;
enum group_type group;
} CALLS[] = {
/* clang-format off */
{"acct_mgmt", pam_sm_acct_mgmt, GROUP_ACCOUNT },
{"authenticate", pam_sm_authenticate, GROUP_AUTH },
{"setcred", pam_sm_setcred, GROUP_AUTH },
{"chauthtok", pam_sm_chauthtok, GROUP_PASSWORD},
{"open_session", pam_sm_open_session, GROUP_SESSION },
{"close_session", pam_sm_close_session, GROUP_SESSION },
/* clang-format on */
};
/* Mapping of PAM flag names without the leading PAM_ to values. */
static const struct {
const char *name;
int value;
} FLAGS[] = {
/* clang-format off */
{"CHANGE_EXPIRED_AUTHTOK", PAM_CHANGE_EXPIRED_AUTHTOK},
{"DELETE_CRED", PAM_DELETE_CRED },
{"DISALLOW_NULL_AUTHTOK", PAM_DISALLOW_NULL_AUTHTOK },
{"ESTABLISH_CRED", PAM_ESTABLISH_CRED },
{"PRELIM_CHECK", PAM_PRELIM_CHECK },
{"REFRESH_CRED", PAM_REFRESH_CRED },
{"REINITIALIZE_CRED", PAM_REINITIALIZE_CRED },
{"SILENT", PAM_SILENT },
{"UPDATE_AUTHTOK", PAM_UPDATE_AUTHTOK },
/* clang-format on */
};
/* Mapping of strings to PAM groups. */
static const struct {
const char *name;
enum group_type group;
} GROUPS[] = {
/* clang-format off */
{"account", GROUP_ACCOUNT },
{"auth", GROUP_AUTH },
{"password", GROUP_PASSWORD},
{"session", GROUP_SESSION },
/* clang-format on */
};
/* Mapping of strings to PAM return values. */
static const struct {
const char *name;
int status;
} RETURNS[] = {
/* clang-format off */
{"PAM_AUTH_ERR", PAM_AUTH_ERR },
{"PAM_AUTHINFO_UNAVAIL", PAM_AUTHINFO_UNAVAIL},
{"PAM_AUTHTOK_ERR", PAM_AUTHTOK_ERR },
{"PAM_DATA_SILENT", PAM_DATA_SILENT },
{"PAM_IGNORE", PAM_IGNORE },
{"PAM_NEW_AUTHTOK_REQD", PAM_NEW_AUTHTOK_REQD},
{"PAM_SESSION_ERR", PAM_SESSION_ERR },
{"PAM_SUCCESS", PAM_SUCCESS },
{"PAM_USER_UNKNOWN", PAM_USER_UNKNOWN },
/* clang-format on */
};
/* Mapping of PAM prompt styles to their values. */
static const struct {
const char *name;
int style;
} STYLES[] = {
/* clang-format off */
{"echo_off", PAM_PROMPT_ECHO_OFF},
{"echo_on", PAM_PROMPT_ECHO_ON },
{"error_msg", PAM_ERROR_MSG },
{"info", PAM_TEXT_INFO },
/* clang-format on */
};
/* Mappings of strings to syslog priorities. */
static const struct {
const char *name;
int priority;
} PRIORITIES[] = {
/* clang-format off */
{"DEBUG", LOG_DEBUG },
{"INFO", LOG_INFO },
{"NOTICE", LOG_NOTICE},
{"ERR", LOG_ERR },
{"CRIT", LOG_CRIT },
/* clang-format on */
};
/*
* Given a pointer to a string, skip any leading whitespace and return a
* pointer to the first non-whitespace character.
*/
static char *
skip_whitespace(char *p)
{
while (isspace((unsigned char) (*p)))
p++;
return p;
}
/*
* Read a line from a file into a BUFSIZ buffer, failing if the line was too
* long to fit into the buffer, and returns a copy of that line in newly
* allocated memory. Ignores blank lines and comments. Caller is responsible
* for freeing. Returns NULL on end of file and fails on read errors.
*/
static char *
readline(FILE *file)
{
char buffer[BUFSIZ];
char *line, *first;
do {
line = fgets(buffer, sizeof(buffer), file);
if (line == NULL) {
if (feof(file))
return NULL;
sysbail("cannot read line from script");
}
if (buffer[strlen(buffer) - 1] != '\n')
bail("script line too long");
buffer[strlen(buffer) - 1] = '\0';
first = skip_whitespace(buffer);
} while (first[0] == '#' || first[0] == '\0');
line = bstrdup(buffer);
return line;
}
/*
* Given the name of a PAM call, map it to a call enum. This is used later in
* switch statements to determine which function to call. Fails on any
* unrecognized string. If the optional second argument is not NULL, also
* store the group number in that argument.
*/
static pam_call
string_to_call(const char *name, enum group_type *group)
{
size_t i;
for (i = 0; i < ARRAY_SIZE(CALLS); i++)
if (strcmp(name, CALLS[i].name) == 0) {
if (group != NULL)
*group = CALLS[i].group;
return CALLS[i].call;
}
bail("unrecognized PAM call %s", name);
}
/*
* Given a PAM flag value without the leading PAM_, map it to the numeric
* value of that flag. Fails on any unrecognized string.
*/
static int
string_to_flag(const char *name)
{
size_t i;
for (i = 0; i < ARRAY_SIZE(FLAGS); i++)
if (strcmp(name, FLAGS[i].name) == 0)
return FLAGS[i].value;
bail("unrecognized PAM flag %s", name);
}
/*
* Given a PAM group name, map it to the array index for the options array for
* that group. Fails on any unrecognized string.
*/
static enum group_type
string_to_group(const char *name)
{
size_t i;
for (i = 0; i < ARRAY_SIZE(GROUPS); i++)
if (strcmp(name, GROUPS[i].name) == 0)
return GROUPS[i].group;
bail("unrecognized PAM group %s", name);
}
/*
* Given a syslog priority name, map it to the numeric value of that priority.
* Fails on any unrecognized string.
*/
static int
string_to_priority(const char *name)
{
size_t i;
for (i = 0; i < ARRAY_SIZE(PRIORITIES); i++)
if (strcmp(name, PRIORITIES[i].name) == 0)
return PRIORITIES[i].priority;
bail("unrecognized syslog priority %s", name);
}
/*
* Given a PAM return status, map it to the actual expected value. Fails on
* any unrecognized string.
*/
static int
string_to_status(const char *name)
{
size_t i;
if (name == NULL)
bail("no PAM status on line");
for (i = 0; i < ARRAY_SIZE(RETURNS); i++)
if (strcmp(name, RETURNS[i].name) == 0)
return RETURNS[i].status;
bail("unrecognized PAM status %s", name);
}
/*
* Given a PAM prompt style value without the leading PAM_PROMPT_, map it to
* the numeric value of that flag. Fails on any unrecognized string.
*/
static int
string_to_style(const char *name)
{
size_t i;
for (i = 0; i < ARRAY_SIZE(STYLES); i++)
if (strcmp(name, STYLES[i].name) == 0)
return STYLES[i].style;
bail("unrecognized PAM prompt style %s", name);
}
/*
* We found a section delimiter while parsing another section. Rewind our
* input file back before the section delimiter so that we'll read it again.
* Takes the length of the line we read, which is used to determine how far to
* rewind.
*/
static void
rewind_section(FILE *script, size_t length)
{
if (fseek(script, -length - 1, SEEK_CUR) != 0)
sysbail("cannot rewind file");
}
/*
* Given a string that may contain %-escapes, expand it into the resulting
* value. The following escapes are supported:
*
* %i current UID (not target user UID)
* %n new password
* %p password
* %u username
* %0 user-supplied string
* ...
* %9 user-supplied string
*
* The %* escape is preserved as-is, as it has to be interpreted at the time
* of checking output. Returns the expanded string in newly-allocated memory.
*/
static char *
expand_string(const char *template, const struct script_config *config)
{
size_t length = 0;
const char *p, *extra;
char *output, *out;
char *uid = NULL;
length = 0;
for (p = template; *p != '\0'; p++) {
if (*p != '%')
length++;
else {
p++;
switch (*p) {
case 'i':
if (uid == NULL)
basprintf(&uid, "%lu", (unsigned long) getuid());
length += strlen(uid);
break;
case 'n':
if (config->newpass == NULL)
bail("new password not set");
length += strlen(config->newpass);
break;
case 'p':
if (config->password == NULL)
bail("password not set");
length += strlen(config->password);
break;
case 'u':
length += strlen(config->user);
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
if (config->extra[*p - '0'] == NULL)
bail("extra script parameter %%%c not set", *p);
length += strlen(config->extra[*p - '0']);
break;
case '*':
length += 2;
break;
default:
length++;
break;
}
}
}
output = bmalloc(length + 1);
for (p = template, out = output; *p != '\0'; p++) {
if (*p != '%')
*out++ = *p;
else {
p++;
switch (*p) {
case 'i':
assert(uid != NULL);
memcpy(out, uid, strlen(uid));
out += strlen(uid);
break;
case 'n':
memcpy(out, config->newpass, strlen(config->newpass));
out += strlen(config->newpass);
break;
case 'p':
memcpy(out, config->password, strlen(config->password));
out += strlen(config->password);
break;
case 'u':
memcpy(out, config->user, strlen(config->user));
out += strlen(config->user);
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
extra = config->extra[*p - '0'];
memcpy(out, extra, strlen(extra));
out += strlen(extra);
break;
case '*':
*out++ = '%';
*out++ = '*';
break;
default:
*out++ = *p;
break;
}
}
}
*out = '\0';
free(uid);
return output;
}
/*
* Given a whitespace-delimited string of PAM options, split it into an argv
* array and argc count and store it in the provided option struct.
*/
static void
split_options(char *string, struct options *options,
const struct script_config *config)
{
char *opt;
size_t size, count;
for (opt = strtok(string, " "); opt != NULL; opt = strtok(NULL, " ")) {
if (options->argv == NULL) {
options->argv = bcalloc(2, sizeof(const char *));
options->argv[0] = expand_string(opt, config);
options->argc = 1;
} else {
count = (options->argc + 2);
size = sizeof(const char *);
options->argv = breallocarray(options->argv, count, size);
options->argv[options->argc] = expand_string(opt, config);
options->argv[options->argc + 1] = NULL;
options->argc++;
}
}
}
/*
* Parse the options section of a PAM script. This consists of one or more
* lines in the format:
*
* =
*
* where options are either option names or option=value pairs, where the
* value may not contain whitespace. Returns an options struct, which stores
* argc and argv values for each group.
*
* Takes the work struct as an argument and puts values into its array.
*/
static void
parse_options(FILE *script, struct work *work,
const struct script_config *config)
{
char *line, *group, *token;
size_t length = 0;
enum group_type type;
for (line = readline(script); line != NULL; line = readline(script)) {
length = strlen(line);
group = strtok(line, " ");
if (group == NULL)
bail("malformed script line");
if (group[0] == '[')
break;
type = string_to_group(group);
token = strtok(NULL, " ");
if (token == NULL)
bail("malformed action line");
if (strcmp(token, "=") != 0)
bail("malformed action line near %s", token);
token = strtok(NULL, "");
split_options(token, &work->options[type], config);
free(line);
}
if (line != NULL) {
free(line);
rewind_section(script, length);
}
}
/*
* Parse the call portion of a PAM call in the run section of a PAM script.
* This handles parsing the PAM flags that optionally may be given as part of
* the call. Takes the token representing the call and a pointer to the
* action struct to fill in with the call and the option flags.
*/
static void
parse_call(char *token, struct action *action)
{
char *flags, *flag;
action->flags = 0;
flags = strchr(token, '(');
if (flags != NULL) {
*flags = '\0';
flags++;
for (flag = strtok(flags, "|,)"); flag != NULL;
flag = strtok(NULL, "|,)")) {
action->flags |= string_to_flag(flag);
}
}
action->call = string_to_call(token, &action->group);
}
/*
* Parse the run section of a PAM script. This consists of one or more lines
* in the format:
*
* =
*
* where is a PAM call and is what it should return. Returns
* a linked list of actions. Fails on any error in parsing.
*/
static struct action *
parse_run(FILE *script)
{
struct action *head = NULL, *current = NULL, *next;
char *line, *token, *call;
size_t length = 0;
for (line = readline(script); line != NULL; line = readline(script)) {
length = strlen(line);
token = strtok(line, " ");
if (token[0] == '[')
break;
next = bmalloc(sizeof(struct action));
next->next = NULL;
if (head == NULL)
head = next;
else
current->next = next;
next->name = bstrdup(token);
call = token;
token = strtok(NULL, " ");
if (token == NULL)
bail("malformed action line");
if (strcmp(token, "=") != 0)
bail("malformed action line near %s", token);
token = strtok(NULL, " ");
next->status = string_to_status(token);
parse_call(call, next);
free(line);
current = next;
}
if (head == NULL)
bail("empty run section in script");
if (line != NULL) {
free(line);
rewind_section(script, length);
}
return head;
}
/*
* Parse the end section of a PAM script. There is one supported line in the
* format:
*
* flags = |
*
* where is a flag to pass to pam_end. Returns the flags.
*/
static int
parse_end(FILE *script)
{
char *line, *token, *flag;
size_t length = 0;
int flags = PAM_SUCCESS;
for (line = readline(script); line != NULL; line = readline(script)) {
length = strlen(line);
token = strtok(line, " ");
if (token[0] == '[')
break;
if (strcmp(token, "flags") != 0)
bail("unknown end setting %s", token);
token = strtok(NULL, " ");
if (token == NULL)
bail("malformed end line");
if (strcmp(token, "=") != 0)
bail("malformed end line near %s", token);
token = strtok(NULL, " ");
flag = strtok(token, "|");
while (flag != NULL) {
flags |= string_to_status(flag);
flag = strtok(NULL, "|");
}
free(line);
}
if (line != NULL) {
free(line);
rewind_section(script, length);
}
return flags;
}
/*
* Parse the output section of a PAM script. This consists of zero or more
* lines in the format:
*
* PRIORITY some output information
* PRIORITY /output regex/
*
* where PRIORITY is replaced by the numeric syslog priority corresponding to
* that priority and the rest of the output undergoes %-esacape expansion.
* Returns the accumulated output as a vector.
*/
static struct output *
parse_output(FILE *script, const struct script_config *config)
{
char *line, *token, *message;
struct output *output;
int priority;
output = output_new();
if (output == NULL)
sysbail("cannot allocate vector");
for (line = readline(script); line != NULL; line = readline(script)) {
token = strtok(line, " ");
priority = string_to_priority(token);
token = strtok(NULL, "");
if (token == NULL)
bail("malformed line %s", line);
message = expand_string(token, config);
output_add(output, priority, message);
free(message);
free(line);
}
return output;
}
/*
* Parse the prompts section of a PAM script. This consists of zero or more
* lines in one of the formats:
*
* type = prompt
* type = /prompt/
* type = prompt|response
* type = /prompt/|response
*
* If the type is error_msg or info, there is no response. Otherwise,
* everything after the last | is taken to be the response that should be
* provided to that prompt. The response undergoes %-escape expansion.
*/
static struct prompts *
parse_prompts(FILE *script, const struct script_config *config)
{
struct prompts *prompts = NULL;
struct prompt *prompt;
char *line, *token, *style, *end;
size_t size, count, i;
size_t length = 0;
for (line = readline(script); line != NULL; line = readline(script)) {
length = strlen(line);
token = strtok(line, " ");
if (token[0] == '[')
break;
if (prompts == NULL) {
prompts = bcalloc(1, sizeof(struct prompts));
prompts->prompts = bcalloc(1, sizeof(struct prompt));
prompts->allocated = 1;
} else if (prompts->allocated == prompts->size) {
count = prompts->allocated * 2;
size = sizeof(struct prompt);
prompts->prompts = breallocarray(prompts->prompts, count, size);
prompts->allocated = count;
for (i = prompts->size; i < prompts->allocated; i++) {
prompts->prompts[i].prompt = NULL;
prompts->prompts[i].response = NULL;
}
}
prompt = &prompts->prompts[prompts->size];
style = token;
token = strtok(NULL, " ");
if (token == NULL)
bail("malformed prompt line");
if (strcmp(token, "=") != 0)
bail("malformed prompt line near %s", token);
prompt->style = string_to_style(style);
token = strtok(NULL, "");
if (prompt->style == PAM_ERROR_MSG || prompt->style == PAM_TEXT_INFO)
prompt->prompt = expand_string(token, config);
else {
end = strrchr(token, '|');
if (end == NULL)
bail("malformed prompt line near %s", token);
*end = '\0';
prompt->prompt = expand_string(token, config);
token = end + 1;
prompt->response = expand_string(token, config);
}
prompts->size++;
free(line);
}
if (line != NULL) {
free(line);
rewind_section(script, length);
}
return prompts;
}
/*
* Parse a PAM interaction script. This handles parsing of the top-level
* section markers and dispatches the parsing to other functions. Returns the
* total work to do as a work struct.
*/
struct work *
parse_script(FILE *script, const struct script_config *config)
{
struct work *work;
char *line, *token;
work = bmalloc(sizeof(struct work));
memset(work, 0, sizeof(struct work));
work->end_flags = PAM_SUCCESS;
for (line = readline(script); line != NULL; line = readline(script)) {
token = strtok(line, " ");
if (token[0] != '[')
bail("line outside of section: %s", line);
if (strcmp(token, "[options]") == 0)
parse_options(script, work, config);
else if (strcmp(token, "[run]") == 0)
work->actions = parse_run(script);
else if (strcmp(token, "[end]") == 0)
work->end_flags = parse_end(script);
else if (strcmp(token, "[output]") == 0)
work->output = parse_output(script, config);
else if (strcmp(token, "[prompts]") == 0)
work->prompts = parse_prompts(script, config);
else
bail("unknown section: %s", token);
free(line);
}
if (work->actions == NULL)
bail("no run section defined");
return work;
}