/*
* 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 2016, 2018, 2020-2021 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
#ifdef HAVE_REGCOMP
# include
#endif
#include
#include
#include
#include
#include
#include
#include
/*
* Compare a regex to a string. If regular expression support isn't
* available, we skip this test.
*/
#ifdef HAVE_REGCOMP
static void __attribute__((__format__(printf, 3, 4)))
like(const char *wanted, const char *seen, const char *format, ...)
{
va_list args;
regex_t regex;
char err[BUFSIZ];
int status;
if (seen == NULL) {
fflush(stderr);
printf("# wanted: /%s/\n# seen: (null)\n", wanted);
va_start(args, format);
okv(0, format, args);
va_end(args);
return;
}
memset(®ex, 0, sizeof(regex));
status = regcomp(®ex, wanted, REG_EXTENDED | REG_NOSUB);
if (status != 0) {
regerror(status, ®ex, err, sizeof(err));
bail("invalid regex /%s/: %s", wanted, err);
}
status = regexec(®ex, seen, 0, NULL, 0);
switch (status) {
case 0:
va_start(args, format);
okv(1, format, args);
va_end(args);
break;
case REG_NOMATCH:
printf("# wanted: /%s/\n# seen: %s\n", wanted, seen);
va_start(args, format);
okv(0, format, args);
va_end(args);
break;
default:
regerror(status, ®ex, err, sizeof(err));
bail("regexec failed for regex /%s/: %s", wanted, err);
}
regfree(®ex);
}
#else /* !HAVE_REGCOMP */
static void
like(const char *wanted, const char *seen, const char *format UNUSED, ...)
{
diag("wanted /%s/", wanted);
diag(" seen %s", seen);
skip("regex support not available");
}
#endif /* !HAVE_REGCOMP */
/*
* Compare an expected string with a seen string, used by both output checking
* and prompt checking. This is a separate function because the expected
* string may be a regex, determined by seeing if it starts and ends with a
* slash (/), which may require a regex comparison.
*
* Eventually calls either is_string or ok to report results via TAP.
*/
static void __attribute__((__format__(printf, 3, 4)))
compare_string(char *wanted, char *seen, const char *format, ...)
{
va_list args;
char *comment, *regex;
size_t length;
/* Format the comment since we need it regardless. */
va_start(args, format);
bvasprintf(&comment, format, args);
va_end(args);
/* Check whether the wanted string is a regex. */
length = strlen(wanted);
if (wanted[0] == '/' && wanted[length - 1] == '/') {
regex = bstrndup(wanted + 1, length - 2);
like(regex, seen, "%s", comment);
free(regex);
} else {
is_string(wanted, seen, "%s", comment);
}
free(comment);
}
/*
* The PAM conversation function. Takes the prompts struct from the
* configuration and interacts appropriately. If a prompt is of the expected
* type but not the expected string, it still responds; if it's not of the
* expected type, it returns PAM_CONV_ERR.
*
* Currently only handles a single prompt at a time.
*/
static int
converse(int num_msg, const struct pam_message **msg,
struct pam_response **resp, void *appdata_ptr)
{
struct prompts *prompts = appdata_ptr;
struct prompt *prompt;
char *message;
size_t length;
int i;
*resp = bcalloc(num_msg, sizeof(struct pam_response));
for (i = 0; i < num_msg; i++) {
message = bstrdup(msg[i]->msg);
/* Remove newlines for comparison purposes. */
length = strlen(message);
while (length > 0 && message[length - 1] == '\n')
message[length-- - 1] = '\0';
/* Check if we've gotten too many prompts but quietly ignore them. */
if (prompts->current >= prompts->size) {
diag("unexpected prompt: %s", message);
free(message);
ok(0, "more prompts than expected");
continue;
}
/* Be sure everything matches and return the response, if any. */
prompt = &prompts->prompts[prompts->current];
is_int(prompt->style, msg[i]->msg_style, "style of prompt %lu",
(unsigned long) prompts->current + 1);
compare_string(prompt->prompt, message, "value of prompt %lu",
(unsigned long) prompts->current + 1);
free(message);
prompts->current++;
if (prompt->style == msg[i]->msg_style && prompt->response != NULL) {
(*resp)[i].resp = bstrdup(prompt->response);
(*resp)[i].resp_retcode = 0;
}
}
/*
* Always return success even if the prompts don't match. Otherwise,
* we're likely to abort the conversation in the middle and possibly
* leave passwords set incorrectly.
*/
return PAM_SUCCESS;
}
/*
* Check the actual PAM output against the expected output. We divide the
* expected and seen output into separate lines and compare each one so that
* we can handle regular expressions and the output priority.
*/
static void
check_output(const struct output *wanted, const struct output *seen)
{
size_t i;
if (wanted == NULL && seen == NULL)
ok(1, "no output");
else if (wanted == NULL) {
for (i = 0; i < seen->count; i++)
diag("unexpected: (%d) %s", seen->lines[i].priority,
seen->lines[i].line);
ok(0, "no output");
} else if (seen == NULL) {
for (i = 0; i < wanted->count; i++) {
is_int(wanted->lines[i].priority, 0, "output priority %lu",
(unsigned long) i + 1);
is_string(wanted->lines[i].line, NULL, "output line %lu",
(unsigned long) i + 1);
}
} else {
for (i = 0; i < wanted->count && i < seen->count; i++) {
is_int(wanted->lines[i].priority, seen->lines[i].priority,
"output priority %lu", (unsigned long) i + 1);
compare_string(wanted->lines[i].line, seen->lines[i].line,
"output line %lu", (unsigned long) i + 1);
}
if (wanted->count > seen->count)
for (i = seen->count; i < wanted->count; i++) {
is_int(wanted->lines[i].priority, 0, "output priority %lu",
(unsigned long) i + 1);
is_string(wanted->lines[i].line, NULL, "output line %lu",
(unsigned long) i + 1);
}
if (seen->count > wanted->count) {
for (i = wanted->count; i < seen->count; i++)
diag("unexpected: (%d) %s", seen->lines[i].priority,
seen->lines[i].line);
ok(0, "unexpected output lines");
} else {
ok(1, "no excess output");
}
}
}
/*
* The core of the work. Given the path to a PAM interaction script, which
* may be relative to C_TAP_SOURCE or C_TAP_BUILD, the user (may be NULL), and
* the stored password (may be NULL), run that script, outputting the results
* in TAP format.
*/
void
run_script(const char *file, const struct script_config *config)
{
char *path;
struct output *output;
FILE *script;
struct work *work;
struct options *opts;
struct action *action, *oaction;
struct pam_conv conv = {NULL, NULL};
pam_handle_t *pamh;
int status;
size_t i, j;
const char *argv_empty[] = {NULL};
/* Open and parse the script. */
if (access(file, R_OK) == 0)
path = bstrdup(file);
else {
path = test_file_path(file);
if (path == NULL)
bail("cannot find PAM script %s", file);
}
script = fopen(path, "r");
if (script == NULL)
sysbail("cannot open %s", path);
work = parse_script(script, config);
fclose(script);
diag("Starting %s", file);
if (work->prompts != NULL) {
conv.conv = converse;
conv.appdata_ptr = work->prompts;
}
/* Initialize PAM. */
status = pam_start("test", config->user, &conv, &pamh);
if (status != PAM_SUCCESS)
sysbail("cannot create PAM handle");
if (config->authtok != NULL)
pamh->authtok = bstrdup(config->authtok);
if (config->oldauthtok != NULL)
pamh->oldauthtok = bstrdup(config->oldauthtok);
/* Run the actions and check their return status. */
for (action = work->actions; action != NULL; action = action->next) {
if (work->options[action->group].argv == NULL)
status = (*action->call)(pamh, action->flags, 0, argv_empty);
else {
opts = &work->options[action->group];
status = (*action->call)(pamh, action->flags, opts->argc,
(const char **) opts->argv);
}
is_int(action->status, status, "status for %s", action->name);
}
output = pam_output();
check_output(work->output, output);
pam_output_free(output);
/* If we have a test callback, call it now. */
if (config->callback != NULL)
config->callback(pamh, config, config->data);
/* Free memory and return. */
pam_end(pamh, work->end_flags);
action = work->actions;
while (action != NULL) {
free(action->name);
oaction = action;
action = action->next;
free(oaction);
}
for (i = 0; i < ARRAY_SIZE(work->options); i++)
if (work->options[i].argv != NULL) {
for (j = 0; work->options[i].argv[j] != NULL; j++)
free(work->options[i].argv[j]);
free(work->options[i].argv);
}
if (work->output)
pam_output_free(work->output);
if (work->prompts != NULL) {
for (i = 0; i < work->prompts->size; i++) {
free(work->prompts->prompts[i].prompt);
free(work->prompts->prompts[i].response);
}
free(work->prompts->prompts);
free(work->prompts);
}
free(work);
free(path);
}
/*
* Check a filename for acceptable characters. Returns true if the file
* consists solely of [a-zA-Z0-9-] and false otherwise.
*/
static bool
valid_filename(const char *filename)
{
const char *p;
for (p = filename; *p != '\0'; p++) {
if (*p >= 'A' && *p <= 'Z')
continue;
if (*p >= 'a' && *p <= 'z')
continue;
if (*p >= '0' && *p <= '9')
continue;
if (*p == '-')
continue;
return false;
}
return true;
}
/*
* The same as run_script, but run every script found in the given directory,
* skipping file names that contain characters other than alphanumerics and -.
*/
void
run_script_dir(const char *dir, const struct script_config *config)
{
DIR *handle;
struct dirent *entry;
const char *path;
char *file;
if (access(dir, R_OK) == 0)
path = dir;
else
path = test_file_path(dir);
handle = opendir(path);
if (handle == NULL)
sysbail("cannot open directory %s", dir);
errno = 0;
while ((entry = readdir(handle)) != NULL) {
if (!valid_filename(entry->d_name))
continue;
basprintf(&file, "%s/%s", path, entry->d_name);
run_script(file, config);
free(file);
errno = 0;
}
if (errno != 0)
sysbail("cannot read directory %s", dir);
closedir(handle);
if (path != dir)
test_file_path_free((char *) path);
}