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