1 /* 2 * Utility functions for tests that use Kerberos. 3 * 4 * The core function is kerberos_setup, which loads Kerberos test 5 * configuration and returns a struct of information. It also supports 6 * obtaining initial tickets from the configured keytab and setting up 7 * KRB5CCNAME and KRB5_KTNAME if a Kerberos keytab is present. Also included 8 * are utility functions for setting up a krb5.conf file and reporting 9 * Kerberos errors or warnings during testing. 10 * 11 * Some of the functionality here is only available if the Kerberos libraries 12 * are available. 13 * 14 * The canonical version of this file is maintained in the rra-c-util package, 15 * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. 16 * 17 * Written by Russ Allbery <eagle@eyrie.org> 18 * Copyright 2017 Russ Allbery <eagle@eyrie.org> 19 * Copyright 2006-2007, 2009-2014 20 * The Board of Trustees of the Leland Stanford Junior University 21 * 22 * Permission is hereby granted, free of charge, to any person obtaining a 23 * copy of this software and associated documentation files (the "Software"), 24 * to deal in the Software without restriction, including without limitation 25 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 26 * and/or sell copies of the Software, and to permit persons to whom the 27 * Software is furnished to do so, subject to the following conditions: 28 * 29 * The above copyright notice and this permission notice shall be included in 30 * all copies or substantial portions of the Software. 31 * 32 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 33 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 34 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 35 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 36 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 37 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 38 * DEALINGS IN THE SOFTWARE. 39 * 40 * SPDX-License-Identifier: MIT 41 */ 42 43 #include <config.h> 44 #ifdef HAVE_KRB5 45 # include <portable/krb5.h> 46 #endif 47 #include <portable/system.h> 48 49 #include <sys/stat.h> 50 51 #include <tests/tap/basic.h> 52 #include <tests/tap/kerberos.h> 53 #include <tests/tap/macros.h> 54 #include <tests/tap/process.h> 55 #include <tests/tap/string.h> 56 57 /* 58 * Disable the requirement that format strings be literals, since it's easier 59 * to handle the possible patterns for kinit commands as an array. 60 */ 61 #if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ > 2) || defined(__clang__) 62 # pragma GCC diagnostic ignored "-Wformat-nonliteral" 63 #endif 64 65 66 /* 67 * These variables hold the allocated configuration struct, the environment to 68 * point to a different Kerberos ticket cache, keytab, and configuration file, 69 * and the temporary directories used. We store them so that we can free them 70 * on exit for cleaner valgrind output, making it easier to find real memory 71 * leaks in the tested programs. 72 */ 73 static struct kerberos_config *config = NULL; 74 static char *krb5ccname = NULL; 75 static char *krb5_ktname = NULL; 76 static char *krb5_config = NULL; 77 static char *tmpdir_ticket = NULL; 78 static char *tmpdir_conf = NULL; 79 80 81 /* 82 * Obtain Kerberos tickets and fill in the principal config entry. 83 * 84 * There are two implementations of this function, one if we have native 85 * Kerberos libraries available and one if we don't. Uses keytab to obtain 86 * credentials, and fills in the cache member of the provided config struct. 87 */ 88 #ifdef HAVE_KRB5 89 90 static void 91 kerberos_kinit(void) 92 { 93 char *name, *krbtgt; 94 krb5_error_code code; 95 krb5_context ctx; 96 krb5_ccache ccache; 97 krb5_principal kprinc; 98 krb5_keytab keytab; 99 krb5_get_init_creds_opt *opts; 100 krb5_creds creds; 101 const char *realm; 102 103 /* 104 * Determine the principal corresponding to that keytab. We copy the 105 * memory to ensure that it's allocated in the right memory domain on 106 * systems where that may matter (like Windows). 107 */ 108 code = krb5_init_context(&ctx); 109 if (code != 0) 110 bail_krb5(ctx, code, "error initializing Kerberos"); 111 kprinc = kerberos_keytab_principal(ctx, config->keytab); 112 code = krb5_unparse_name(ctx, kprinc, &name); 113 if (code != 0) 114 bail_krb5(ctx, code, "error unparsing name"); 115 krb5_free_principal(ctx, kprinc); 116 config->principal = bstrdup(name); 117 krb5_free_unparsed_name(ctx, name); 118 119 /* Now do the Kerberos initialization. */ 120 code = krb5_cc_default(ctx, &ccache); 121 if (code != 0) 122 bail_krb5(ctx, code, "error setting ticket cache"); 123 code = krb5_parse_name(ctx, config->principal, &kprinc); 124 if (code != 0) 125 bail_krb5(ctx, code, "error parsing principal %s", config->principal); 126 realm = krb5_principal_get_realm(ctx, kprinc); 127 basprintf(&krbtgt, "krbtgt/%s@%s", realm, realm); 128 code = krb5_kt_resolve(ctx, config->keytab, &keytab); 129 if (code != 0) 130 bail_krb5(ctx, code, "cannot open keytab %s", config->keytab); 131 code = krb5_get_init_creds_opt_alloc(ctx, &opts); 132 if (code != 0) 133 bail_krb5(ctx, code, "cannot allocate credential options"); 134 krb5_get_init_creds_opt_set_default_flags(ctx, NULL, realm, opts); 135 krb5_get_init_creds_opt_set_forwardable(opts, 0); 136 krb5_get_init_creds_opt_set_proxiable(opts, 0); 137 code = krb5_get_init_creds_keytab(ctx, &creds, kprinc, keytab, 0, krbtgt, 138 opts); 139 if (code != 0) 140 bail_krb5(ctx, code, "cannot get Kerberos tickets"); 141 code = krb5_cc_initialize(ctx, ccache, kprinc); 142 if (code != 0) 143 bail_krb5(ctx, code, "error initializing ticket cache"); 144 code = krb5_cc_store_cred(ctx, ccache, &creds); 145 if (code != 0) 146 bail_krb5(ctx, code, "error storing credentials"); 147 krb5_cc_close(ctx, ccache); 148 krb5_free_cred_contents(ctx, &creds); 149 krb5_kt_close(ctx, keytab); 150 krb5_free_principal(ctx, kprinc); 151 krb5_get_init_creds_opt_free(ctx, opts); 152 krb5_free_context(ctx); 153 free(krbtgt); 154 } 155 156 #else /* !HAVE_KRB5 */ 157 158 static void 159 kerberos_kinit(void) 160 { 161 static const char *const format[] = { 162 "kinit --no-afslog -k -t %s %s >/dev/null 2>&1 </dev/null", 163 "kinit -k -t %s %s >/dev/null 2>&1 </dev/null", 164 "kinit -t %s %s >/dev/null 2>&1 </dev/null", 165 "kinit -k -K %s %s >/dev/null 2>&1 </dev/null"}; 166 FILE *file; 167 char *path; 168 char principal[BUFSIZ], *command; 169 size_t i; 170 int status; 171 172 /* Read the principal corresponding to the keytab. */ 173 path = test_file_path("config/principal"); 174 if (path == NULL) { 175 test_file_path_free(config->keytab); 176 config->keytab = NULL; 177 return; 178 } 179 file = fopen(path, "r"); 180 if (file == NULL) { 181 test_file_path_free(path); 182 return; 183 } 184 test_file_path_free(path); 185 if (fgets(principal, sizeof(principal), file) == NULL) 186 bail("cannot read %s", path); 187 fclose(file); 188 if (principal[strlen(principal) - 1] != '\n') 189 bail("no newline in %s", path); 190 principal[strlen(principal) - 1] = '\0'; 191 config->principal = bstrdup(principal); 192 193 /* Now do the Kerberos initialization. */ 194 for (i = 0; i < ARRAY_SIZE(format); i++) { 195 basprintf(&command, format[i], config->keytab, principal); 196 status = system(command); 197 free(command); 198 if (status != -1 && WEXITSTATUS(status) == 0) 199 break; 200 } 201 if (status == -1 || WEXITSTATUS(status) != 0) 202 bail("cannot get Kerberos tickets"); 203 } 204 205 #endif /* !HAVE_KRB5 */ 206 207 208 /* 209 * Free all the memory associated with our Kerberos setup, but don't remove 210 * the ticket cache. This is used when cleaning up on exit from a non-primary 211 * process so that test programs that fork don't remove the ticket cache still 212 * used by the main program. 213 */ 214 static void 215 kerberos_free(void) 216 { 217 test_tmpdir_free(tmpdir_ticket); 218 tmpdir_ticket = NULL; 219 if (config != NULL) { 220 test_file_path_free(config->keytab); 221 free(config->principal); 222 free(config->cache); 223 free(config->userprinc); 224 free(config->username); 225 free(config->password); 226 free(config->pkinit_principal); 227 free(config->pkinit_cert); 228 free(config); 229 config = NULL; 230 } 231 if (krb5ccname != NULL) { 232 putenv((char *) "KRB5CCNAME="); 233 free(krb5ccname); 234 krb5ccname = NULL; 235 } 236 if (krb5_ktname != NULL) { 237 putenv((char *) "KRB5_KTNAME="); 238 free(krb5_ktname); 239 krb5_ktname = NULL; 240 } 241 } 242 243 244 /* 245 * Clean up at the end of a test. This removes the ticket cache and resets 246 * and frees the memory allocated for the environment variables so that 247 * valgrind output on test suites is cleaner. Most of the work is done by 248 * kerberos_free, but this function also deletes the ticket cache. 249 */ 250 void 251 kerberos_cleanup(void) 252 { 253 char *path; 254 255 if (tmpdir_ticket != NULL) { 256 basprintf(&path, "%s/krb5cc_test", tmpdir_ticket); 257 unlink(path); 258 free(path); 259 } 260 kerberos_free(); 261 } 262 263 264 /* 265 * The cleanup handler for the TAP framework. Call kerberos_cleanup if we're 266 * in the primary process and kerberos_free if not. The first argument, which 267 * indicates whether the test succeeded or not, is ignored, since we need to 268 * do the same thing either way. 269 */ 270 static void 271 kerberos_cleanup_handler(int success UNUSED, int primary) 272 { 273 if (primary) 274 kerberos_cleanup(); 275 else 276 kerberos_free(); 277 } 278 279 280 /* 281 * Obtain Kerberos tickets for the principal specified in config/principal 282 * using the keytab specified in config/keytab, both of which are presumed to 283 * be in tests in either the build or the source tree. Also sets KRB5_KTNAME 284 * and KRB5CCNAME. 285 * 286 * Returns the contents of config/principal in newly allocated memory or NULL 287 * if Kerberos tests are apparently not configured. If Kerberos tests are 288 * configured but something else fails, calls bail. 289 */ 290 struct kerberos_config * 291 kerberos_setup(enum kerberos_needs needs) 292 { 293 char *path; 294 char buffer[BUFSIZ]; 295 FILE *file = NULL; 296 297 /* If we were called before, clean up after the previous run. */ 298 if (config != NULL) 299 kerberos_cleanup(); 300 config = bcalloc(1, sizeof(struct kerberos_config)); 301 302 /* 303 * If we have a config/keytab file, set the KRB5CCNAME and KRB5_KTNAME 304 * environment variables and obtain initial tickets. 305 */ 306 config->keytab = test_file_path("config/keytab"); 307 if (config->keytab == NULL) { 308 if (needs == TAP_KRB_NEEDS_KEYTAB || needs == TAP_KRB_NEEDS_BOTH) 309 skip_all("Kerberos tests not configured"); 310 } else { 311 tmpdir_ticket = test_tmpdir(); 312 basprintf(&config->cache, "%s/krb5cc_test", tmpdir_ticket); 313 basprintf(&krb5ccname, "KRB5CCNAME=%s/krb5cc_test", tmpdir_ticket); 314 basprintf(&krb5_ktname, "KRB5_KTNAME=%s", config->keytab); 315 putenv(krb5ccname); 316 putenv(krb5_ktname); 317 kerberos_kinit(); 318 } 319 320 /* 321 * If we have a config/password file, read it and fill out the relevant 322 * members of our config struct. 323 */ 324 path = test_file_path("config/password"); 325 if (path != NULL) 326 file = fopen(path, "r"); 327 if (file == NULL) { 328 if (needs == TAP_KRB_NEEDS_PASSWORD || needs == TAP_KRB_NEEDS_BOTH) 329 skip_all("Kerberos tests not configured"); _get_exceptions(subclass)330 } else { 331 if (fgets(buffer, sizeof(buffer), file) == NULL) 332 bail("cannot read %s", path); 333 if (buffer[strlen(buffer) - 1] != '\n') 334 bail("no newline in %s", path); 335 buffer[strlen(buffer) - 1] = '\0'; 336 config->userprinc = bstrdup(buffer); 337 if (fgets(buffer, sizeof(buffer), file) == NULL) 338 bail("cannot read password from %s", path); 339 fclose(file); 340 if (buffer[strlen(buffer) - 1] != '\n') 341 bail("password too long in %s", path); 342 buffer[strlen(buffer) - 1] = '\0'; 343 config->password = bstrdup(buffer); 344 345 /* 346 * Strip the realm from the principal and set realm and username. 347 * This is not strictly correct; it doesn't cope with escaped @-signs 348 * or enterprise names. 349 */ 350 config->username = bstrdup(config->userprinc); 351 config->realm = strchr(config->username, '@'); 352 if (config->realm == NULL) 353 bail("test principal has no realm"); 354 *config->realm = '\0'; 355 config->realm++; 356 } 357 test_file_path_free(path); 358 359 /* 360 * If we have PKINIT configuration, read it and fill out the relevant 361 * members of our config struct. 362 */ 363 path = test_file_path("config/pkinit-principal"); 364 if (path != NULL) get_warnings()365 file = fopen(path, "r"); 366 if (path != NULL && file != NULL) { 367 if (fgets(buffer, sizeof(buffer), file) == NULL) 368 bail("cannot read %s", path); 369 if (buffer[strlen(buffer) - 1] != '\n') 370 bail("no newline in %s", path); 371 buffer[strlen(buffer) - 1] = '\0'; 372 fclose(file); 373 test_file_path_free(path); 374 path = test_file_path("config/pkinit-cert"); 375 if (path != NULL) { 376 config->pkinit_principal = bstrdup(buffer); 377 config->pkinit_cert = bstrdup(path); 378 } 379 } 380 test_file_path_free(path); 381 if (config->pkinit_cert == NULL && (needs & TAP_KRB_NEEDS_PKINIT) != 0) 382 skip_all("PKINIT tests not configured"); 383 384 /* 385 * Register the cleanup function so that the caller doesn't have to do 386 * explicit cleanup. 387 */ 388 test_cleanup_register(kerberos_cleanup_handler); 389 390 /* Return the configuration. */ 391 return config; 392 } 393 394 395 /* 396 * Clean up the krb5.conf file generated by kerberos_generate_conf and free 397 * the memory used to set the environment variable. This doesn't fail if the 398 * file and variable are already gone, allowing it to be harmlessly run 399 * multiple times. 400 * 401 * Normally called via an atexit handler. 402 */ 403 void 404 kerberos_cleanup_conf(void) 405 { 406 char *path; 407 408 if (tmpdir_conf != NULL) { 409 basprintf(&path, "%s/krb5.conf", tmpdir_conf); 410 unlink(path); 411 free(path); 412 test_tmpdir_free(tmpdir_conf); 413 tmpdir_conf = NULL; 414 } 415 putenv((char *) "KRB5_CONFIG="); 416 free(krb5_config); 417 krb5_config = NULL; 418 } 419 420 421 /* 422 * Generate a krb5.conf file for testing and set KRB5_CONFIG to point to it. 423 * The [appdefaults] section will be stripped out and the default realm will 424 * be set to the realm specified, if not NULL. This will use config/krb5.conf 425 * in preference, so users can configure the tests by creating that file if 426 * the system file isn't suitable. 427 * 428 * Depends on data/generate-krb5-conf being present in the test suite. 429 */ 430 void 431 kerberos_generate_conf(const char *realm) 432 { 433 char *path; 434 const char *argv[3]; 435 436 if (tmpdir_conf != NULL) 437 kerberos_cleanup_conf(); 438 path = test_file_path("data/generate-krb5-conf"); 439 if (path == NULL) 440 bail("cannot find generate-krb5-conf"); 441 argv[0] = path; 442 argv[1] = realm; 443 argv[2] = NULL; 444 run_setup(argv); 445 test_file_path_free(path); 446 tmpdir_conf = test_tmpdir(); 447 basprintf(&krb5_config, "KRB5_CONFIG=%s/krb5.conf", tmpdir_conf); 448 putenv(krb5_config); 449 if (atexit(kerberos_cleanup_conf) != 0) 450 sysdiag("cannot register cleanup function"); 451 } 452 453 454 /* 455 * The remaining functions in this file are only available if Kerberos 456 * libraries are available. 457 */ 458 #ifdef HAVE_KRB5 459 460 461 /* 462 * Report a Kerberos error and bail out. Takes a long instead of a 463 * krb5_error_code because it can also handle a kadm5_ret_t (which may be a 464 * different size). 465 */ 466 void 467 bail_krb5(krb5_context ctx, long code, const char *format, ...) 468 { 469 const char *k5_msg = NULL; 470 char *message; 471 va_list args; 472 473 if (ctx != NULL) 474 k5_msg = krb5_get_error_message(ctx, (krb5_error_code) code); 475 va_start(args, format); 476 bvasprintf(&message, format, args); 477 va_end(args); 478 if (k5_msg == NULL) 479 bail("%s", message); 480 else 481 bail("%s: %s", message, k5_msg); 482 } 483 484 485 /* 486 * Report a Kerberos error as a diagnostic to stderr. Takes a long instead of 487 * a krb5_error_code because it can also handle a kadm5_ret_t (which may be a 488 * different size). 489 */ 490 void 491 diag_krb5(krb5_context ctx, long code, const char *format, ...) 492 { 493 const char *k5_msg = NULL; 494 char *message; 495 va_list args; 496 497 if (ctx != NULL) 498 k5_msg = krb5_get_error_message(ctx, (krb5_error_code) code); 499 va_start(args, format); 500 bvasprintf(&message, format, args); 501 va_end(args); 502 if (k5_msg == NULL) 503 diag("%s", message); 504 else 505 diag("%s: %s", message, k5_msg); 506 free(message); 507 if (k5_msg != NULL) 508 krb5_free_error_message(ctx, k5_msg); 509 } 510 511 512 /* 513 * Find the principal of the first entry of a keytab and return it. The 514 * caller is responsible for freeing the result with krb5_free_principal. 515 * Exit on error. 516 */ 517 krb5_principal 518 kerberos_keytab_principal(krb5_context ctx, const char *path) 519 { 520 krb5_keytab keytab; 521 krb5_kt_cursor cursor; 522 krb5_keytab_entry entry; 523 krb5_principal princ; 524 krb5_error_code status; 525 526 status = krb5_kt_resolve(ctx, path, &keytab); 527 if (status != 0) 528 bail_krb5(ctx, status, "error opening %s", path); 529 status = krb5_kt_start_seq_get(ctx, keytab, &cursor); 530 if (status != 0) 531 bail_krb5(ctx, status, "error reading %s", path); 532 status = krb5_kt_next_entry(ctx, keytab, &entry, &cursor); 533 if (status != 0) 534 bail("no principal found in keytab file %s", path); 535 status = krb5_copy_principal(ctx, entry.principal, &princ); 536 if (status != 0) 537 bail_krb5(ctx, status, "error copying principal from %s", path); 538 krb5_kt_free_entry(ctx, &entry); 539 krb5_kt_end_seq_get(ctx, keytab, &cursor); 540 krb5_kt_close(ctx, keytab); 541 return princ; 542 } 543 544 #endif /* HAVE_KRB5 */ 545