From 10f1d26cc70f9382fb084a122176635fa2cb64a8 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Tue, 24 Sep 2013 15:22:58 -0700 Subject: [PATCH] Rewrite the Heimdal plugin tests in C Add a new Perl program to generate a C version of the password test data from the JSON source, and run that program during autogen so that we can ship the source in the distribution and not require JSON support for the basic test suite. Add a new shell script to generate the necessary krb5.conf file. Rewrite the Heimdal plugin test in pure C, using the generated C data and calling the shell script to create the necessary krb5.conf file. --- .gitignore | 3 +- Makefile.am | 19 +- autogen | 3 + configure.ac | 6 + portable/krb5-extra.c | 35 +++ portable/krb5.h | 40 +++ tests/data/make-c-data | 218 ++++++++++++++++ tests/data/make-krb5-conf | 39 +++ tests/data/password-tests.h | 30 +++ tests/heimdal/plugin-t | 101 -------- tests/heimdal/plugin-t.c | 214 ++++++++++++++++ tests/heimdal/plugin.c | 141 ----------- tests/mit/plugin-t | 2 +- tests/tap/kerberos.c | 488 ++++++++++++++++++++++++++++++++++++ tests/tap/kerberos.h | 125 +++++++++ 15 files changed, 1211 insertions(+), 253 deletions(-) create mode 100755 tests/data/make-c-data create mode 100755 tests/data/make-krb5-conf create mode 100644 tests/data/password-tests.h delete mode 100755 tests/heimdal/plugin-t create mode 100644 tests/heimdal/plugin-t.c delete mode 100644 tests/heimdal/plugin.c create mode 100644 tests/tap/kerberos.c create mode 100644 tests/tap/kerberos.h diff --git a/.gitignore b/.gitignore index 314059a..cff7964 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,9 @@ /m4/lt~obsolete.m4 /stamp-h1 /tests/data/.placeholder +/tests/data/cracklib.c /tests/data/dictionary.* -/tests/heimdal/plugin +/tests/heimdal/plugin-t /tests/mit/plugin /tests/portable/asprintf-t /tests/portable/snprintf-t diff --git a/Makefile.am b/Makefile.am index 94af198..f5f9cb0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -11,9 +11,8 @@ EXTRA_DIST = .gitignore LICENSE autogen cracklib/HISTORY cracklib/LICENCE \ cracklib/README cracklib/genrules.pl cracklib/mkdict \ external/heimdal-strength.pod patches/README patches/mit-krb5-1.4.4 \ tests/HOWTO tests/TESTS tests/data/wordlist \ - tests/heimdal/external-t tests/heimdal/plugin-t \ - tests/heimdal/pod-spelling-t tests/heimdal/pod-t tests/mit/plugin-t \ - tests/tap/libtap.sh + tests/heimdal/external-t tests/heimdal/pod-spelling-t \ + tests/heimdal/pod-t tests/mit/plugin-t tests/tap/libtap.sh # Do this globally. Everything needs to find the Kerberos headers and # libraries, and if we're using the system CrackLib, add its location @@ -106,7 +105,7 @@ warnings: $(MAKE) V=0 CFLAGS='$(WARNINGS)' $(check_PROGRAMS) # The bits below are for the test suite, not for the main package. -check_PROGRAMS = tests/heimdal/plugin tests/mit/plugin \ +check_PROGRAMS = tests/heimdal/plugin-t tests/mit/plugin \ tests/portable/asprintf-t tests/portable/snprintf-t \ tests/portable/strlcat-t tests/portable/strlcpy-t tests/runtests \ tests/util/messages-t tests/util/xmalloc @@ -116,14 +115,16 @@ endif tests_runtests_CPPFLAGS = -DSOURCE='"$(abs_top_srcdir)/tests"' \ -DBUILD='"$(abs_top_builddir)/tests"' check_LIBRARIES = tests/tap/libtap.a -tests_tap_libtap_a_CPPFLAGS = -I$(abs_top_srcdir)/tests +tests_tap_libtap_a_CPPFLAGS = -I$(abs_top_srcdir)/tests $(KRB5_CPPFLAGS) tests_tap_libtap_a_SOURCES = tests/tap/basic.c tests/tap/basic.h \ - tests/tap/macros.h tests/tap/process.c tests/tap/process.h \ - tests/tap/string.c tests/tap/string.h + tests/tap/kerberos.c tests/tap/kerberos.h tests/tap/macros.h \ + tests/tap/process.c tests/tap/process.h tests/tap/string.c \ + tests/tap/string.h # The actual test programs. -tests_heimdal_plugin_SOURCES = tests/heimdal/plugin.c -tests_heimdal_plugin_LDADD = portable/libportable.la $(KRB5_LIBS) $(DL_LIBS) +tests_heimdal_plugin_t_CPPFLAGS = $(KRB5_CPPFLAGS) +tests_heimdal_plugin_t_LDADD = tests/tap/libtap.a portable/libportable.la \ + $(KRB5_LIBS) $(DL_LIBS) tests_mit_plugin_SOURCES = tests/mit/plugin.c tests_mit_plugin_LDADD = portable/libportable.la $(KRB5_LIBS) $(DL_LIBS) tests_portable_asprintf_t_SOURCES = tests/portable/asprintf-t.c \ diff --git a/autogen b/autogen index 49ba0d2..151330a 100755 --- a/autogen +++ b/autogen @@ -11,3 +11,6 @@ autoreconf -i --force version=`grep '^krb5-strength' NEWS | head -1 | cut -d' ' -f2` pod2man --release="$version" --center='krb5-strength' \ external/heimdal-strength.pod > external/heimdal-strength.1 + +# Generate the C version of our password test data. +tests/data/make-c-data tests/data/cracklib.json > tests/data/cracklib.c diff --git a/configure.ac b/configure.ac index d2d165e..cb08a55 100644 --- a/configure.ac +++ b/configure.ac @@ -34,7 +34,13 @@ AC_CHECK_HEADERS([kadm5/kadm5-pwcheck.h krb5/pwqual_plugin.h], [], [], [RRA_INCLUDES_KRB5]) AC_CHECK_TYPES([krb5_realm], [], [], [RRA_INCLUDES_KRB5]) AC_CHECK_FUNCS([krb5_free_default_realm \ + krb5_principal_get_realm \ + krb5_get_init_creds_opt_alloc \ + krb5_get_init_creds_opt_set_default_flags \ krb5_xfree]) +AC_CHECK_FUNCS([krb5_get_init_creds_opt_free], + [RRA_FUNC_KRB5_GET_INIT_CREDS_OPT_FREE_ARGS]) +AC_CHECK_DECLS([krb5_kt_free_entry], [], [], [RRA_INCLUDES_KRB5]) AC_LIBOBJ([krb5-extra]) RRA_LIB_KRB5_RESTORE diff --git a/portable/krb5-extra.c b/portable/krb5-extra.c index 852e6de..849842c 100644 --- a/portable/krb5-extra.c +++ b/portable/krb5-extra.c @@ -99,3 +99,38 @@ krb5_free_error_message(krb5_context ctx UNUSED, const char *msg) # endif } #endif /* !HAVE_KRB5_FREE_ERROR_MESSAGE */ + + +#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC +/* + * Allocate and initialize a krb5_get_init_creds_opt struct. This code + * assumes that an all-zero bit pattern will create a NULL pointer. + */ +krb5_error_code +krb5_get_init_creds_opt_alloc(krb5_context ctx UNUSED, + krb5_get_init_creds_opt **opts) +{ + *opts = calloc(1, sizeof(krb5_get_init_creds_opt)); + if (*opts == NULL) + return errno; + krb5_get_init_creds_opt_init(*opts); + return 0; +} +#endif /* !HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC */ + + +#ifndef HAVE_KRB5_PRINCIPAL_GET_REALM +/* + * Return the realm of a principal as a const char *. + */ +const char * +krb5_principal_get_realm(krb5_context ctx UNUSED, krb5_const_principal princ) +{ + const krb5_data *data; + + data = krb5_princ_realm(ctx, princ); + if (data == NULL || data->data == NULL) + return NULL; + return data->data; +} +#endif /* !HAVE_KRB5_PRINCIPAL_GET_REALM */ diff --git a/portable/krb5.h b/portable/krb5.h index 71de984..9bc9ef8 100644 --- a/portable/krb5.h +++ b/portable/krb5.h @@ -80,6 +80,46 @@ const char *krb5_get_error_message(krb5_context, krb5_error_code); void krb5_free_error_message(krb5_context, const char *); #endif +/* + * Both current MIT and current Heimdal prefer _opt_alloc and _opt_free, but + * older versions of both require allocating your own struct and calling + * _opt_init. + */ +#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC +krb5_error_code krb5_get_init_creds_opt_alloc(krb5_context, + krb5_get_init_creds_opt **); +#endif +#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_FREE +# ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_FREE_2_ARGS +# define krb5_get_init_creds_opt_free(c, o) krb5_get_init_creds_opt_free(o) +# endif +#else +# define krb5_get_init_creds_opt_free(c, o) free(o) +#endif + +/* Heimdal-specific. */ +#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_DEFAULT_FLAGS +# define krb5_get_init_creds_opt_set_default_flags(c, p, r, o) /* empty */ +#endif + +/* + * Heimdal: krb5_kt_free_entry, MIT: krb5_free_keytab_entry_contents. We + * check for the declaration rather than the function since the function is + * present in older MIT Kerberos libraries but not prototyped. + */ +#if !HAVE_DECL_KRB5_KT_FREE_ENTRY +# define krb5_kt_free_entry(c, e) krb5_free_keytab_entry_contents((c), (e)) +#endif + +/* + * Heimdal provides a nice function that just returns a const char *. On MIT, + * there's an accessor macro that returns the krb5_data pointer, which + * requires more work to get at the underlying char *. + */ +#ifndef HAVE_KRB5_PRINCIPAL_GET_REALM +const char *krb5_principal_get_realm(krb5_context, krb5_const_principal); +#endif + /* Undo default visibility change. */ #pragma GCC visibility pop diff --git a/tests/data/make-c-data b/tests/data/make-c-data new file mode 100755 index 0000000..021b2dc --- /dev/null +++ b/tests/data/make-c-data @@ -0,0 +1,218 @@ +#!/usr/bin/perl +# +# Read a JSON file of password tests and generate C data. +# +# The canonical representation of our password tests is in JSON, but I don't +# want to require a JSON parser for the C tests to run. This script reads the +# JSON input and generates a C data structure that holds all of the tests. + +use 5.010; +use autodie; +use strict; +use warnings; + +use Carp qw(croak); +use File::Basename qw(basename); +use JSON; +use Perl6::Slurp qw(slurp); +use Readonly; + +############################################################################## +# Global variables +############################################################################## + +# The header on the generated source file. +Readonly my $HEADER => <<'END_HEADER'; +/* + * Automatically generated -- do not edit! + * + * This file was automatically generated from the original JSON source file + * for the use in C test programs. To make changes, modify the original + * JSON source or (more rarely) the make-c-data script and run it again. + * + * Copyright 2013 + * The Board of Trustees of the Leland Stanford Junior University + * + * See LICENSE for licensing terms. + */ + +#include + +END_HEADER + +# The list of attributes, in order, whose values go into the C struct. +Readonly my @ATTRIBUTES = qw(name principal password code error); + +# A hash of attributes that should be put in the C struct as they literally +# appear in the JSON, rather than as strings. (In other words, attributes +# that are numbers or C constants.) Only the keys are of interest. +Readonly my %IS_LITERAL_ATTRIBUTE = (code => 1); + +############################################################################## +# Functions +############################################################################## + +# print with error checking and an explicit file handle. autodie +# unfortunately can't help us with these because they can't be prototyped and +# hence can't be overridden. +# +# $fh - Output file handle +# @args - Remaining arguments to print +# +# Returns: undef +# Throws: Text exception on output failure +sub print_fh { + my ($fh, @args) = @_; + print {$fh} @args or croak('print failed'); + return; +} + +# The same for say. +sub say_fh { + my ($fh, @args) = @_; + say {$fh} @args or croak('say failed'); + return; +} + +# Load a password test cases and return them as a list. +# +# $file - The path to the file containing the test data in JSON +# +# Returns: List of anonymous hashes representing password test cases +# Throws: Text exception on failure to load the test data +sub load_password_tests { + my ($file) = @_; + + # Load the test file data into memory. + my $testdata = slurp($file); + + # Decode the JSON into Perl objects and return them. + my $json = JSON->new->utf8; + return $json->decode($testdata); +} + +# Output one struct's data, representing a test case. +# +# $fh - The output file handle to which to send the C data +# $test_ref - The hash reference holding the test data +# +# Returns: undef +# Throws: Text exception on I/O failure +sub output_test { + my ($fh, $test_ref) = @_; + my $prefix = q{ } x 4; + + # Output the data in the order of @ATTRIBUTES. + say_fh($fh, $prefix, "{\n"); + for my $attr (@ATTRIBUTES) { + my $value = $test_ref->{$attr}; + if ($IS_LITERAL_ATTRIBUTE{$attr}) { + $value //= 0; + } else { + $value = defined($value) ? qq{"$value"} : 'NULL'; + } + say_fh($fh, $prefix x 2, $value, q{,}); + } + say_fh($fh, $prefix, '},'); + return; +} + +############################################################################## +# Main routine +############################################################################## + +# Parse command-line arguments. +if (@ARGV != 1) { + die "Syntax: make-c-data \n"; +} +my $datafile = $ARGV[0]; + +# Load the test data. +my $tests_ref = load_password_tests($datafile); + +# Print out the header. +my $name = basename($datafile); +$name =~ s{ [.]json \z }{}xms; +print_fh(\*STDOUT, $HEADER); +say_fh(\*STDOUT, "const struct password_test ${name}_tests[] = {"); + +# Print out the test data. +for my $test_ref (@{$tests_ref}) { + output_test(\*STDOUT, $test_ref); +} + +# Close the struct. +say_fh(\*STDOUT, '};'); + +__END__ + +############################################################################## +# Documentation +############################################################################## + +=for stopwords +Allbery JSON krb5-strength struct sublicense MERCHANTABILITY +NONINFRINGEMENT + +=head1 NAME + +make-c-data - Generate C data from JSON test data for krb5-strength + +=head1 SYNOPSIS + +B I + +=head1 DESCRIPTION + +The canonical form of the password test data for the krb5-strength package +is in JSON, but requiring a C JSON parser to run the test suite (or +writing one) is undesirable. Hence this script. B takes a +JSON file as input, interprets it as a list of password test cases, and +outputs a C file that defines an array of C. That +struct is expected to have the following definition: + + struct password_test { + const char *name; + const char *principal; + const char *password; + krb5_error_code code; + const char *error; + }; + +All JSON objects are expected to have fields corresponding to the above +struct element names. All of them are written as C strings except for +code, where the value from JSON is written as a literal. It should +therefore be either a number or a symbolic constant. + +The written file will also include C, which +should define the above struct and any constants that will be used for +the code field. + +=head1 AUTHOR + +Russ Allbery + +=head1 COPYRIGHT AND LICENSE + +Copyright 2013 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. + +=cut diff --git a/tests/data/make-krb5-conf b/tests/data/make-krb5-conf new file mode 100755 index 0000000..59abf6f --- /dev/null +++ b/tests/data/make-krb5-conf @@ -0,0 +1,39 @@ +#!/bin/sh +# +# Generate a krb5.conf file with an [appdefault] password_dictionary setting +# pointing to the password dictionary we generated for the build. This script +# is used by C tests to set up the environment. +# +# Written by Russ Allbery +# Copyright 2009, 2013 +# The Board of Trustees of the Leland Stanford Junior University +# +# See LICENSE for licensing terms. + +set -e + +# Command-line arguments are the path to the password dictionary, the source +# krb5.conf template, and the directory into which to write the resulting +# krb5.conf file. +dict="$1" +source="$2" +tmpdir="$3" +if [ -z "$tmpdir" ] ; then + echo 'Syntax: make-krb5-conf ' >&2 + exit 1 +fi + +# Copy over the template. +cp "$source" "$tmpdir"/krb5.conf + +# Add the appdefaults section. +cat <>"$tmpdir"/krb5.conf + +[appdefaults] + krb5-strength = { + password_dictionary = $dict + } +EOF + +# Done. +exit 0 diff --git a/tests/data/password-tests.h b/tests/data/password-tests.h new file mode 100644 index 0000000..8c21820 --- /dev/null +++ b/tests/data/password-tests.h @@ -0,0 +1,30 @@ +/* + * Type definition for password test data. + * + * This header provides the struct definition for password test data written + * out by make-c-data. It's included by the test data files. + * + * Written by Russ Allbery + * Copyright 2013 + * The Board of Trustees of the Leland Stanford Junior University + * + * See LICENSE for licensing terms. + */ + +#include +#include + +#include + +/* Heimdal doesn't define KADM5_PASS_Q_GENERIC. */ +#ifndef KADM5_PASS_Q_GENERIC +# define KADM5_PASS_Q_GENERIC KADM5_PASS_Q_DICT +#endif + +struct password_test { + const char *name; + const char *principal; + const char *password; + krb5_error_code code; + const char *error; +}; diff --git a/tests/heimdal/plugin-t b/tests/heimdal/plugin-t deleted file mode 100755 index 4182e36..0000000 --- a/tests/heimdal/plugin-t +++ /dev/null @@ -1,101 +0,0 @@ -#!/bin/sh -# -# Test suite wrapper for the Heimdal shared module API. -# -# Written by Russ Allbery -# Copyright 2009, 2012, 2013 -# The Board of Trustees of the Leland Stanford Junior University -# -# See LICENSE for licensing terms. - -. "$SOURCE/tap/libtap.sh" -cd "$BUILD/data" - -# Run the plugin program to check a password. Takes the test description, -# the principal, the password, the expected exit status, and the expected -# output to standard error. -ok_password () { - desc="$1" - princ="$2" - password="$3" - w_status="$4" - w_stderr="$5" - stderr=`"$BUILD/heimdal/plugin" "$princ" "$password" 2>&1` - status="$?" - echo "# status: $status" - echo "# stderr: $stderr" - ok "$desc: status" [ "$status" -eq "$w_status" ] - ok "$desc: stderr" [ "$stderr" = "$w_stderr" ] -} - -# We need a modified krb5.conf file to add the password_dictionary setting. -# We first generate a modified copy of the krb5.conf file that doesn't contain -# this setting so that we can test error handling. -krb5conf= -for p in /etc/krb5.conf /usr/local/etc/krb5.conf data/krb5.conf ; do - if [ -r "$p" ] ; then - krb5conf="$p" - sed -e '/^[ ]*password_dictionary[ ]*=/d' "$p" > ./krb5.conf - KRB5_CONFIG="./krb5.conf" - export KRB5_CONFIG - break - fi -done -if [ -z "$krb5conf" ] ; then - skip_all 'no krb5.conf found, put one in tests/data/krb5.conf' -fi - -# Check whether we can run the test at all. -"$BUILD/heimdal/plugin" 'test@EXAMPLE.COM' 'test' >/dev/null 2>&1 -if [ $? = 42 ] ; then - rm -f krb5.conf - skip_all 'not built against Heimdal libraries' -fi - -# Okay, we should be good to run the test suite. -plan 28 - -# We don't have a password_dictionary setting, so we should fail with an -# initialization error. -ok_password "no dictionary configured" 'test@EXAMPLE.ORG' 'password' 1 \ - 'password_dictionary not configured in krb5.conf' - -# Now add the password dictionary configuration. -cat <> ./krb5.conf - -[appdefaults] - krb5-strength = { - password_dictionary = $BUILD/data/dictionary - } - -EOF - -# Check the basic functionality. -ok_password "good password" 'test@EXAMPLE.ORG' 'known good password' 0 '' -ok_password "password in dictionary" 'test@EXAMPLE.ORG' 'password' 1 \ - 'it is based on a dictionary word' -ok_password "password in dictionary" 'test@EXAMPLE.ORG' 'bitterbane' 1 \ - 'it is based on a dictionary word' -ok_password "password in dictionary" 'test@EXAMPLE.ORG' 'enabrettib' 1 \ - 'it is based on a (reversed) dictionary word' -ok_password "password too short" 'test@EXAMPLE.ORG' 'food' 1 \ - "it is too short" -ok_password "password way too short" 'test@EXAMPLE.ORG' 'foo' 1 \ - "it's WAY too short" -ok_password "password empty" 'test@EXAMPLE.ORG' '' 1 \ - "it's WAY too short" -ok_password "password all whitespace" 'test@EXAMPLE.ORG' ' ' 1 \ - 'it does not contain enough DIFFERENT characters' -ok_password "password too simplistic" 'test@EXAMPLE.ORG' 'abcdefghi' 1 \ - 'it is too simplistic/systematic' -ok_password "not enough characters" 'test@EXAMPLE.ORG' '22413411' 1 \ - 'it does not contain enough DIFFERENT characters' -ok_password "password based on principal" 'someuser@EXAMPLE.ORG' 'someuser' \ - 1 'Password based on username' -ok_password "password based on principal" 'someuser@EXAMPLE.ORG' 'resuemos' \ - 1 'Password based on username' -ok_password "password is principal" 'test@EXAMPLE.ORG' 'test@EXAMPLE.ORG' \ - 1 'Password based on username' - -# Clean up. -rm -f krb5.conf diff --git a/tests/heimdal/plugin-t.c b/tests/heimdal/plugin-t.c new file mode 100644 index 0000000..9bbec79 --- /dev/null +++ b/tests/heimdal/plugin-t.c @@ -0,0 +1,214 @@ +/* + * Test for the Heimdal shared module API. + * + * Written by Russ Allbery + * Copyright 2009, 2013 + * The Board of Trustees of the Leland Stanford Junior University + * + * See LICENSE for licensing terms. + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +/* kadm5-pwcheck.h isn't always installed by Heimdal. */ +#ifdef HAVE_KADM5_PWCHECK_H +# include +#else +# define KADM5_PASSWD_VERSION_V1 1 + +typedef int +(*kadm5_passwd_quality_check_func)(krb5_context context, + krb5_principal principal, + krb5_data *password, + const char *tuning, + char *message, + size_t length); + +struct kadm5_pw_policy_check_func { + const char *name; + kadm5_passwd_quality_check_func func; +}; + +struct kadm5_pw_policy_verifier { + const char *name; + int version; + const char *vendor; + const struct kadm5_pw_policy_check_func *funcs; +}; +#endif /* !HAVE_KADM5_PWCHECK_H */ + +/* + * The password test data, generated from the JSON source. Defines an array + * named cracklib_tests. + */ +#include + + +/* + * Loads the Heimdal password change plugin and tests that its metadata is + * correct. Returns a pointer to the kadm5_pw_policy_verifier struct or bails + * on failure to load the plugin. + */ +static struct kadm5_pw_policy_verifier * +load_plugin(void) +{ + char *path; + void *handle; + struct kadm5_pw_policy_verifier *verifier; + + /* Load the module. */ + path = test_file_path("../plugin/.libs/passwd_strength.so"); + if (path == NULL) + bail("cannot find plugin"); + handle = dlopen(path, RTLD_NOW); + if (handle == NULL) + sysbail("cannot dlopen %s: %s", path, dlerror()); + test_file_path_free(path); + + /* Find the dispatch table and do a basic sanity check. */ + verifier = dlsym(handle, "kadm5_password_verifier"); + if (verifier == NULL) + sysbail("cannot get kadm5_password_verifier symbol: %s", dlerror()); + if (verifier->funcs == NULL || verifier->funcs[0].func == NULL) + bail("no verifier functions in module"); + + /* Verify the metadata. */ + is_string("krb5-strength", verifier->name, "Module name"); + is_string("Russ Allbery", verifier->vendor, "Module vendor"); + is_int(KADM5_PASSWD_VERSION_V1, verifier->version, "Module version"); + is_string("krb5-strength", verifier->funcs[0].name, + "Module function name"); + ok(verifier->funcs[1].name == NULL, "Only one function in module"); + + /* Return the dispatch table. */ + return verifier; +} + + +/* + * Given the dispatch table and a test case, call out to the password strength + * checking module and check the results. + */ +static void +is_password_test(const struct kadm5_pw_policy_verifier *verifier, + const struct password_test *test) +{ + krb5_context ctx; + krb5_principal princ; + krb5_error_code code; + krb5_data password; + int result; + char error[BUFSIZ] = ""; + + /* Obtain a Kerberos context to use for parsing principal names. */ + code = krb5_init_context(&ctx); + if (code != 0) + bail_krb5(ctx, code, "cannot initialize Kerberos context"); + + /* Translate the test data into the form that the verifier expects. */ + code = krb5_parse_name(ctx, test->principal, &princ); + if (code != 0) + bail_krb5(ctx, code, "cannot parse principal %s", test->principal); + password.data = (char *) test->password; + password.length = strlen(test->password); + + /* Call the verifier. */ + result = (verifier->funcs[0].func)(ctx, princ, &password, NULL, error, + sizeof(error)); + + /* Heimdal only returns 0 or 1, so translate the expected code. */ + is_int(test->code == 0 ? 0 : 1, result, "%s (status)", test->name); + is_string(test->error == NULL ? "" : test->error, error, "%s (error)", + test->name); + + /* Free data structures. */ + krb5_free_principal(ctx, princ); + krb5_free_context(ctx); +} + + +int +main(void) +{ + char *path, *krb5_config, *krb5_config_empty, *tmpdir; + char *setup_argv[5]; + size_t i; + struct kadm5_pw_policy_verifier *verifier; + struct password_test no_dictionary_test = { + "no dictionary configured", + "test@EXAMPLE.ORG", + "password", + 1, + "password_dictionary not configured in krb5.conf", + }; + + /* If we're not building with Heimdal, we can't run this test. */ +#ifndef HAVE_KRB5_REALM + skip_all("not built against Heimdal libraries"); +#endif + + /* + * Calculate how many tests we have. There are five tests for the module + * metadata, one more password test than the list of password tests we + * have configured, and two tests per password test. + */ + plan(5 + (ARRAY_SIZE(cracklib_tests) + 1) * 2); + + /* Start with the krb5.conf that contains no dictionary configuration. */ + path = test_file_path("data/krb5.conf"); + if (path == NULL) + bail("cannot find data/krb5.conf in the test suite"); + basprintf(&krb5_config_empty, "KRB5_CONFIG=%s", path); + putenv(krb5_config_empty); + + /* Load the plugin. */ + verifier = load_plugin(); + + /* Try an initial password verification with no dictionary configured. */ + is_password_test(verifier, &no_dictionary_test); + + /* Set up our krb5.conf with the dictionary configuration. */ + setup_argv[0] = test_file_path("data/make-krb5-conf"); + if (setup_argv[0] == NULL) + bail("cannot find data/make-krb5-conf in the test suite"); + basprintf(&setup_argv[1], "%s/data/dictionary", getenv("BUILD")); + tmpdir = test_tmpdir(); + setup_argv[2] = path; + setup_argv[3] = tmpdir; + setup_argv[4] = NULL; + run_setup((const char **) setup_argv); + test_file_path_free(setup_argv[0]); + free(setup_argv[1]); + test_file_path_free(path); + + /* Point KRB5_CONFIG at the newly-generated krb5.conf file. */ + basprintf(&krb5_config, "KRB5_CONFIG=%s/krb5.conf", tmpdir); + putenv(krb5_config); + free(krb5_config_empty); + + /* Now, run all of the tests. */ + for (i = 0; i < ARRAY_SIZE(cracklib_tests); i++) + is_password_test(verifier, &cracklib_tests[i]); + + /* Manually clean up after the results of make-krb5-conf. */ + basprintf(&path, "%s/krb5.conf", tmpdir); + unlink(path); + free(path); + test_tmpdir_free(tmpdir); + + /* Keep valgrind clean by freeing environmental memory. */ + putenv((char *) "KRB5_CONFIG="); + free(krb5_config); + return 0; +} diff --git a/tests/heimdal/plugin.c b/tests/heimdal/plugin.c deleted file mode 100644 index 056dc91..0000000 --- a/tests/heimdal/plugin.c +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Test for the Heimdal shared module API. - * - * Written by Russ Allbery - * Copyright 2009, 2013 - * The Board of Trustees of the Leland Stanford Junior University - * - * See LICENSE for licensing terms. - */ - -#include -#include -#include - -#include -#include - -/* kadm5-pwcheck.h isn't always installed by Heimdal. */ -#ifdef HAVE_KADM5_PWCHECK_H -# include -#else -# define KADM5_PASSWD_VERSION_V1 1 - -typedef int -(*kadm5_passwd_quality_check_func)(krb5_context context, - krb5_principal principal, - krb5_data *password, - const char *tuning, - char *message, - size_t length); - -struct kadm5_pw_policy_check_func { - const char *name; - kadm5_passwd_quality_check_func func; -}; - -struct kadm5_pw_policy_verifier { - const char *name; - int version; - const char *vendor; - const struct kadm5_pw_policy_check_func *funcs; -}; -#endif /* !HAVE_KADM5_PWCHECK_H */ - - -/* - * Expects a principal and a password to check on the command line. Loads the - * Heimdal plugin, converts the input into the necessary format, calls the - * plugin, and reports the results. Exits with a status matching the return - * value of the plugin function. - * - * We assume that the plugin is available as: - * - * BUILD/../plugin/.libs/passwd_strength.so - * - * since we don't want to embed Libtool's libtldl just to run a test. - */ -int -main(int argc, char *argv[]) -{ - const char *build; - char *path; - size_t length; - krb5_context ctx; - krb5_principal princ; - krb5_data password; - krb5_error_code status; - void *handle; - struct kadm5_pw_policy_verifier *verifier; - int result; - char error[BUFSIZ] = ""; - - /* - * If we're not building with Heimdal, we can't run this test. Exit with - * a special status to communicate this to the test wrapper. - */ -#ifndef HAVE_KRB5_REALM - exit(42); -#endif - - /* Build the path of the plugin. */ - if (argc != 3) { - fprintf(stderr, "Wrong number of arguments\n"); - exit(1); - } - build = getenv("BUILD"); - if (build == NULL) { - fprintf(stderr, "No BUILD environment variable set\n"); - exit(1); - } - length = strlen(build) + strlen("/../plugin/.libs/passwd_strength.so"); - path = malloc(length + 1); - if (path == NULL) { - fprintf(stderr, "Cannot allocate memory: %s\n", strerror(errno)); - exit(1); - } - strlcpy(path, build, length + 1); - strlcat(path, "/../plugin/.libs/passwd_strength.so", length + 1); - - /* Initialize the data structures. */ - status = krb5_init_context(&ctx); - if (status != 0) { - fprintf(stderr, "Cannot initialize Kerberos context\n"); - exit(1); - } - status = krb5_parse_name(ctx, argv[1], &princ); - if (status != 0) { - fprintf(stderr, "Cannot parse principal name\n"); - exit(1); - } - password.length = strlen(argv[2]); - password.data = argv[2]; - - /* Load the module and find the correct symbol. */ - handle = dlopen(path, RTLD_NOW); - if (handle == NULL) { - fprintf(stderr, "Cannot dlopen %s: %s\n", path, dlerror()); - exit(1); - } - verifier = dlsym(handle, "kadm5_password_verifier"); - if (verifier == NULL) { - fprintf(stderr, "Cannot get kadm5_password_verifier symbol: %s\n", - dlerror()); - exit(1); - } - if (strcmp(verifier->name, "krb5-strength") != 0 - || strcmp(verifier->vendor, "Russ Allbery") != 0 - || verifier->version != KADM5_PASSWD_VERSION_V1 - || verifier->funcs == NULL - || strcmp(verifier->funcs[0].name, "krb5-strength") != 0 - || verifier->funcs[0].func == NULL - || verifier->funcs[1].name != NULL) { - fprintf(stderr, "Invalid metadata in plugin\n"); - exit(1); - } - result = (verifier->funcs[0].func)(ctx, princ, &password, NULL, error, - sizeof(error)); - if (error[0] != '\0') - fprintf(stderr, "%s\n", error); - exit(result); -} diff --git a/tests/mit/plugin-t b/tests/mit/plugin-t index ccb9d03..ed9ae99 100755 --- a/tests/mit/plugin-t +++ b/tests/mit/plugin-t @@ -9,7 +9,7 @@ # See LICENSE for licensing terms. . "$SOURCE/tap/libtap.sh" -cd "$BUILD/data" +cd "$BUILD" # Run the plugin program to check a password. Takes the test description, # the principal, the password, the expected exit status, and the expected diff --git a/tests/tap/kerberos.c b/tests/tap/kerberos.c new file mode 100644 index 0000000..474cf4f --- /dev/null +++ b/tests/tap/kerberos.c @@ -0,0 +1,488 @@ +/* + * 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 2006, 2007, 2009, 2010, 2011, 2012 + * 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. + */ + +#include +#ifdef HAVE_KERBEROS +# include +#endif +#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. + */ +#pragma GCC diagnostic ignored "-Wformat-nonliteral" + + +/* + * 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_KERBEROS + +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_KERBEROS */ + +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_KERBEROS */ + + +/* + * 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. + */ +void +kerberos_cleanup(void) +{ + char *path; + + if (tmpdir_ticket != NULL) { + basprintf(&path, "%s/krb5cc_test", tmpdir_ticket); + unlink(path); + free(path); + test_tmpdir_free(tmpdir_ticket); + tmpdir_ticket = NULL; + } + if (config != NULL) { + if (config->keytab != NULL) { + test_file_path_free(config->keytab); + free(config->principal); + free(config->cache); + } + if (config->userprinc != NULL) { + free(config->userprinc); + free(config->username); + free(config->password); + } + 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; + } +} + + +/* + * 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++; + } + if (path != NULL) + test_file_path_free(path); + + /* + * Register the cleanup function as an atexit handler so that the caller + * doesn't have to worry about cleanup. + */ + if (atexit(kerberos_cleanup) != 0) + sysdiag("cannot register cleanup function"); + + /* 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="); + if (krb5_config != NULL) { + 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_KERBEROS + + +/* + * Report a Kerberos error and bail out. + */ +void +bail_krb5(krb5_context ctx, krb5_error_code code, const char *format, ...) +{ + const char *k5_msg = NULL; + char *message; + va_list args; + + if (ctx != NULL) + k5_msg = krb5_get_error_message(ctx, 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. + */ +void +diag_krb5(krb5_context ctx, krb5_error_code code, const char *format, ...) +{ + const char *k5_msg = NULL; + char *message; + va_list args; + + if (ctx != NULL) + k5_msg = krb5_get_error_message(ctx, 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) { + 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); + } + if (status != 0) + bail("no principal found in keytab file %s", path); + krb5_kt_end_seq_get(ctx, keytab, &cursor); + krb5_kt_close(ctx, keytab); + return princ; +} + +#endif /* HAVE_KERBEROS */ diff --git a/tests/tap/kerberos.h b/tests/tap/kerberos.h new file mode 100644 index 0000000..8ce0da9 --- /dev/null +++ b/tests/tap/kerberos.h @@ -0,0 +1,125 @@ +/* + * Utility functions for tests that use Kerberos. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at . + * + * Written by Russ Allbery + * Copyright 2006, 2007, 2009, 2011, 2012, 2013 + * 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. + */ + +#ifndef TAP_KERBEROS_H +#define TAP_KERBEROS_H 1 + +#include +#include + +#ifdef HAVE_KERBEROS +# include +#endif + +/* Holds the information parsed from the Kerberos test configuration. */ +struct kerberos_config { + char *keytab; /* Path to the keytab. */ + char *principal; /* Principal whose keys are in the keytab. */ + char *cache; /* Path to the Kerberos ticket cache. */ + char *userprinc; /* The fully-qualified principal. */ + char *username; /* The local (non-realm) part of principal. */ + char *realm; /* The realm part of the principal. */ + char *password; /* The password. */ +}; + +/* + * Whether to skip all tests (by calling skip_all) in kerberos_setup if + * certain configuration information isn't available. + */ +enum kerberos_needs { + TAP_KRB_NEEDS_NONE = 0x00, + TAP_KRB_NEEDS_KEYTAB = 0x01, + TAP_KRB_NEEDS_PASSWORD = 0x02, + TAP_KRB_NEEDS_BOTH = 0x01 | 0x02 +}; + +BEGIN_DECLS + +/* + * Set up Kerberos, returning the test configuration information. This + * obtains Kerberos tickets from config/keytab, if one is present, and stores + * them in a Kerberos ticket cache, sets KRB5_KTNAME and KRB5CCNAME. It also + * loads the principal and password from config/password, if it exists, and + * stores the principal, password, username, and realm in the returned struct. + * + * If there is no config/keytab file, KRB5_KTNAME and KRB5CCNAME won't be set + * and the keytab field will be NULL. If there is no config/password file, + * the principal field will be NULL. If the files exist but loading them + * fails, or authentication fails, kerberos_setup calls bail. + * + * kerberos_cleanup will be set up to run from an atexit handler. This means + * that any child processes that should not remove the Kerberos ticket cache + * should call _exit instead of exit. The principal will be automatically + * freed when kerberos_cleanup is called or if kerberos_setup is called again. + * The caller doesn't need to worry about it. + */ +struct kerberos_config *kerberos_setup(enum kerberos_needs) + __attribute__((__malloc__)); +void kerberos_cleanup(void); + +/* + * 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. + * + * kerberos_cleanup_conf will clean up after this function, but usually + * doesn't need to be called directly since it's registered as an atexit + * handler. + */ +void kerberos_generate_conf(const char *realm); +void kerberos_cleanup_conf(void); + +/* Thes interfaces are only available with native Kerberos support. */ +#ifdef HAVE_KERBEROS + +/* Bail out with an error, appending the Kerberos error message. */ +void bail_krb5(krb5_context, krb5_error_code, const char *format, ...) + __attribute__((__noreturn__, __nonnull__, __format__(printf, 3, 4))); + +/* Report a diagnostic with Kerberos error to stderr prefixed with #. */ +void diag_krb5(krb5_context, krb5_error_code, const char *format, ...) + __attribute__((__nonnull__, __format__(printf, 3, 4))); + +/* + * Given a Kerberos context and the path to a keytab, retrieve the principal + * for the first entry in the keytab and return it. Calls bail on failure. + * The returned principal should be freed with krb5_free_principal. + */ +krb5_principal kerberos_keytab_principal(krb5_context, const char *path) + __attribute__((__nonnull__)); + +#endif /* HAVE_KERBEROS */ + +END_DECLS + +#endif /* !TAP_MESSAGES_H */ -- 2.39.2