/*
* Utility functions for tests that use Kerberos.
*
* The core function is kerberos_setup, which loads Kerberos test
* configuration and returns a struct of information. It also supports
* obtaining initial tickets from the configured keytab and setting up
* KRB5CCNAME and KRB5_KTNAME if a Kerberos keytab is present. Also included
* are utility functions for setting up a krb5.conf file and reporting
* Kerberos errors or warnings during testing.
*
* Some of the functionality here is only available if the Kerberos libraries
* are available.
*
* The canonical version of this file is maintained in the rra-c-util package,
* which can be found at .
*
* Written by Russ Allbery
* Copyright 2017 Russ Allbery
* Copyright 2006-2007, 2009-2014
* The Board of Trustees of the Leland Stanford Junior University
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* SPDX-License-Identifier: MIT
*/
#include
#ifdef HAVE_KRB5
# include
#endif
#include
#include
#include
#include
#include
#include
#include
/*
* Disable the requirement that format strings be literals, since it's easier
* to handle the possible patterns for kinit commands as an array.
*/
#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ > 2) || defined(__clang__)
# pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
/*
* These variables hold the allocated configuration struct, the environment to
* point to a different Kerberos ticket cache, keytab, and configuration file,
* and the temporary directories used. We store them so that we can free them
* on exit for cleaner valgrind output, making it easier to find real memory
* leaks in the tested programs.
*/
static struct kerberos_config *config = NULL;
static char *krb5ccname = NULL;
static char *krb5_ktname = NULL;
static char *krb5_config = NULL;
static char *tmpdir_ticket = NULL;
static char *tmpdir_conf = NULL;
/*
* Obtain Kerberos tickets and fill in the principal config entry.
*
* There are two implementations of this function, one if we have native
* Kerberos libraries available and one if we don't. Uses keytab to obtain
* credentials, and fills in the cache member of the provided config struct.
*/
#ifdef HAVE_KRB5
static void
kerberos_kinit(void)
{
char *name, *krbtgt;
krb5_error_code code;
krb5_context ctx;
krb5_ccache ccache;
krb5_principal kprinc;
krb5_keytab keytab;
krb5_get_init_creds_opt *opts;
krb5_creds creds;
const char *realm;
/*
* Determine the principal corresponding to that keytab. We copy the
* memory to ensure that it's allocated in the right memory domain on
* systems where that may matter (like Windows).
*/
code = krb5_init_context(&ctx);
if (code != 0)
bail_krb5(ctx, code, "error initializing Kerberos");
kprinc = kerberos_keytab_principal(ctx, config->keytab);
code = krb5_unparse_name(ctx, kprinc, &name);
if (code != 0)
bail_krb5(ctx, code, "error unparsing name");
krb5_free_principal(ctx, kprinc);
config->principal = bstrdup(name);
krb5_free_unparsed_name(ctx, name);
/* Now do the Kerberos initialization. */
code = krb5_cc_default(ctx, &ccache);
if (code != 0)
bail_krb5(ctx, code, "error setting ticket cache");
code = krb5_parse_name(ctx, config->principal, &kprinc);
if (code != 0)
bail_krb5(ctx, code, "error parsing principal %s", config->principal);
realm = krb5_principal_get_realm(ctx, kprinc);
basprintf(&krbtgt, "krbtgt/%s@%s", realm, realm);
code = krb5_kt_resolve(ctx, config->keytab, &keytab);
if (code != 0)
bail_krb5(ctx, code, "cannot open keytab %s", config->keytab);
code = krb5_get_init_creds_opt_alloc(ctx, &opts);
if (code != 0)
bail_krb5(ctx, code, "cannot allocate credential options");
krb5_get_init_creds_opt_set_default_flags(ctx, NULL, realm, opts);
krb5_get_init_creds_opt_set_forwardable(opts, 0);
krb5_get_init_creds_opt_set_proxiable(opts, 0);
code = krb5_get_init_creds_keytab(ctx, &creds, kprinc, keytab, 0, krbtgt,
opts);
if (code != 0)
bail_krb5(ctx, code, "cannot get Kerberos tickets");
code = krb5_cc_initialize(ctx, ccache, kprinc);
if (code != 0)
bail_krb5(ctx, code, "error initializing ticket cache");
code = krb5_cc_store_cred(ctx, ccache, &creds);
if (code != 0)
bail_krb5(ctx, code, "error storing credentials");
krb5_cc_close(ctx, ccache);
krb5_free_cred_contents(ctx, &creds);
krb5_kt_close(ctx, keytab);
krb5_free_principal(ctx, kprinc);
krb5_get_init_creds_opt_free(ctx, opts);
krb5_free_context(ctx);
free(krbtgt);
}
#else /* !HAVE_KRB5 */
static void
kerberos_kinit(void)
{
static const char * const format[] = {
"kinit --no-afslog -k -t %s %s >/dev/null 2>&1 /dev/null 2>&1 /dev/null 2>&1 /dev/null 2>&1 keytab);
config->keytab = NULL;
return;
}
file = fopen(path, "r");
if (file == NULL) {
test_file_path_free(path);
return;
}
test_file_path_free(path);
if (fgets(principal, sizeof(principal), file) == NULL)
bail("cannot read %s", path);
fclose(file);
if (principal[strlen(principal) - 1] != '\n')
bail("no newline in %s", path);
principal[strlen(principal) - 1] = '\0';
config->principal = bstrdup(principal);
/* Now do the Kerberos initialization. */
for (i = 0; i < ARRAY_SIZE(format); i++) {
basprintf(&command, format[i], config->keytab, principal);
status = system(command);
free(command);
if (status != -1 && WEXITSTATUS(status) == 0)
break;
}
if (status == -1 || WEXITSTATUS(status) != 0)
bail("cannot get Kerberos tickets");
}
#endif /* !HAVE_KRB5 */
/*
* Free all the memory associated with our Kerberos setup, but don't remove
* the ticket cache. This is used when cleaning up on exit from a non-primary
* process so that test programs that fork don't remove the ticket cache still
* used by the main program.
*/
static void
kerberos_free(void)
{
test_tmpdir_free(tmpdir_ticket);
tmpdir_ticket = NULL;
if (config != NULL) {
test_file_path_free(config->keytab);
free(config->principal);
free(config->cache);
free(config->userprinc);
free(config->username);
free(config->password);
free(config->pkinit_principal);
free(config->pkinit_cert);
free(config);
config = NULL;
}
if (krb5ccname != NULL) {
putenv((char *) "KRB5CCNAME=");
free(krb5ccname);
krb5ccname = NULL;
}
if (krb5_ktname != NULL) {
putenv((char *) "KRB5_KTNAME=");
free(krb5_ktname);
krb5_ktname = NULL;
}
}
/*
* Clean up at the end of a test. This removes the ticket cache and resets
* and frees the memory allocated for the environment variables so that
* valgrind output on test suites is cleaner. Most of the work is done by
* kerberos_free, but this function also deletes the ticket cache.
*/
void
kerberos_cleanup(void)
{
char *path;
if (tmpdir_ticket != NULL) {
basprintf(&path, "%s/krb5cc_test", tmpdir_ticket);
unlink(path);
free(path);
}
kerberos_free();
}
/*
* The cleanup handler for the TAP framework. Call kerberos_cleanup if we're
* in the primary process and kerberos_free if not. The first argument, which
* indicates whether the test succeeded or not, is ignored, since we need to
* do the same thing either way.
*/
static void
kerberos_cleanup_handler(int success UNUSED, int primary)
{
if (primary)
kerberos_cleanup();
else
kerberos_free();
}
/*
* Obtain Kerberos tickets for the principal specified in config/principal
* using the keytab specified in config/keytab, both of which are presumed to
* be in tests in either the build or the source tree. Also sets KRB5_KTNAME
* and KRB5CCNAME.
*
* Returns the contents of config/principal in newly allocated memory or NULL
* if Kerberos tests are apparently not configured. If Kerberos tests are
* configured but something else fails, calls bail.
*/
struct kerberos_config *
kerberos_setup(enum kerberos_needs needs)
{
char *path;
char buffer[BUFSIZ];
FILE *file = NULL;
/* If we were called before, clean up after the previous run. */
if (config != NULL)
kerberos_cleanup();
config = bcalloc(1, sizeof(struct kerberos_config));
/*
* If we have a config/keytab file, set the KRB5CCNAME and KRB5_KTNAME
* environment variables and obtain initial tickets.
*/
config->keytab = test_file_path("config/keytab");
if (config->keytab == NULL) {
if (needs == TAP_KRB_NEEDS_KEYTAB || needs == TAP_KRB_NEEDS_BOTH)
skip_all("Kerberos tests not configured");
} else {
tmpdir_ticket = test_tmpdir();
basprintf(&config->cache, "%s/krb5cc_test", tmpdir_ticket);
basprintf(&krb5ccname, "KRB5CCNAME=%s/krb5cc_test", tmpdir_ticket);
basprintf(&krb5_ktname, "KRB5_KTNAME=%s", config->keytab);
putenv(krb5ccname);
putenv(krb5_ktname);
kerberos_kinit();
}
/*
* If we have a config/password file, read it and fill out the relevant
* members of our config struct.
*/
path = test_file_path("config/password");
if (path != NULL)
file = fopen(path, "r");
if (file == NULL) {
if (needs == TAP_KRB_NEEDS_PASSWORD || needs == TAP_KRB_NEEDS_BOTH)
skip_all("Kerberos tests not configured");
} else {
if (fgets(buffer, sizeof(buffer), file) == NULL)
bail("cannot read %s", path);
if (buffer[strlen(buffer) - 1] != '\n')
bail("no newline in %s", path);
buffer[strlen(buffer) - 1] = '\0';
config->userprinc = bstrdup(buffer);
if (fgets(buffer, sizeof(buffer), file) == NULL)
bail("cannot read password from %s", path);
fclose(file);
if (buffer[strlen(buffer) - 1] != '\n')
bail("password too long in %s", path);
buffer[strlen(buffer) - 1] = '\0';
config->password = bstrdup(buffer);
/*
* Strip the realm from the principal and set realm and username.
* This is not strictly correct; it doesn't cope with escaped @-signs
* or enterprise names.
*/
config->username = bstrdup(config->userprinc);
config->realm = strchr(config->username, '@');
if (config->realm == NULL)
bail("test principal has no realm");
*config->realm = '\0';
config->realm++;
}
test_file_path_free(path);
/*
* If we have PKINIT configuration, read it and fill out the relevant
* members of our config struct.
*/
path = test_file_path("config/pkinit-principal");
if (path != NULL)
file = fopen(path, "r");
if (path != NULL && file != NULL) {
if (fgets(buffer, sizeof(buffer), file) == NULL)
bail("cannot read %s", path);
if (buffer[strlen(buffer) - 1] != '\n')
bail("no newline in %s", path);
buffer[strlen(buffer) - 1] = '\0';
fclose(file);
test_file_path_free(path);
path = test_file_path("config/pkinit-cert");
if (path != NULL) {
config->pkinit_principal = bstrdup(buffer);
config->pkinit_cert = bstrdup(path);
}
}
test_file_path_free(path);
if (config->pkinit_cert == NULL && (needs & TAP_KRB_NEEDS_PKINIT) != 0)
skip_all("PKINIT tests not configured");
/*
* Register the cleanup function so that the caller doesn't have to do
* explicit cleanup.
*/
test_cleanup_register(kerberos_cleanup_handler);
/* Return the configuration. */
return config;
}
/*
* Clean up the krb5.conf file generated by kerberos_generate_conf and free
* the memory used to set the environment variable. This doesn't fail if the
* file and variable are already gone, allowing it to be harmlessly run
* multiple times.
*
* Normally called via an atexit handler.
*/
void
kerberos_cleanup_conf(void)
{
char *path;
if (tmpdir_conf != NULL) {
basprintf(&path, "%s/krb5.conf", tmpdir_conf);
unlink(path);
free(path);
test_tmpdir_free(tmpdir_conf);
tmpdir_conf = NULL;
}
putenv((char *) "KRB5_CONFIG=");
free(krb5_config);
krb5_config = NULL;
}
/*
* Generate a krb5.conf file for testing and set KRB5_CONFIG to point to it.
* The [appdefaults] section will be stripped out and the default realm will
* be set to the realm specified, if not NULL. This will use config/krb5.conf
* in preference, so users can configure the tests by creating that file if
* the system file isn't suitable.
*
* Depends on data/generate-krb5-conf being present in the test suite.
*/
void
kerberos_generate_conf(const char *realm)
{
char *path;
const char *argv[3];
if (tmpdir_conf != NULL)
kerberos_cleanup_conf();
path = test_file_path("data/generate-krb5-conf");
if (path == NULL)
bail("cannot find generate-krb5-conf");
argv[0] = path;
argv[1] = realm;
argv[2] = NULL;
run_setup(argv);
test_file_path_free(path);
tmpdir_conf = test_tmpdir();
basprintf(&krb5_config, "KRB5_CONFIG=%s/krb5.conf", tmpdir_conf);
putenv(krb5_config);
if (atexit(kerberos_cleanup_conf) != 0)
sysdiag("cannot register cleanup function");
}
/*
* The remaining functions in this file are only available if Kerberos
* libraries are available.
*/
#ifdef HAVE_KRB5
/*
* Report a Kerberos error and bail out. Takes a long instead of a
* krb5_error_code because it can also handle a kadm5_ret_t (which may be a
* different size).
*/
void
bail_krb5(krb5_context ctx, long code, const char *format, ...)
{
const char *k5_msg = NULL;
char *message;
va_list args;
if (ctx != NULL)
k5_msg = krb5_get_error_message(ctx, (krb5_error_code) code);
va_start(args, format);
bvasprintf(&message, format, args);
va_end(args);
if (k5_msg == NULL)
bail("%s", message);
else
bail("%s: %s", message, k5_msg);
}
/*
* Report a Kerberos error as a diagnostic to stderr. Takes a long instead of
* a krb5_error_code because it can also handle a kadm5_ret_t (which may be a
* different size).
*/
void
diag_krb5(krb5_context ctx, long code, const char *format, ...)
{
const char *k5_msg = NULL;
char *message;
va_list args;
if (ctx != NULL)
k5_msg = krb5_get_error_message(ctx, (krb5_error_code) code);
va_start(args, format);
bvasprintf(&message, format, args);
va_end(args);
if (k5_msg == NULL)
diag("%s", message);
else
diag("%s: %s", message, k5_msg);
free(message);
if (k5_msg != NULL)
krb5_free_error_message(ctx, k5_msg);
}
/*
* Find the principal of the first entry of a keytab and return it. The
* caller is responsible for freeing the result with krb5_free_principal.
* Exit on error.
*/
krb5_principal
kerberos_keytab_principal(krb5_context ctx, const char *path)
{
krb5_keytab keytab;
krb5_kt_cursor cursor;
krb5_keytab_entry entry;
krb5_principal princ;
krb5_error_code status;
status = krb5_kt_resolve(ctx, path, &keytab);
if (status != 0)
bail_krb5(ctx, status, "error opening %s", path);
status = krb5_kt_start_seq_get(ctx, keytab, &cursor);
if (status != 0)
bail_krb5(ctx, status, "error reading %s", path);
status = krb5_kt_next_entry(ctx, keytab, &entry, &cursor);
if (status != 0)
bail("no principal found in keytab file %s", path);
status = krb5_copy_principal(ctx, entry.principal, &princ);
if (status != 0)
bail_krb5(ctx, status, "error copying principal from %s", path);
krb5_kt_free_entry(ctx, &entry);
krb5_kt_end_seq_get(ctx, keytab, &cursor);
krb5_kt_close(ctx, keytab);
return princ;
}
#endif /* HAVE_KRB5 */