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
kerberos_kinit(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
kerberos_kinit(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
kerberos_free(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
kerberos_cleanup(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
kerberos_cleanup_handler(int success UNUSED,int primary)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 *
kerberos_setup(enum kerberos_needs needs)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");
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)
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
kerberos_cleanup_conf(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
kerberos_generate_conf(const char * realm)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
bail_krb5(krb5_context ctx,long code,const char * format,...)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
diag_krb5(krb5_context ctx,long code,const char * format,...)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
kerberos_keytab_principal(krb5_context ctx,const char * path)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