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 2017-2018, 2020 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 <assert.h> 43 #include <ctype.h> 44 #include <dirent.h> 45 #include <errno.h> 46 #include <syslog.h> 47 48 #include <tests/fakepam/internal.h> 49 #include <tests/fakepam/script.h> 50 #include <tests/tap/basic.h> 51 #include <tests/tap/string.h> 52 53 /* Used for enumerating arrays. */ 54 #define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) 55 56 /* Mapping of strings to PAM function pointers and group numbers. */ 57 static const struct { 58 const char *name; 59 pam_call call; 60 enum group_type group; 61 } CALLS[] = { 62 /* clang-format off */ 63 {"acct_mgmt", pam_sm_acct_mgmt, GROUP_ACCOUNT }, 64 {"authenticate", pam_sm_authenticate, GROUP_AUTH }, 65 {"setcred", pam_sm_setcred, GROUP_AUTH }, 66 {"chauthtok", pam_sm_chauthtok, GROUP_PASSWORD}, 67 {"open_session", pam_sm_open_session, GROUP_SESSION }, 68 {"close_session", pam_sm_close_session, GROUP_SESSION }, 69 /* clang-format on */ 70 }; 71 72 /* Mapping of PAM flag names without the leading PAM_ to values. */ 73 static const struct { 74 const char *name; 75 int value; 76 } FLAGS[] = { 77 /* clang-format off */ 78 {"CHANGE_EXPIRED_AUTHTOK", PAM_CHANGE_EXPIRED_AUTHTOK}, 79 {"DELETE_CRED", PAM_DELETE_CRED }, 80 {"DISALLOW_NULL_AUTHTOK", PAM_DISALLOW_NULL_AUTHTOK }, 81 {"ESTABLISH_CRED", PAM_ESTABLISH_CRED }, 82 {"PRELIM_CHECK", PAM_PRELIM_CHECK }, 83 {"REFRESH_CRED", PAM_REFRESH_CRED }, 84 {"REINITIALIZE_CRED", PAM_REINITIALIZE_CRED }, 85 {"SILENT", PAM_SILENT }, 86 {"UPDATE_AUTHTOK", PAM_UPDATE_AUTHTOK }, 87 /* clang-format on */ 88 }; 89 90 /* Mapping of strings to PAM groups. */ 91 static const struct { 92 const char *name; 93 enum group_type group; 94 } GROUPS[] = { 95 /* clang-format off */ 96 {"account", GROUP_ACCOUNT }, 97 {"auth", GROUP_AUTH }, 98 {"password", GROUP_PASSWORD}, 99 {"session", GROUP_SESSION }, 100 /* clang-format on */ 101 }; 102 103 /* Mapping of strings to PAM return values. */ 104 static const struct { 105 const char *name; 106 int status; 107 } RETURNS[] = { 108 /* clang-format off */ 109 {"PAM_AUTH_ERR", PAM_AUTH_ERR }, 110 {"PAM_AUTHINFO_UNAVAIL", PAM_AUTHINFO_UNAVAIL}, 111 {"PAM_AUTHTOK_ERR", PAM_AUTHTOK_ERR }, 112 {"PAM_DATA_SILENT", PAM_DATA_SILENT }, 113 {"PAM_IGNORE", PAM_IGNORE }, 114 {"PAM_NEW_AUTHTOK_REQD", PAM_NEW_AUTHTOK_REQD}, 115 {"PAM_SESSION_ERR", PAM_SESSION_ERR }, 116 {"PAM_SUCCESS", PAM_SUCCESS }, 117 {"PAM_USER_UNKNOWN", PAM_USER_UNKNOWN }, 118 /* clang-format on */ 119 }; 120 121 /* Mapping of PAM prompt styles to their values. */ 122 static const struct { 123 const char *name; 124 int style; 125 } STYLES[] = { 126 /* clang-format off */ 127 {"echo_off", PAM_PROMPT_ECHO_OFF}, 128 {"echo_on", PAM_PROMPT_ECHO_ON }, 129 {"error_msg", PAM_ERROR_MSG }, 130 {"info", PAM_TEXT_INFO }, 131 /* clang-format on */ 132 }; 133 134 /* Mappings of strings to syslog priorities. */ 135 static const struct { 136 const char *name; 137 int priority; 138 } PRIORITIES[] = { 139 /* clang-format off */ 140 {"DEBUG", LOG_DEBUG }, 141 {"INFO", LOG_INFO }, 142 {"NOTICE", LOG_NOTICE}, 143 {"ERR", LOG_ERR }, 144 {"CRIT", LOG_CRIT }, 145 /* clang-format on */ 146 }; 147 148 149 /* 150 * Given a pointer to a string, skip any leading whitespace and return a 151 * pointer to the first non-whitespace character. 152 */ 153 static char * 154 skip_whitespace(char *p) 155 { 156 while (isspace((unsigned char) (*p))) 157 p++; 158 return p; 159 } 160 161 162 /* 163 * Read a line from a file into a BUFSIZ buffer, failing if the line was too 164 * long to fit into the buffer, and returns a copy of that line in newly 165 * allocated memory. Ignores blank lines and comments. Caller is responsible 166 * for freeing. Returns NULL on end of file and fails on read errors. 167 */ 168 static char * 169 readline(FILE *file) 170 { 171 char buffer[BUFSIZ]; 172 char *line, *first; 173 174 do { 175 line = fgets(buffer, sizeof(buffer), file); 176 if (line == NULL) { 177 if (feof(file)) 178 return NULL; 179 sysbail("cannot read line from script"); 180 } 181 if (buffer[strlen(buffer) - 1] != '\n') 182 bail("script line too long"); 183 buffer[strlen(buffer) - 1] = '\0'; 184 first = skip_whitespace(buffer); 185 } while (first[0] == '#' || first[0] == '\0'); 186 line = bstrdup(buffer); 187 return line; 188 } 189 190 191 /* 192 * Given the name of a PAM call, map it to a call enum. This is used later in 193 * switch statements to determine which function to call. Fails on any 194 * unrecognized string. If the optional second argument is not NULL, also 195 * store the group number in that argument. 196 */ 197 static pam_call 198 string_to_call(const char *name, enum group_type *group) 199 { 200 size_t i; 201 202 for (i = 0; i < ARRAY_SIZE(CALLS); i++) 203 if (strcmp(name, CALLS[i].name) == 0) { 204 if (group != NULL) 205 *group = CALLS[i].group; 206 return CALLS[i].call; 207 } 208 bail("unrecognized PAM call %s", name); 209 } 210 211 212 /* 213 * Given a PAM flag value without the leading PAM_, map it to the numeric 214 * value of that flag. Fails on any unrecognized string. 215 */ 216 static int 217 string_to_flag(const char *name) 218 { 219 size_t i; 220 221 for (i = 0; i < ARRAY_SIZE(FLAGS); i++) 222 if (strcmp(name, FLAGS[i].name) == 0) 223 return FLAGS[i].value; 224 bail("unrecognized PAM flag %s", name); 225 } 226 227 228 /* 229 * Given a PAM group name, map it to the array index for the options array for 230 * that group. Fails on any unrecognized string. 231 */ 232 static enum group_type 233 string_to_group(const char *name) 234 { 235 size_t i; 236 237 for (i = 0; i < ARRAY_SIZE(GROUPS); i++) 238 if (strcmp(name, GROUPS[i].name) == 0) 239 return GROUPS[i].group; 240 bail("unrecognized PAM group %s", name); 241 } 242 243 244 /* 245 * Given a syslog priority name, map it to the numeric value of that priority. 246 * Fails on any unrecognized string. 247 */ 248 static int 249 string_to_priority(const char *name) 250 { 251 size_t i; 252 253 for (i = 0; i < ARRAY_SIZE(PRIORITIES); i++) 254 if (strcmp(name, PRIORITIES[i].name) == 0) 255 return PRIORITIES[i].priority; 256 bail("unrecognized syslog priority %s", name); 257 } 258 259 260 /* 261 * Given a PAM return status, map it to the actual expected value. Fails on 262 * any unrecognized string. 263 */ 264 static int 265 string_to_status(const char *name) 266 { 267 size_t i; 268 269 if (name == NULL) 270 bail("no PAM status on line"); 271 for (i = 0; i < ARRAY_SIZE(RETURNS); i++) 272 if (strcmp(name, RETURNS[i].name) == 0) 273 return RETURNS[i].status; 274 bail("unrecognized PAM status %s", name); 275 } 276 277 278 /* 279 * Given a PAM prompt style value without the leading PAM_PROMPT_, map it to 280 * the numeric value of that flag. Fails on any unrecognized string. 281 */ 282 static int 283 string_to_style(const char *name) 284 { 285 size_t i; 286 287 for (i = 0; i < ARRAY_SIZE(STYLES); i++) 288 if (strcmp(name, STYLES[i].name) == 0) 289 return STYLES[i].style; 290 bail("unrecognized PAM prompt style %s", name); 291 } 292 293 294 /* 295 * We found a section delimiter while parsing another section. Rewind our 296 * input file back before the section delimiter so that we'll read it again. 297 * Takes the length of the line we read, which is used to determine how far to 298 * rewind. 299 */ 300 static void 301 rewind_section(FILE *script, size_t length) 302 { 303 if (fseek(script, -length - 1, SEEK_CUR) != 0) 304 sysbail("cannot rewind file"); 305 } 306 307 308 /* 309 * Given a string that may contain %-escapes, expand it into the resulting 310 * value. The following escapes are supported: 311 * 312 * %i current UID (not target user UID) 313 * %n new password 314 * %p password 315 * %u username 316 * %0 user-supplied string 317 * ... 318 * %9 user-supplied string 319 * 320 * The %* escape is preserved as-is, as it has to be interpreted at the time 321 * of checking output. Returns the expanded string in newly-allocated memory. 322 */ 323 static char * 324 expand_string(const char *template, const struct script_config *config) 325 { 326 size_t length = 0; 327 const char *p, *extra; 328 char *output, *out; 329 char *uid = NULL; 330 331 length = 0; 332 for (p = template; *p != '\0'; p++) { 333 if (*p != '%') 334 length++; 335 else { 336 p++; 337 switch (*p) { 338 case 'i': 339 if (uid == NULL) 340 basprintf(&uid, "%lu", (unsigned long) getuid()); 341 length += strlen(uid); 342 break; 343 case 'n': 344 if (config->newpass == NULL) 345 bail("new password not set"); 346 length += strlen(config->newpass); 347 break; 348 case 'p': 349 if (config->password == NULL) 350 bail("password not set"); 351 length += strlen(config->password); 352 break; 353 case 'u': 354 length += strlen(config->user); 355 break; 356 case '0': 357 case '1': 358 case '2': 359 case '3': 360 case '4': 361 case '5': 362 case '6': 363 case '7': 364 case '8': 365 case '9': 366 if (config->extra[*p - '0'] == NULL) 367 bail("extra script parameter %%%c not set", *p); 368 length += strlen(config->extra[*p - '0']); 369 break; 370 case '*': 371 length += 2; 372 break; 373 default: 374 length++; 375 break; 376 } 377 } 378 } 379 output = bmalloc(length + 1); 380 for (p = template, out = output; *p != '\0'; p++) { 381 if (*p != '%') 382 *out++ = *p; 383 else { 384 p++; 385 switch (*p) { 386 case 'i': 387 assert(uid != NULL); 388 memcpy(out, uid, strlen(uid)); 389 out += strlen(uid); 390 break; 391 case 'n': 392 memcpy(out, config->newpass, strlen(config->newpass)); 393 out += strlen(config->newpass); 394 break; 395 case 'p': 396 memcpy(out, config->password, strlen(config->password)); 397 out += strlen(config->password); 398 break; 399 case 'u': 400 memcpy(out, config->user, strlen(config->user)); 401 out += strlen(config->user); 402 break; 403 case '0': 404 case '1': 405 case '2': 406 case '3': 407 case '4': 408 case '5': 409 case '6': 410 case '7': 411 case '8': 412 case '9': 413 extra = config->extra[*p - '0']; 414 memcpy(out, extra, strlen(extra)); 415 out += strlen(extra); 416 break; 417 case '*': 418 *out++ = '%'; 419 *out++ = '*'; 420 break; 421 default: 422 *out++ = *p; 423 break; 424 } 425 } 426 } 427 *out = '\0'; 428 free(uid); 429 return output; 430 } 431 432 433 /* 434 * Given a whitespace-delimited string of PAM options, split it into an argv 435 * array and argc count and store it in the provided option struct. 436 */ 437 static void 438 split_options(char *string, struct options *options, 439 const struct script_config *config) 440 { 441 char *opt; 442 size_t size, count; 443 444 for (opt = strtok(string, " "); opt != NULL; opt = strtok(NULL, " ")) { 445 if (options->argv == NULL) { 446 options->argv = bcalloc(2, sizeof(const char *)); 447 options->argv[0] = expand_string(opt, config); 448 options->argc = 1; 449 } else { 450 count = (options->argc + 2); 451 size = sizeof(const char *); 452 options->argv = breallocarray(options->argv, count, size); 453 options->argv[options->argc] = expand_string(opt, config); 454 options->argv[options->argc + 1] = NULL; 455 options->argc++; 456 } 457 } 458 } 459 460 461 /* 462 * Parse the options section of a PAM script. This consists of one or more 463 * lines in the format: 464 * 465 * <group> = <options> 466 * 467 * where options are either option names or option=value pairs, where the 468 * value may not contain whitespace. Returns an options struct, which stores 469 * argc and argv values for each group. 470 * 471 * Takes the work struct as an argument and puts values into its array. 472 */ 473 static void 474 parse_options(FILE *script, struct work *work, 475 const struct script_config *config) 476 { 477 char *line, *group, *token; 478 size_t length = 0; 479 enum group_type type; 480 481 for (line = readline(script); line != NULL; line = readline(script)) { 482 length = strlen(line); 483 group = strtok(line, " "); 484 if (group == NULL) 485 bail("malformed script line"); 486 if (group[0] == '[') 487 break; 488 type = string_to_group(group); 489 token = strtok(NULL, " "); 490 if (token == NULL) 491 bail("malformed action line"); 492 if (strcmp(token, "=") != 0) 493 bail("malformed action line near %s", token); 494 token = strtok(NULL, ""); 495 split_options(token, &work->options[type], config); 496 free(line); 497 } 498 if (line != NULL) { 499 free(line); 500 rewind_section(script, length); 501 } 502 } 503 504 505 /* 506 * Parse the call portion of a PAM call in the run section of a PAM script. 507 * This handles parsing the PAM flags that optionally may be given as part of 508 * the call. Takes the token representing the call and a pointer to the 509 * action struct to fill in with the call and the option flags. 510 */ 511 static void 512 parse_call(char *token, struct action *action) 513 { 514 char *flags, *flag; 515 516 action->flags = 0; 517 flags = strchr(token, '('); 518 if (flags != NULL) { 519 *flags = '\0'; 520 flags++; 521 for (flag = strtok(flags, "|,)"); flag != NULL; 522 flag = strtok(NULL, "|,)")) { 523 action->flags |= string_to_flag(flag); 524 } 525 } 526 action->call = string_to_call(token, &action->group); 527 } 528 529 530 /* 531 * Parse the run section of a PAM script. This consists of one or more lines 532 * in the format: 533 * 534 * <call> = <status> 535 * 536 * where <call> is a PAM call and <status> is what it should return. Returns 537 * a linked list of actions. Fails on any error in parsing. 538 */ 539 static struct action * 540 parse_run(FILE *script) 541 { 542 struct action *head = NULL, *current = NULL, *next; 543 char *line, *token, *call; 544 size_t length = 0; 545 546 for (line = readline(script); line != NULL; line = readline(script)) { 547 length = strlen(line); 548 token = strtok(line, " "); 549 if (token[0] == '[') 550 break; 551 next = bmalloc(sizeof(struct action)); 552 next->next = NULL; 553 if (head == NULL) 554 head = next; 555 else 556 current->next = next; 557 next->name = bstrdup(token); 558 call = token; 559 token = strtok(NULL, " "); 560 if (token == NULL) 561 bail("malformed action line"); 562 if (strcmp(token, "=") != 0) 563 bail("malformed action line near %s", token); 564 token = strtok(NULL, " "); 565 next->status = string_to_status(token); 566 parse_call(call, next); 567 free(line); 568 current = next; 569 } 570 if (head == NULL) 571 bail("empty run section in script"); 572 if (line != NULL) { 573 free(line); 574 rewind_section(script, length); 575 } 576 return head; 577 } 578 579 580 /* 581 * Parse the end section of a PAM script. There is one supported line in the 582 * format: 583 * 584 * flags = <flag>|<flag> 585 * 586 * where <flag> is a flag to pass to pam_end. Returns the flags. 587 */ 588 static int 589 parse_end(FILE *script) 590 { 591 char *line, *token, *flag; 592 size_t length = 0; 593 int flags = PAM_SUCCESS; 594 595 for (line = readline(script); line != NULL; line = readline(script)) { 596 length = strlen(line); 597 token = strtok(line, " "); 598 if (token[0] == '[') 599 break; 600 if (strcmp(token, "flags") != 0) 601 bail("unknown end setting %s", token); 602 token = strtok(NULL, " "); 603 if (token == NULL) 604 bail("malformed end line"); 605 if (strcmp(token, "=") != 0) 606 bail("malformed end line near %s", token); 607 token = strtok(NULL, " "); 608 flag = strtok(token, "|"); 609 while (flag != NULL) { 610 flags |= string_to_status(flag); 611 flag = strtok(NULL, "|"); 612 } 613 free(line); 614 } 615 if (line != NULL) { 616 free(line); 617 rewind_section(script, length); 618 } 619 return flags; 620 } 621 622 623 /* 624 * Parse the output section of a PAM script. This consists of zero or more 625 * lines in the format: 626 * 627 * PRIORITY some output information 628 * PRIORITY /output regex/ 629 * 630 * where PRIORITY is replaced by the numeric syslog priority corresponding to 631 * that priority and the rest of the output undergoes %-esacape expansion. 632 * Returns the accumulated output as a vector. 633 */ 634 static struct output * 635 parse_output(FILE *script, const struct script_config *config) 636 { 637 char *line, *token, *message; 638 struct output *output; 639 int priority; 640 641 output = output_new(); 642 if (output == NULL) 643 sysbail("cannot allocate vector"); 644 for (line = readline(script); line != NULL; line = readline(script)) { 645 token = strtok(line, " "); 646 priority = string_to_priority(token); 647 token = strtok(NULL, ""); 648 if (token == NULL) 649 bail("malformed line %s", line); 650 message = expand_string(token, config); 651 output_add(output, priority, message); 652 free(message); 653 free(line); 654 } 655 return output; 656 } 657 658 659 /* 660 * Parse the prompts section of a PAM script. This consists of zero or more 661 * lines in one of the formats: 662 * 663 * type = prompt 664 * type = /prompt/ 665 * type = prompt|response 666 * type = /prompt/|response 667 * 668 * If the type is error_msg or info, there is no response. Otherwise, 669 * everything after the last | is taken to be the response that should be 670 * provided to that prompt. The response undergoes %-escape expansion. 671 */ 672 static struct prompts * 673 parse_prompts(FILE *script, const struct script_config *config) 674 { 675 struct prompts *prompts = NULL; 676 struct prompt *prompt; 677 char *line, *token, *style, *end; 678 size_t size, count, i; 679 size_t length = 0; 680 681 for (line = readline(script); line != NULL; line = readline(script)) { 682 length = strlen(line); 683 token = strtok(line, " "); 684 if (token[0] == '[') 685 break; 686 if (prompts == NULL) { 687 prompts = bcalloc(1, sizeof(struct prompts)); 688 prompts->prompts = bcalloc(1, sizeof(struct prompt)); 689 prompts->allocated = 1; 690 } else if (prompts->allocated == prompts->size) { 691 count = prompts->allocated * 2; 692 size = sizeof(struct prompt); 693 prompts->prompts = breallocarray(prompts->prompts, count, size); 694 prompts->allocated = count; 695 for (i = prompts->size; i < prompts->allocated; i++) { 696 prompts->prompts[i].prompt = NULL; 697 prompts->prompts[i].response = NULL; 698 } 699 } 700 prompt = &prompts->prompts[prompts->size]; 701 style = token; 702 token = strtok(NULL, " "); 703 if (token == NULL) 704 bail("malformed prompt line"); 705 if (strcmp(token, "=") != 0) 706 bail("malformed prompt line near %s", token); 707 prompt->style = string_to_style(style); 708 token = strtok(NULL, ""); 709 if (prompt->style == PAM_ERROR_MSG || prompt->style == PAM_TEXT_INFO) 710 prompt->prompt = expand_string(token, config); 711 else { 712 end = strrchr(token, '|'); 713 if (end == NULL) 714 bail("malformed prompt line near %s", token); 715 *end = '\0'; 716 prompt->prompt = expand_string(token, config); 717 token = end + 1; 718 prompt->response = expand_string(token, config); 719 } 720 prompts->size++; 721 free(line); 722 } 723 if (line != NULL) { 724 free(line); 725 rewind_section(script, length); 726 } 727 return prompts; 728 } 729 730 731 /* 732 * Parse a PAM interaction script. This handles parsing of the top-level 733 * section markers and dispatches the parsing to other functions. Returns the 734 * total work to do as a work struct. 735 */ 736 struct work * 737 parse_script(FILE *script, const struct script_config *config) 738 { 739 struct work *work; 740 char *line, *token; 741 742 work = bmalloc(sizeof(struct work)); 743 memset(work, 0, sizeof(struct work)); 744 work->end_flags = PAM_SUCCESS; 745 for (line = readline(script); line != NULL; line = readline(script)) { 746 token = strtok(line, " "); 747 if (token[0] != '[') 748 bail("line outside of section: %s", line); 749 if (strcmp(token, "[options]") == 0) 750 parse_options(script, work, config); 751 else if (strcmp(token, "[run]") == 0) 752 work->actions = parse_run(script); 753 else if (strcmp(token, "[end]") == 0) 754 work->end_flags = parse_end(script); 755 else if (strcmp(token, "[output]") == 0) 756 work->output = parse_output(script, config); 757 else if (strcmp(token, "[prompts]") == 0) 758 work->prompts = parse_prompts(script, config); 759 else 760 bail("unknown section: %s", token); 761 free(line); 762 } 763 if (work->actions == NULL) 764 bail("no run section defined"); 765 return work; 766 } 767