diff --git a/src/plugins/kdb/test/Makefile.in b/src/plugins/kdb/test/Makefile.in
new file mode 100644
index 0000000..f9578a3
--- /dev/null
+++ b/src/plugins/kdb/test/Makefile.in
@@ -0,0 +1,21 @@
+mydir=plugins$(S)kdb$(S)test
+BUILDTOP=$(REL)..$(S)..$(S)..
+
+LIBBASE=test
+LIBMAJOR=0
+LIBMINOR=0
+RELDIR=../plugins/kdb/test
+SHLIB_EXPDEPS=$(KADMSRV_DEPLIB) $(KRB5_BASE_DEPLIBS)
+SHLIB_EXPLIBS=$(KADMSRV_LIBS) $(KRB5_BASE_LIBS)
+LOCALINCLUDES=-I../../../lib/kdb -I$(srcdir)/../../../lib/kdb
+
+SRCS = $(srcdir)/kdb_test.c
+
+STLIBOBJS = kdb_test.o
+
+all-unix:: all-liblinks
+install-unix::
+clean-unix:: clean-liblinks clean-libs clean-libobjs
+
+@libnover_frag@
+@libobj_frag@
diff --git a/src/plugins/kdb/test/deps b/src/plugins/kdb/test/deps
new file mode 100644
index 0000000..e69de29
diff --git a/src/plugins/kdb/test/kdb_test.c b/src/plugins/kdb/test/kdb_test.c
new file mode 100644
index 0000000..a0e4970
--- /dev/null
+++ b/src/plugins/kdb/test/kdb_test.c
@@ -0,0 +1,561 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* plugins/kdb/test/kdb_test.c - Test KDB module */
+/*
+ * Copyright (C) 2015 by the Massachusetts Institute of Technology.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * This is a read-only KDB module intended to help test KDC behavior which
+ * cannot be exercised with the DB2 module. Responses are read from the
+ * dbmodules subsection according to this example:
+ *
+ * [dbmodules]
+ * test = {
+ * alias = {
+ * aliasname = canonname
+ * # For cross-realm aliases, only the realm part will
+ * # matter to the client.
+ * aliasname = @FOREIGN_REALM
+ * enterprise@PRINC = @FOREIGN_REALM
+ * }
+ * princs = {
+ * krbtgt/KRBTEST.COM = {
+ * flags = +preauth +ok-to-auth-as-delegate
+ * maxlife = 1d
+ * maxrenewlife = 7d
+ * expiration = 14d # relative to current time
+ * pwexpiration = 1h
+ * # Initial number is kvno; defaults to 1.
+ * keys = 3 aes256-cts aes128-cts:normal
+ * keys = 2 rc4-hmac
+ * }
+ * }
+ * delegation = {
+ * intermediate_service = target_service
+ * }
+ * }
+ *
+ * Key values are generated using a hash of the kvno, enctype, salt type, and
+ * principal name. This module does not use master key encryption, so it
+ * serves as a partial test of the DAL's ability to avoid that.
+ */
+
+#include "k5-int.h"
+#include "kdb5.h"
+#include "adm_proto.h"
+#include <ctype.h>
+
+typedef struct {
+ void *profile;
+ char *section;
+ const char *names[6];
+} *testhandle;
+
+static void *
+ealloc(size_t sz)
+{
+ void *p = calloc(sz, 1);
+
+ if (p == NULL)
+ abort();
+ return p;
+}
+
+static char *
+estrdup(const char *s)
+{
+ char *copy = strdup(s);
+
+ if (copy == NULL)
+ abort();
+ return copy;
+}
+
+static void
+check(krb5_error_code code)
+{
+ if (code != 0)
+ abort();
+}
+
+/* Set up for a profile query using h->names. Look up s1 -> s2 -> s3 (some of
+ * which may be NULL) within this database's dbmodules section. */
+static void
+set_names(testhandle h, const char *s1, const char *s2, const char *s3)
+{
+ h->names[0] = KDB_MODULE_SECTION;
+ h->names[1] = h->section;
+ h->names[2] = s1;
+ h->names[3] = s2;
+ h->names[4] = s3;
+ h->names[5] = NULL;
+}
+
+/* Look up a string within this database's dbmodules section. */
+static char *
+get_string(testhandle h, const char *s1, const char *s2, const char *s3)
+{
+ krb5_error_code ret;
+ char **values, *val;
+
+ set_names(h, s1, s2, s3);
+ ret = profile_get_values(h->profile, h->names, &values);
+ if (ret == PROF_NO_RELATION)
+ return NULL;
+ if (ret)
+ abort();
+ val = estrdup(values[0]);
+ profile_free_list(values);
+ return val;
+}
+
+/* Look up a duration within this database's dbmodules section. */
+static krb5_deltat
+get_duration(testhandle h, const char *s1, const char *s2, const char *s3)
+{
+ char *strval = get_string(h, s1, s2, s3);
+ krb5_deltat val;
+
+ if (strval == NULL)
+ return 0;
+ check(krb5_string_to_deltat(strval, &val));
+ free(strval);
+ return val;
+}
+
+/* Look up an absolute time within this database's dbmodules section. The time
+ * is expressed in the profile as an interval relative to the current time. */
+static krb5_timestamp
+get_time(testhandle h, const char *s1, const char *s2, const char *s3)
+{
+ char *strval = get_string(h, s1, s2, s3);
+ krb5_deltat val;
+
+ if (strval == NULL)
+ return 0;
+ check(krb5_string_to_deltat(strval, &val));
+ free(strval);
+ return val + time(NULL);
+}
+
+/* Initialize kb_out with a key of type etype, using a hash of kvno, etype,
+ * salttype, and princstr for the key bytes. */
+static void
+make_keyblock(krb5_kvno kvno, krb5_enctype etype, int32_t salttype,
+ const char *princstr, krb5_keyblock *kb_out)
+{
+ size_t keybytes, keylength, pos, n;
+ char *hashstr;
+ krb5_data d, rndin;
+ krb5_checksum cksum;
+
+ check(krb5_c_keylengths(NULL, etype, &keybytes, &keylength));
+ alloc_data(&rndin, keybytes);
+
+ /* Hash the kvno, enctype, salt type, and principal name together. */
+ if (asprintf(&hashstr, "%d %d %d %s", (int)kvno, (int)etype,
+ (int)salttype, princstr) < 0)
+ abort();
+ d = string2data(hashstr);
+ check(krb5_c_make_checksum(NULL, CKSUMTYPE_NIST_SHA, NULL, 0, &d, &cksum));
+
+ /* Make the appropriate number of input bytes from the hash result. */
+ for (pos = 0; pos < keybytes; pos += n) {
+ n = (cksum.length < keybytes - pos) ? cksum.length : keybytes - pos;
+ memcpy(rndin.data + pos, cksum.contents, n);
+ }
+
+ kb_out->enctype = etype;
+ kb_out->length = keylength;
+ kb_out->contents = ealloc(keylength);
+ check(krb5_c_random_to_key(NULL, etype, &rndin, kb_out));
+ free(cksum.contents);
+ free(rndin.data);
+ free(hashstr);
+}
+
+/* Return key data for the given key/salt tuple strings, using hashes of the
+ * enctypes, salts, and princstr for the key contents. */
+static void
+make_keys(char **strings, const char *princstr, krb5_db_entry *ent)
+{
+ krb5_key_data *key_data, *kd;
+ krb5_keyblock kb;
+ int32_t *ks_list_sizes, nstrings, nkeys, i, j;
+ krb5_key_salt_tuple **ks_lists, *ks;
+ krb5_kvno *kvnos;
+ char *s;
+
+ for (nstrings = 0; strings[nstrings] != NULL; nstrings++);
+ ks_lists = ealloc(nstrings * sizeof(*ks_lists));
+ ks_list_sizes = ealloc(nstrings * sizeof(*ks_list_sizes));
+ kvnos = ealloc(nstrings * sizeof(*kvnos));
+
+ /* Convert each string into a key/salt tuple list and count the total
+ * number of key data structures needed. */
+ nkeys = 0;
+ for (i = 0; i < nstrings; i++) {
+ s = strings[i];
+ /* Read a leading kvno if present; otherwise assume kvno 1. */
+ if (isdigit(*s)) {
+ kvnos[i] = strtol(s, &s, 10);
+ while (isspace(*s))
+ s++;
+ } else {
+ kvnos[i] = 1;
+ }
+ check(krb5_string_to_keysalts(s, NULL, NULL, FALSE, &ks_lists[i],
+ &ks_list_sizes[i]));
+ nkeys += ks_list_sizes[i];
+ }
+
+ /* Turn each key/salt tuple into a key data entry. */
+ kd = key_data = ealloc(nkeys * sizeof(*kd));
+ for (i = 0; i < nstrings; i++) {
+ ks = ks_lists[i];
+ for (j = 0; j < ks_list_sizes[i]; j++) {
+ make_keyblock(kvnos[i], ks[j].ks_enctype, ks[j].ks_salttype,
+ princstr, &kb);
+ kd->key_data_ver = 2;
+ kd->key_data_kvno = kvnos[i];
+ kd->key_data_type[0] = ks[j].ks_enctype;
+ kd->key_data_length[0] = kb.length;
+ kd->key_data_contents[0] = kb.contents;
+ kd->key_data_type[1] = ks[j].ks_salttype;
+ kd++;
+ }
+ }
+
+ for (i = 0; i < nstrings; i++)
+ free(ks_lists[i]);
+ free(ks_lists);
+ free(ks_list_sizes);
+ free(kvnos);
+ ent->key_data = key_data;
+ ent->n_key_data = nkeys;
+}
+
+static krb5_error_code
+test_init()
+{
+ return 0;
+}
+
+static krb5_error_code
+test_cleanup()
+{
+ return 0;
+}
+
+static krb5_error_code
+test_open(krb5_context context, char *conf_section, char **db_args, int mode)
+{
+ testhandle h;
+
+ h = ealloc(sizeof(*h));
+ h->profile = context->profile;
+ h->section = estrdup(conf_section);
+ context->dal_handle->db_context = h;
+ return 0;
+}
+
+static krb5_error_code
+test_close(krb5_context context)
+{
+ testhandle h = context->dal_handle->db_context;
+
+ free(h->section);
+ free(h);
+ return 0;
+}
+
+static krb5_error_code
+test_get_principal(krb5_context context, krb5_const_principal search_for,
+ unsigned int flags, krb5_db_entry **entry)
+{
+ krb5_error_code ret;
+ krb5_principal princ = NULL;
+ krb5_principal_data empty_princ = { KV5M_PRINCIPAL };
+ testhandle h = context->dal_handle->db_context;
+ char *search_name = NULL, *canon = NULL, *flagstr, **names, **key_strings;
+ const char *ename;
+ krb5_db_entry *ent;
+
+ *entry = NULL;
+
+ check(krb5_unparse_name_flags(context, search_for,
+ KRB5_PRINCIPAL_UNPARSE_NO_REALM,
+ &search_name));
+ canon = get_string(h, "alias", search_name, NULL);
+ if (canon != NULL) {
+ if (!(flags & KRB5_KDB_FLAG_ALIAS_OK)) {
+ ret = KRB5_KDB_NOENTRY;
+ goto cleanup;
+ }
+ check(krb5_parse_name(context, canon, &princ));
+ if (!krb5_realm_compare(context, search_for, princ)) {
+ if (flags & KRB5_KDB_FLAG_CLIENT_REFERRALS_ONLY) {
+ /* Return a client referral by creating an entry with only the
+ * principal set. */
+ *entry = ealloc(sizeof(**entry));
+ (*entry)->princ = princ;
+ princ = NULL;
+ ret = 0;
+ goto cleanup;
+ } else {
+ /* We could look up a cross-realm TGS entry, but we don't need
+ * that behavior yet. */
+ ret = KRB5_KDB_NOENTRY;
+ goto cleanup;
+ }
+ }
+ ename = canon;
+ } else {
+ check(krb5_copy_principal(context, search_for, &princ));
+ ename = search_name;
+ }
+
+ /* Check that the entry exists. */
+ set_names(h, "princs", ename, NULL);
+ ret = profile_get_relation_names(h->profile, h->names, &names);
+ if (ret == PROF_NO_RELATION) {
+ ret = KRB5_KDB_NOENTRY;
+ goto cleanup;
+ }
+ profile_free_list(names);
+
+ /* No error exits after this point. */
+
+ ent = ealloc(sizeof(*ent));
+ ent->princ = princ;
+ princ = NULL;
+
+ flagstr = get_string(h, "princs", ename, "flags");
+ if (flagstr != NULL) {
+ check(krb5_flagspec_to_mask(flagstr, &ent->attributes,
+ &ent->attributes));
+ }
+ free(flagstr);
+
+ ent->max_life = get_duration(h, "princs", ename, "maxlife");
+ ent->max_renewable_life = get_duration(h, "princs", ename, "maxrenewlife");
+ ent->expiration = get_time(h, "princs", ename, "expiration");
+ ent->pw_expiration = get_time(h, "princs", ename, "pwexpiration");
+
+ /* Leave last_success, last_failed, fail_auth_count zeroed. */
+ /* Leave tl_data and e_data empty. */
+
+ set_names(h, "princs", ename, "keys");
+ ret = profile_get_values(h->profile, h->names, &key_strings);
+ if (ret != PROF_NO_RELATION) {
+ make_keys(key_strings, ename, ent);
+ profile_free_list(key_strings);
+ }
+
+ /* We must include mod-princ data or kadm5_get_principal() won't work and
+ * we can't extract keys with kadmin.local. */
+ check(krb5_dbe_update_mod_princ_data(context, ent, 0, &empty_princ));
+
+ *entry = ent;
+
+cleanup:
+ krb5_free_unparsed_name(context, search_name);
+ krb5_free_principal(context, princ);
+ free(canon);
+ return ret;
+}
+
+static void
+test_free_principal(krb5_context context, krb5_db_entry *entry)
+{
+ krb5_tl_data *tl, *next;
+ int i, j;
+
+ if (entry == NULL)
+ return;
+ free(entry->e_data);
+ krb5_free_principal(context, entry->princ);
+ for (tl = entry->tl_data; tl != NULL; tl = next) {
+ next = tl->tl_data_next;
+ free(tl->tl_data_contents);
+ free(tl);
+ }
+ for (i = 0; i < entry->n_key_data; i++) {
+ for (j = 0; j < entry->key_data[i].key_data_ver; j++) {
+ if (entry->key_data[i].key_data_length[j]) {
+ zapfree(entry->key_data[i].key_data_contents[j],
+ entry->key_data[i].key_data_length[j]);
+ }
+ entry->key_data[i].key_data_contents[j] = NULL;
+ entry->key_data[i].key_data_length[j] = 0;
+ entry->key_data[i].key_data_type[j] = 0;
+ }
+ }
+ free(entry->key_data);
+ free(entry);
+}
+
+static void *
+test_alloc(krb5_context context, void *ptr, size_t size)
+{
+ return realloc(ptr, size);
+}
+
+static void
+test_free(krb5_context context, void *ptr)
+{
+ free(ptr);
+}
+
+static krb5_error_code
+test_fetch_master_key(krb5_context context, krb5_principal mname,
+ krb5_keyblock *key_out, krb5_kvno *kvno_out,
+ char *db_args)
+{
+ memset(key_out, 0, sizeof(*key_out));
+ *kvno_out = 0;
+ return 0;
+}
+
+static krb5_error_code
+test_fetch_master_key_list(krb5_context context, krb5_principal mname,
+ const krb5_keyblock *key,
+ krb5_keylist_node **mkeys_out)
+{
+ /* krb5_dbe_get_mkvno() returns an error if we produce NULL, so return an
+ * empty node to make kadm5_get_principal() work. */
+ *mkeys_out = ealloc(sizeof(**mkeys_out));
+ return 0;
+}
+
+static krb5_error_code
+test_decrypt_key_data(krb5_context context, const krb5_keyblock *mkey,
+ const krb5_key_data *kd, krb5_keyblock *key_out,
+ krb5_keysalt *salt_out)
+{
+ key_out->magic = KV5M_KEYBLOCK;
+ key_out->enctype = kd->key_data_type[0];
+ key_out->length = kd->key_data_length[0];
+ key_out->contents = ealloc(key_out->length);
+ memcpy(key_out->contents, kd->key_data_contents[0], key_out->length);
+ if (salt_out != NULL) {
+ salt_out->type = (kd->key_data_ver > 1) ? kd->key_data_type[1] :
+ KRB5_KDB_SALTTYPE_NORMAL;
+ salt_out->data = empty_data();
+ }
+ return 0;
+}
+
+static krb5_error_code
+test_encrypt_key_data(krb5_context context, const krb5_keyblock *mkey,
+ const krb5_keyblock *key, const krb5_keysalt *salt,
+ int kvno, krb5_key_data *kd_out)
+{
+ memset(kd_out, 0, sizeof(*kd_out));
+ kd_out->key_data_ver = 2;
+ kd_out->key_data_kvno = kvno;
+ kd_out->key_data_type[0] = key->enctype;
+ kd_out->key_data_length[0] = key->length;
+ kd_out->key_data_contents[0] = ealloc(key->length);
+ memcpy(kd_out->key_data_contents[0], key->contents, key->length);
+ kd_out->key_data_type[1] = (salt != NULL) ? salt->type :
+ KRB5_KDB_SALTTYPE_NORMAL;
+ return 0;
+}
+
+static krb5_error_code
+test_check_allowed_to_delegate(krb5_context context,
+ krb5_const_principal client,
+ const krb5_db_entry *server,
+ krb5_const_principal proxy)
+{
+ krb5_error_code ret;
+ testhandle h = context->dal_handle->db_context;
+ char *sprinc, *tprinc, **values, **v;
+ krb5_boolean found = FALSE;
+
+ check(krb5_unparse_name_flags(context, server->princ,
+ KRB5_PRINCIPAL_UNPARSE_NO_REALM, &sprinc));
+ check(krb5_unparse_name_flags(context, proxy,
+ KRB5_PRINCIPAL_UNPARSE_NO_REALM, &tprinc));
+ set_names(h, "delegation", sprinc, NULL);
+ ret = profile_get_values(h->profile, h->names, &values);
+ if (ret == PROF_NO_RELATION)
+ return KRB5KDC_ERR_POLICY;
+ for (v = values; *v != NULL; v++) {
+ if (strcmp(*v, tprinc) == 0) {
+ found = TRUE;
+ break;
+ }
+ }
+ profile_free_list(values);
+ return found ? 0 : KRB5KDC_ERR_POLICY;
+}
+
+kdb_vftabl PLUGIN_SYMBOL_NAME(krb5_test, kdb_function_table) = {
+ KRB5_KDB_DAL_MAJOR_VERSION, /* major version number */
+ 0, /* minor version number 0 */
+ test_init,
+ test_cleanup,
+ test_open,
+ test_close,
+ NULL, /* create */
+ NULL, /* destroy */
+ NULL, /* get_age */
+ NULL, /* lock */
+ NULL, /* unlock */
+ test_get_principal,
+ test_free_principal,
+ NULL, /* put_principal */
+ NULL, /* delete_principal */
+ NULL, /* iterate */
+ NULL, /* create_policy */
+ NULL, /* get_policy */
+ NULL, /* put_policy */
+ NULL, /* iter_policy */
+ NULL, /* delete_policy */
+ NULL, /* free_policy */
+ test_alloc,
+ test_free,
+ test_fetch_master_key,
+ test_fetch_master_key_list,
+ NULL, /* store_master_key_list */
+ NULL, /* dbe_search_enctype */
+ NULL, /* change_pwd */
+ NULL, /* promote_db */
+ test_decrypt_key_data,
+ test_encrypt_key_data,
+ NULL, /* sign_authdata */
+ NULL, /* check_transited_realms */
+ NULL, /* check_policy_as */
+ NULL, /* check_policy_tgs */
+ NULL, /* audit_as_req */
+ NULL, /* refresh_config */
+ test_check_allowed_to_delegate
+};
diff --git a/src/plugins/kdb/test/test.exports b/src/plugins/kdb/test/test.exports
new file mode 100644
index 0000000..f2b7c11
--- /dev/null
+++ b/src/plugins/kdb/test/test.exports
@@ -0,0 +1 @@
+kdb_function_table