1 /*
2 * Run a PAM interaction script for testing.
3 *
4 * Provides an interface that loads a PAM interaction script from a file and
5 * runs through that script, calling the internal PAM module functions and
6 * checking their results. This allows automation of PAM testing through
7 * external data files instead of coding everything in C.
8 *
9 * The canonical version of this file is maintained in the rra-c-util package,
10 * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
11 *
12 * Written by Russ Allbery <eagle@eyrie.org>
13 * Copyright 2016, 2018, 2020-2021 Russ Allbery <eagle@eyrie.org>
14 * Copyright 2011-2012, 2014
15 * The Board of Trustees of the Leland Stanford Junior University
16 *
17 * Permission is hereby granted, free of charge, to any person obtaining a
18 * copy of this software and associated documentation files (the "Software"),
19 * to deal in the Software without restriction, including without limitation
20 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
21 * and/or sell copies of the Software, and to permit persons to whom the
22 * Software is furnished to do so, subject to the following conditions:
23 *
24 * The above copyright notice and this permission notice shall be included in
25 * all copies or substantial portions of the Software.
26 *
27 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
30 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
33 * DEALINGS IN THE SOFTWARE.
34 *
35 * SPDX-License-Identifier: MIT
36 */
37
38 #include <config.h>
39 #include <portable/pam.h>
40 #include <portable/system.h>
41
42 #include <ctype.h>
43 #include <dirent.h>
44 #include <errno.h>
45 #ifdef HAVE_REGCOMP
46 # include <regex.h>
47 #endif
48 #include <syslog.h>
49
50 #include <tests/fakepam/internal.h>
51 #include <tests/fakepam/pam.h>
52 #include <tests/fakepam/script.h>
53 #include <tests/tap/basic.h>
54 #include <tests/tap/macros.h>
55 #include <tests/tap/string.h>
56
57
58 /*
59 * Compare a regex to a string. If regular expression support isn't
60 * available, we skip this test.
61 */
62 #ifdef HAVE_REGCOMP
63 static void __attribute__((__format__(printf, 3, 4)))
like(const char * wanted,const char * seen,const char * format,...)64 like(const char *wanted, const char *seen, const char *format, ...)
65 {
66 va_list args;
67 regex_t regex;
68 char err[BUFSIZ];
69 int status;
70
71 if (seen == NULL) {
72 fflush(stderr);
73 printf("# wanted: /%s/\n# seen: (null)\n", wanted);
74 va_start(args, format);
75 okv(0, format, args);
76 va_end(args);
77 return;
78 }
79 memset(®ex, 0, sizeof(regex));
80 status = regcomp(®ex, wanted, REG_EXTENDED | REG_NOSUB);
81 if (status != 0) {
82 regerror(status, ®ex, err, sizeof(err));
83 bail("invalid regex /%s/: %s", wanted, err);
84 }
85 status = regexec(®ex, seen, 0, NULL, 0);
86 switch (status) {
87 case 0:
88 va_start(args, format);
89 okv(1, format, args);
90 va_end(args);
91 break;
92 case REG_NOMATCH:
93 printf("# wanted: /%s/\n# seen: %s\n", wanted, seen);
94 va_start(args, format);
95 okv(0, format, args);
96 va_end(args);
97 break;
98 default:
99 regerror(status, ®ex, err, sizeof(err));
100 bail("regexec failed for regex /%s/: %s", wanted, err);
101 }
102 regfree(®ex);
103 }
104 #else /* !HAVE_REGCOMP */
105 static void
like(const char * wanted,const char * seen,const char * format UNUSED,...)106 like(const char *wanted, const char *seen, const char *format UNUSED, ...)
107 {
108 diag("wanted /%s/", wanted);
109 diag(" seen %s", seen);
110 skip("regex support not available");
111 }
112 #endif /* !HAVE_REGCOMP */
113
114
115 /*
116 * Compare an expected string with a seen string, used by both output checking
117 * and prompt checking. This is a separate function because the expected
118 * string may be a regex, determined by seeing if it starts and ends with a
119 * slash (/), which may require a regex comparison.
120 *
121 * Eventually calls either is_string or ok to report results via TAP.
122 */
123 static void __attribute__((__format__(printf, 3, 4)))
compare_string(char * wanted,char * seen,const char * format,...)124 compare_string(char *wanted, char *seen, const char *format, ...)
125 {
126 va_list args;
127 char *comment, *regex;
128 size_t length;
129
130 /* Format the comment since we need it regardless. */
131 va_start(args, format);
132 bvasprintf(&comment, format, args);
133 va_end(args);
134
135 /* Check whether the wanted string is a regex. */
136 length = strlen(wanted);
137 if (wanted[0] == '/' && wanted[length - 1] == '/') {
138 regex = bstrndup(wanted + 1, length - 2);
139 like(regex, seen, "%s", comment);
140 free(regex);
141 } else {
142 is_string(wanted, seen, "%s", comment);
143 }
144 free(comment);
145 }
146
147
148 /*
149 * The PAM conversation function. Takes the prompts struct from the
150 * configuration and interacts appropriately. If a prompt is of the expected
151 * type but not the expected string, it still responds; if it's not of the
152 * expected type, it returns PAM_CONV_ERR.
153 *
154 * Currently only handles a single prompt at a time.
155 */
156 static int
converse(int num_msg,const struct pam_message ** msg,struct pam_response ** resp,void * appdata_ptr)157 converse(int num_msg, const struct pam_message **msg,
158 struct pam_response **resp, void *appdata_ptr)
159 {
160 struct prompts *prompts = appdata_ptr;
161 struct prompt *prompt;
162 char *message;
163 size_t length;
164 int i;
165
166 *resp = bcalloc(num_msg, sizeof(struct pam_response));
167 for (i = 0; i < num_msg; i++) {
168 message = bstrdup(msg[i]->msg);
169
170 /* Remove newlines for comparison purposes. */
171 length = strlen(message);
172 while (length > 0 && message[length - 1] == '\n')
173 message[length-- - 1] = '\0';
174
175 /* Check if we've gotten too many prompts but quietly ignore them. */
176 if (prompts->current >= prompts->size) {
177 diag("unexpected prompt: %s", message);
178 free(message);
179 ok(0, "more prompts than expected");
180 continue;
181 }
182
183 /* Be sure everything matches and return the response, if any. */
184 prompt = &prompts->prompts[prompts->current];
185 is_int(prompt->style, msg[i]->msg_style, "style of prompt %lu",
186 (unsigned long) prompts->current + 1);
187 compare_string(prompt->prompt, message, "value of prompt %lu",
188 (unsigned long) prompts->current + 1);
189 free(message);
190 prompts->current++;
191 if (prompt->style == msg[i]->msg_style && prompt->response != NULL) {
192 (*resp)[i].resp = bstrdup(prompt->response);
193 (*resp)[i].resp_retcode = 0;
194 }
195 }
196
197 /*
198 * Always return success even if the prompts don't match. Otherwise,
199 * we're likely to abort the conversation in the middle and possibly
200 * leave passwords set incorrectly.
201 */
202 return PAM_SUCCESS;
203 }
204
205
206 /*
207 * Check the actual PAM output against the expected output. We divide the
208 * expected and seen output into separate lines and compare each one so that
209 * we can handle regular expressions and the output priority.
210 */
211 static void
check_output(const struct output * wanted,const struct output * seen)212 check_output(const struct output *wanted, const struct output *seen)
213 {
214 size_t i;
215
216 if (wanted == NULL && seen == NULL)
217 ok(1, "no output");
218 else if (wanted == NULL) {
219 for (i = 0; i < seen->count; i++)
220 diag("unexpected: (%d) %s", seen->lines[i].priority,
221 seen->lines[i].line);
222 ok(0, "no output");
223 } else if (seen == NULL) {
224 for (i = 0; i < wanted->count; i++) {
225 is_int(wanted->lines[i].priority, 0, "output priority %lu",
226 (unsigned long) i + 1);
227 is_string(wanted->lines[i].line, NULL, "output line %lu",
228 (unsigned long) i + 1);
229 }
230 } else {
231 for (i = 0; i < wanted->count && i < seen->count; i++) {
232 is_int(wanted->lines[i].priority, seen->lines[i].priority,
233 "output priority %lu", (unsigned long) i + 1);
234 compare_string(wanted->lines[i].line, seen->lines[i].line,
235 "output line %lu", (unsigned long) i + 1);
236 }
237 if (wanted->count > seen->count)
238 for (i = seen->count; i < wanted->count; i++) {
239 is_int(wanted->lines[i].priority, 0, "output priority %lu",
240 (unsigned long) i + 1);
241 is_string(wanted->lines[i].line, NULL, "output line %lu",
242 (unsigned long) i + 1);
243 }
244 if (seen->count > wanted->count) {
245 for (i = wanted->count; i < seen->count; i++)
246 diag("unexpected: (%d) %s", seen->lines[i].priority,
247 seen->lines[i].line);
248 ok(0, "unexpected output lines");
249 } else {
250 ok(1, "no excess output");
251 }
252 }
253 }
254
255
256 /*
257 * The core of the work. Given the path to a PAM interaction script, which
258 * may be relative to C_TAP_SOURCE or C_TAP_BUILD, the user (may be NULL), and
259 * the stored password (may be NULL), run that script, outputting the results
260 * in TAP format.
261 */
262 void
run_script(const char * file,const struct script_config * config)263 run_script(const char *file, const struct script_config *config)
264 {
265 char *path;
266 struct output *output;
267 FILE *script;
268 struct work *work;
269 struct options *opts;
270 struct action *action, *oaction;
271 struct pam_conv conv = {NULL, NULL};
272 pam_handle_t *pamh;
273 int status;
274 size_t i, j;
275 const char *argv_empty[] = {NULL};
276
277 /* Open and parse the script. */
278 if (access(file, R_OK) == 0)
279 path = bstrdup(file);
280 else {
281 path = test_file_path(file);
282 if (path == NULL)
283 bail("cannot find PAM script %s", file);
284 }
285 script = fopen(path, "r");
286 if (script == NULL)
287 sysbail("cannot open %s", path);
288 work = parse_script(script, config);
289 fclose(script);
290 diag("Starting %s", file);
291 if (work->prompts != NULL) {
292 conv.conv = converse;
293 conv.appdata_ptr = work->prompts;
294 }
295
296 /* Initialize PAM. */
297 status = pam_start("test", config->user, &conv, &pamh);
298 if (status != PAM_SUCCESS)
299 sysbail("cannot create PAM handle");
300 if (config->authtok != NULL)
301 pamh->authtok = bstrdup(config->authtok);
302 if (config->oldauthtok != NULL)
303 pamh->oldauthtok = bstrdup(config->oldauthtok);
304
305 /* Run the actions and check their return status. */
306 for (action = work->actions; action != NULL; action = action->next) {
307 if (work->options[action->group].argv == NULL)
308 status = (*action->call)(pamh, action->flags, 0, argv_empty);
309 else {
310 opts = &work->options[action->group];
311 status = (*action->call)(pamh, action->flags, opts->argc,
312 (const char **) opts->argv);
313 }
314 is_int(action->status, status, "status for %s", action->name);
315 }
316 output = pam_output();
317 check_output(work->output, output);
318 pam_output_free(output);
319
320 /* If we have a test callback, call it now. */
321 if (config->callback != NULL)
322 config->callback(pamh, config, config->data);
323
324 /* Free memory and return. */
325 pam_end(pamh, work->end_flags);
326 action = work->actions;
327 while (action != NULL) {
328 free(action->name);
329 oaction = action;
330 action = action->next;
331 free(oaction);
332 }
333 for (i = 0; i < ARRAY_SIZE(work->options); i++)
334 if (work->options[i].argv != NULL) {
335 for (j = 0; work->options[i].argv[j] != NULL; j++)
336 free(work->options[i].argv[j]);
337 free(work->options[i].argv);
338 }
339 if (work->output)
340 pam_output_free(work->output);
341 if (work->prompts != NULL) {
342 for (i = 0; i < work->prompts->size; i++) {
343 free(work->prompts->prompts[i].prompt);
344 free(work->prompts->prompts[i].response);
345 }
346 free(work->prompts->prompts);
347 free(work->prompts);
348 }
349 free(work);
350 free(path);
351 }
352
353
354 /*
355 * Check a filename for acceptable characters. Returns true if the file
356 * consists solely of [a-zA-Z0-9-] and false otherwise.
357 */
358 static bool
valid_filename(const char * filename)359 valid_filename(const char *filename)
360 {
361 const char *p;
362
363 for (p = filename; *p != '\0'; p++) {
364 if (*p >= 'A' && *p <= 'Z')
365 continue;
366 if (*p >= 'a' && *p <= 'z')
367 continue;
368 if (*p >= '0' && *p <= '9')
369 continue;
370 if (*p == '-')
371 continue;
372 return false;
373 }
374 return true;
375 }
376
377
378 /*
379 * The same as run_script, but run every script found in the given directory,
380 * skipping file names that contain characters other than alphanumerics and -.
381 */
382 void
run_script_dir(const char * dir,const struct script_config * config)383 run_script_dir(const char *dir, const struct script_config *config)
384 {
385 DIR *handle;
386 struct dirent *entry;
387 const char *path;
388 char *file;
389
390 if (access(dir, R_OK) == 0)
391 path = dir;
392 else
393 path = test_file_path(dir);
394 handle = opendir(path);
395 if (handle == NULL)
396 sysbail("cannot open directory %s", dir);
397 errno = 0;
398 while ((entry = readdir(handle)) != NULL) {
399 if (!valid_filename(entry->d_name))
400 continue;
401 basprintf(&file, "%s/%s", path, entry->d_name);
402 run_script(file, config);
403 free(file);
404 errno = 0;
405 }
406 if (errno != 0)
407 sysbail("cannot read directory %s", dir);
408 closedir(handle);
409 if (path != dir)
410 test_file_path_free((char *) path);
411 }
412