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))) 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 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))) 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 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 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 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 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 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