]> eyrie.org Git - kerberos/krb5-strength.git/blob - tests/tap/kerberos.c
New upstream version 3.3
[kerberos/krb5-strength.git] / tests / tap / kerberos.c
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, 2022, 2023 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 = 0;
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 void
215 kerberos_free(struct kerberos_config *config_arg)
216 {
217     if (config_arg != config)
218         bail("invalid argument to kerberos_free");
219     test_tmpdir_free(tmpdir_ticket);
220     tmpdir_ticket = NULL;
221     if (config != NULL) {
222         test_file_path_free(config->keytab);
223         free(config->principal);
224         free(config->cache);
225         free(config->userprinc);
226         free(config->username);
227         free(config->password);
228         free(config->pkinit_principal);
229         free(config->pkinit_cert);
230
231         /*
232          * Free config_arg rather than config, since otherwise cppcheck thinks
233          * that config_arg could be const, which while technically true would
234          * look very weird since config_arg is invalidated by calling this
235          * function.
236          */
237         free(config_arg);
238         config = NULL;
239     }
240     if (krb5ccname != NULL) {
241         putenv((char *) "KRB5CCNAME=");
242         free(krb5ccname);
243         krb5ccname = NULL;
244     }
245     if (krb5_ktname != NULL) {
246         putenv((char *) "KRB5_KTNAME=");
247         free(krb5_ktname);
248         krb5_ktname = NULL;
249     }
250 }
251
252
253 /*
254  * Clean up at the end of a test.  This removes the ticket cache and resets
255  * and frees the memory allocated for the environment variables so that
256  * valgrind output on test suites is cleaner.  Most of the work is done by
257  * kerberos_free, but this function also deletes the ticket cache.
258  */
259 void
260 kerberos_cleanup(void)
261 {
262     char *path;
263
264     if (tmpdir_ticket != NULL) {
265         basprintf(&path, "%s/krb5cc_test", tmpdir_ticket);
266         unlink(path);
267         free(path);
268     }
269     kerberos_free(config);
270 }
271
272
273 /*
274  * The cleanup handler for the TAP framework.  Call kerberos_cleanup if we're
275  * in the primary process and kerberos_free if not.  The first argument, which
276  * indicates whether the test succeeded or not, is ignored, since we need to
277  * do the same thing either way.
278  */
279 static void
280 kerberos_cleanup_handler(int success UNUSED, int primary)
281 {
282     if (primary)
283         kerberos_cleanup();
284     else
285         kerberos_free(config);
286 }
287
288
289 /*
290  * Obtain Kerberos tickets for the principal specified in config/principal
291  * using the keytab specified in config/keytab, both of which are presumed to
292  * be in tests in either the build or the source tree.  Also sets KRB5_KTNAME
293  * and KRB5CCNAME.
294  *
295  * Returns the contents of config/principal in newly allocated memory or NULL
296  * if Kerberos tests are apparently not configured.  If Kerberos tests are
297  * configured but something else fails, calls bail.
298  */
299 struct kerberos_config *
300 kerberos_setup(enum kerberos_needs needs)
301 {
302     char *path;
303     char buffer[BUFSIZ];
304     FILE *file = NULL;
305
306     /* If we were called before, clean up after the previous run. */
307     if (config != NULL)
308         kerberos_cleanup();
309     config = bcalloc(1, sizeof(struct kerberos_config));
310
311     /*
312      * If we have a config/keytab file, set the KRB5CCNAME and KRB5_KTNAME
313      * environment variables and obtain initial tickets.
314      */
315     config->keytab = test_file_path("config/keytab");
316     if (config->keytab == NULL) {
317         if (needs == TAP_KRB_NEEDS_KEYTAB || needs == TAP_KRB_NEEDS_BOTH)
318             skip_all("Kerberos tests not configured");
319     } else {
320         tmpdir_ticket = test_tmpdir();
321         basprintf(&config->cache, "%s/krb5cc_test", tmpdir_ticket);
322         basprintf(&krb5ccname, "KRB5CCNAME=%s/krb5cc_test", tmpdir_ticket);
323         basprintf(&krb5_ktname, "KRB5_KTNAME=%s", config->keytab);
324         putenv(krb5ccname);
325         putenv(krb5_ktname);
326         kerberos_kinit();
327     }
328
329     /*
330      * If we have a config/password file, read it and fill out the relevant
331      * members of our config struct.
332      */
333     path = test_file_path("config/password");
334     if (path != NULL)
335         file = fopen(path, "r");
336     if (file == NULL) {
337         if (needs == TAP_KRB_NEEDS_PASSWORD || needs == TAP_KRB_NEEDS_BOTH)
338             skip_all("Kerberos tests not configured");
339     } else {
340         if (fgets(buffer, sizeof(buffer), file) == NULL)
341             bail("cannot read %s", path);
342         if (buffer[strlen(buffer) - 1] != '\n')
343             bail("no newline in %s", path);
344         buffer[strlen(buffer) - 1] = '\0';
345         config->userprinc = bstrdup(buffer);
346         if (fgets(buffer, sizeof(buffer), file) == NULL)
347             bail("cannot read password from %s", path);
348         fclose(file);
349         if (buffer[strlen(buffer) - 1] != '\n')
350             bail("password too long in %s", path);
351         buffer[strlen(buffer) - 1] = '\0';
352         config->password = bstrdup(buffer);
353
354         /*
355          * Strip the realm from the principal and set realm and username.
356          * This is not strictly correct; it doesn't cope with escaped @-signs
357          * or enterprise names.
358          */
359         config->username = bstrdup(config->userprinc);
360         config->realm = strchr(config->username, '@');
361         if (config->realm == NULL)
362             bail("test principal has no realm");
363         *config->realm = '\0';
364         config->realm++;
365     }
366     test_file_path_free(path);
367
368     /*
369      * If we have PKINIT configuration, read it and fill out the relevant
370      * members of our config struct.
371      */
372     path = test_file_path("config/pkinit-principal");
373     if (path != NULL)
374         file = fopen(path, "r");
375     if (path != NULL && file != NULL) {
376         if (fgets(buffer, sizeof(buffer), file) == NULL)
377             bail("cannot read %s", path);
378         if (buffer[strlen(buffer) - 1] != '\n')
379             bail("no newline in %s", path);
380         buffer[strlen(buffer) - 1] = '\0';
381         fclose(file);
382         test_file_path_free(path);
383         path = test_file_path("config/pkinit-cert");
384         if (path != NULL) {
385             config->pkinit_principal = bstrdup(buffer);
386             config->pkinit_cert = bstrdup(path);
387         }
388     }
389     test_file_path_free(path);
390     if (config->pkinit_cert == NULL && (needs & TAP_KRB_NEEDS_PKINIT) != 0)
391         skip_all("PKINIT tests not configured");
392
393     /*
394      * Register the cleanup function so that the caller doesn't have to do
395      * explicit cleanup.
396      */
397     test_cleanup_register(kerberos_cleanup_handler);
398
399     /* Return the configuration. */
400     return config;
401 }
402
403
404 /*
405  * Clean up the krb5.conf file generated by kerberos_generate_conf and free
406  * the memory used to set the environment variable.  This doesn't fail if the
407  * file and variable are already gone, allowing it to be harmlessly run
408  * multiple times.
409  *
410  * Normally called via an atexit handler.
411  */
412 void
413 kerberos_cleanup_conf(void)
414 {
415     char *path;
416
417     if (tmpdir_conf != NULL) {
418         basprintf(&path, "%s/krb5.conf", tmpdir_conf);
419         unlink(path);
420         free(path);
421         test_tmpdir_free(tmpdir_conf);
422         tmpdir_conf = NULL;
423     }
424     putenv((char *) "KRB5_CONFIG=");
425     free(krb5_config);
426     krb5_config = NULL;
427 }
428
429
430 /*
431  * Generate a krb5.conf file for testing and set KRB5_CONFIG to point to it.
432  * The [appdefaults] section will be stripped out and the default realm will
433  * be set to the realm specified, if not NULL.  This will use config/krb5.conf
434  * in preference, so users can configure the tests by creating that file if
435  * the system file isn't suitable.
436  *
437  * Depends on data/generate-krb5-conf being present in the test suite.
438  */
439 void
440 kerberos_generate_conf(const char *realm)
441 {
442     char *path;
443     const char *argv[3];
444
445     if (tmpdir_conf != NULL)
446         kerberos_cleanup_conf();
447     path = test_file_path("data/generate-krb5-conf");
448     if (path == NULL)
449         bail("cannot find generate-krb5-conf");
450     argv[0] = path;
451     argv[1] = realm;
452     argv[2] = NULL;
453     run_setup(argv);
454     test_file_path_free(path);
455     tmpdir_conf = test_tmpdir();
456     basprintf(&krb5_config, "KRB5_CONFIG=%s/krb5.conf", tmpdir_conf);
457     putenv(krb5_config);
458     if (atexit(kerberos_cleanup_conf) != 0)
459         sysdiag("cannot register cleanup function");
460 }
461
462
463 /*
464  * The remaining functions in this file are only available if Kerberos
465  * libraries are available.
466  */
467 #ifdef HAVE_KRB5
468
469
470 /*
471  * Report a Kerberos error and bail out.  Takes a long instead of a
472  * krb5_error_code because it can also handle a kadm5_ret_t (which may be a
473  * different size).
474  */
475 void
476 bail_krb5(krb5_context ctx, long code, const char *format, ...)
477 {
478     const char *k5_msg = NULL;
479     char *message;
480     va_list args;
481
482     if (ctx != NULL)
483         k5_msg = krb5_get_error_message(ctx, (krb5_error_code) code);
484     va_start(args, format);
485     bvasprintf(&message, format, args);
486     va_end(args);
487     if (k5_msg == NULL)
488         bail("%s", message);
489     else
490         bail("%s: %s", message, k5_msg);
491 }
492
493
494 /*
495  * Report a Kerberos error as a diagnostic to stderr.  Takes a long instead of
496  * a krb5_error_code because it can also handle a kadm5_ret_t (which may be a
497  * different size).
498  */
499 void
500 diag_krb5(krb5_context ctx, long code, const char *format, ...)
501 {
502     const char *k5_msg = NULL;
503     char *message;
504     va_list args;
505
506     if (ctx != NULL)
507         k5_msg = krb5_get_error_message(ctx, (krb5_error_code) code);
508     va_start(args, format);
509     bvasprintf(&message, format, args);
510     va_end(args);
511     if (k5_msg == NULL)
512         diag("%s", message);
513     else
514         diag("%s: %s", message, k5_msg);
515     free(message);
516     if (k5_msg != NULL)
517         krb5_free_error_message(ctx, k5_msg);
518 }
519
520
521 /*
522  * Find the principal of the first entry of a keytab and return it.  The
523  * caller is responsible for freeing the result with krb5_free_principal.
524  * Exit on error.
525  */
526 krb5_principal
527 kerberos_keytab_principal(krb5_context ctx, const char *path)
528 {
529     krb5_keytab keytab;
530     krb5_kt_cursor cursor;
531     krb5_keytab_entry entry;
532     krb5_principal princ;
533     krb5_error_code status;
534
535     status = krb5_kt_resolve(ctx, path, &keytab);
536     if (status != 0)
537         bail_krb5(ctx, status, "error opening %s", path);
538     status = krb5_kt_start_seq_get(ctx, keytab, &cursor);
539     if (status != 0)
540         bail_krb5(ctx, status, "error reading %s", path);
541     status = krb5_kt_next_entry(ctx, keytab, &entry, &cursor);
542     if (status != 0)
543         bail("no principal found in keytab file %s", path);
544     status = krb5_copy_principal(ctx, entry.principal, &princ);
545     if (status != 0)
546         bail_krb5(ctx, status, "error copying principal from %s", path);
547     krb5_kt_free_entry(ctx, &entry);
548     krb5_kt_end_seq_get(ctx, keytab, &cursor);
549     krb5_kt_close(ctx, keytab);
550     return princ;
551 }
552
553 #endif /* HAVE_KRB5 */