krb5 commit: Add a helper for testing PKINIT and responder cb

Greg Hudson ghudson at MIT.EDU
Thu Jul 18 00:59:04 EDT 2013


https://github.com/krb5/krb5/commit/e87b20116528dd68299c7cb4783ba68bfe12a5f0
commit e87b20116528dd68299c7cb4783ba68bfe12a5f0
Author: Nalin Dahyabhai <nalin at redhat.com>
Date:   Mon Jul 15 13:30:00 2013 -0400

    Add a helper for testing PKINIT and responder cb
    
    ticket: 7680

 src/tests/Makefile.in |   13 +-
 src/tests/deps        |    5 +
 src/tests/responder.c |  390 +++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 404 insertions(+), 4 deletions(-)

diff --git a/src/tests/Makefile.in b/src/tests/Makefile.in
index 3c61b18..204941b 100644
--- a/src/tests/Makefile.in
+++ b/src/tests/Makefile.in
@@ -5,8 +5,10 @@ SUBDIRS = resolve asn.1 create hammer verify gssapi dejagnu shlib \
 
 RUN_SETUP = @KRB5_RUN_ENV@ KRB5_KDC_PROFILE=kdc.conf KRB5_CONFIG=krb5.conf
 
-OBJS= gcred.o hist.o kdbtest.o plugorder.o t_init_creds.o t_localauth.o
-EXTRADEPSRCS= gcred.c hist.c kdbtest.c plugorder.c t_init_creds.c t_localauth.c
+OBJS= gcred.o hist.o kdbtest.o plugorder.o t_init_creds.o \
+	t_localauth.o responder.o
+EXTRADEPSRCS= gcred.c hist.c kdbtest.c plugorder.c t_init_creds.c \
+	t_localauth.c responder.c
 
 TEST_DB = ./testdb
 TEST_REALM = FOO.TEST.REALM
@@ -31,6 +33,9 @@ kdbtest: kdbtest.o $(KDB5_DEPLIBS) $(KADMSRV_DEPLIBS) $(KRB5_BASE_DEPLIBS)
 plugorder: plugorder.o $(KRB5_BASE_DEPLIBS)
 	$(CC_LINK) -o $@ plugorder.o $(KRB5_BASE_LIBS)
 
+responder: responder.o $(KRB5_BASE_DEPLIBS)
+	$(CC_LINK) -o $@ responder.o $(KRB5_BASE_LIBS)
+
 t_init_creds: t_init_creds.o $(KRB5_BASE_DEPLIBS)
 	$(CC_LINK) -o $@ t_init_creds.o $(KRB5_BASE_LIBS)
 
@@ -79,7 +84,7 @@ kdb_check: kdc.conf krb5.conf
 	$(RUN_SETUP) $(VALGRIND) ../kadmin/dbutil/kdb5_util $(KADMIN_OPTS) destroy -f
 	$(RM) $(TEST_DB)* stash_file
 
-check-pytests:: gcred hist kdbtest plugorder t_init_creds t_localauth
+check-pytests:: gcred hist kdbtest plugorder responder t_init_creds t_localauth
 	$(RUNPYTEST) $(srcdir)/t_general.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_dump.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_iprop.py $(PYTESTFLAGS)
@@ -111,6 +116,6 @@ check-pytests:: gcred hist kdbtest plugorder t_init_creds t_localauth
 	$(RUNPYTEST) $(srcdir)/t_cve-2013-1417.py $(PYTESTFLAGS)
 
 clean::
-	$(RM) gcred hist kdbtest plugorder t_init_creds t_localauth
+	$(RM) gcred hist kdbtest plugorder responder t_init_creds t_localauth
 	$(RM) krb5.conf kdc.conf
 	$(RM) -rf kdc_realm/sandbox ldap
diff --git a/src/tests/deps b/src/tests/deps
index 50e1e7b..b0c84bf 100644
--- a/src/tests/deps
+++ b/src/tests/deps
@@ -59,6 +59,11 @@ $(OUTPRE)plugorder.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
   $(top_srcdir)/include/krb5/authdata_plugin.h $(top_srcdir)/include/krb5/plugin.h \
   $(top_srcdir)/include/krb5/pwqual_plugin.h $(top_srcdir)/include/port-sockets.h \
   $(top_srcdir)/include/socket-utils.h plugorder.c
+$(OUTPRE)responder.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
+  $(BUILDTOP)/include/krb5/krb5.h $(COM_ERR_DEPS) \
+  $(srcdir)/../include/k5-json.h \
+  $(srcdir)/../include/k5-platform.h $(srcdir)/../include/k5-thread.h \
+  $(srcdir)/../include/krb5.h responder.c
 $(OUTPRE)t_init_creds.$(OBJEXT): $(BUILDTOP)/include/krb5/krb5.h \
   $(COM_ERR_DEPS) $(top_srcdir)/include/krb5.h t_init_creds.c
 $(OUTPRE)t_localauth.$(OBJEXT): $(BUILDTOP)/include/krb5/krb5.h \
diff --git a/src/tests/responder.c b/src/tests/responder.c
new file mode 100644
index 0000000..698e397
--- /dev/null
+++ b/src/tests/responder.c
@@ -0,0 +1,390 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* tests/responder.c - Test harness for responder callbacks and the like. */
+/*
+ * Copyright 2013 Red Hat, Inc.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *
+ *    2. 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 OWNER
+ * 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.
+ */
+
+/*
+ *  A helper for testing PKINIT and responder callbacks.
+ *
+ *  This test helper takes multiple options and one argument.
+ *
+ *  responder [options] principal
+ *   -X preauth_option  -> preauth options, as for kinit
+ *   -x challenge       -> expected responder challenge, of the form
+ *                         "question=challenge"
+ *   -r response        -> provide a reponder answer, in the form
+ *                         "question=answer"
+ *   -c                 -> print the pkinit challenge
+ *   -p identity=pin    -> provide a pkinit answer, in the form "identity=pin"
+ *   principal          -> client principal name
+ *
+ *  If the responder callback isn't called, that's treated as an error.
+ *
+ *  If an expected responder challenge is specified, when the responder
+ *  callback is called, the challenge associated with the specified question is
+ *  compared against the specified value.  If the value provided to the
+ *  callback doesn't parse as JSON, a literal string compare is performed,
+ *  otherwise both values are parsed as JSON and then re-encoded before
+ *  comparison.  In either case, the comparison must succeed.
+ *
+ *  Any missing data or mismatches are treated as errors.
+ */
+
+#include <sys/types.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <string.h>
+#include <krb5.h>
+#include <k5-platform.h>
+#include <k5-json.h>
+
+struct responder_data {
+    krb5_boolean called;
+    krb5_boolean print_pkinit_challenge;
+    const char *challenge;
+    const char *response;
+    const char *pkinit_answer;
+};
+
+static krb5_error_code
+responder(krb5_context ctx, void *rawdata, krb5_responder_context rctx)
+{
+    krb5_error_code err;
+    char *key, *value, *encoded1, *encoded2;
+    const char *challenge;
+    k5_json_value decoded1, decoded2;
+    k5_json_object ids;
+    k5_json_number val;
+    krb5_int32 token_flags;
+    struct responder_data *data = rawdata;
+    krb5_responder_pkinit_challenge *chl;
+    unsigned int i, n;
+
+    data->called = TRUE;
+
+    /* Check that a particular challenge has the specified expected value. */
+    if (data->challenge != NULL) {
+        /* Separate the challenge name and its expected value. */
+        key = strdup(data->challenge);
+        if (key == NULL)
+            exit(ENOMEM);
+        value = key + strcspn(key, "=");
+        if (*value != '\0')
+            *value++ = '\0';
+        /* Read the challenge. */
+        challenge = krb5_responder_get_challenge(ctx, rctx, key);
+        if (challenge == NULL)
+            challenge = "";
+        /* See if the expected challenge looks like JSON-encoded data. */
+        err = k5_json_decode(value, &decoded1);
+        if (err != 0) {
+            /* It's not JSON, so assume we're just after a string compare. */
+            if (strcmp(challenge, value) == 0) {
+                fprintf(stderr, "OK: \"%s\" == \"%s\"\n", challenge, value);
+            } else {
+                fprintf(stderr, "ERROR: \"%s\" != \"%s\"\n", challenge, value);
+                exit(1);
+            }
+        } else {
+            /* Assume we're after a JSON compare - decode the actual value. */
+            err = k5_json_decode(challenge, &decoded2);
+            if (err != 0) {
+                fprintf(stderr, "error decoding \"%s\"\n", challenge);
+                exit(1);
+            }
+            /* Re-encode the expected challenge and the actual challenge... */
+            err = k5_json_encode(decoded1, &encoded1);
+            if (err != 0) {
+                fprintf(stderr, "error encoding json data\n");
+                exit(1);
+            }
+            err = k5_json_encode(decoded2, &encoded2);
+            if (err != 0) {
+                fprintf(stderr, "error encoding json data\n");
+                exit(1);
+            }
+            k5_json_release(decoded1);
+            k5_json_release(decoded2);
+            /* ... and see if they look the same. */
+            if (strcmp(encoded1, encoded2) == 0) {
+                fprintf(stderr, "OK: \"%s\" == \"%s\"\n", encoded1, encoded2);
+            } else {
+                fprintf(stderr, "ERROR: \"%s\" != \"%s\"\n", encoded1,
+                        encoded2);
+                exit(1);
+            }
+            free(encoded1);
+            free(encoded2);
+        }
+        free(key);
+    }
+
+    /* Provide a particular response for a challenge. */
+    if (data->response != NULL) {
+        /* Separate the challenge and its data content... */
+        key = strdup(data->response);
+        if (key == NULL)
+            exit(ENOMEM);
+        value = key + strcspn(key, "=");
+        if (*value != '\0')
+            *value++ = '\0';
+        /* ... and pass it in. */
+        err = krb5_responder_set_answer(ctx, rctx, key, value);
+        if (err != 0) {
+            fprintf(stderr, "error setting response\n");
+            exit(1);
+        }
+        free(key);
+    }
+
+    /* Read the challenge, formatted as a structure. */
+    err = krb5_responder_pkinit_get_challenge(ctx, rctx, &chl);
+    if (err != 0) {
+        fprintf(stderr, "error getting pkinit challenge\n");
+        exit(1);
+    }
+
+    if (data->print_pkinit_challenge) {
+        if (chl != NULL) {
+            for (n = 0; chl->identities[n] != NULL; n++)
+                continue;
+            for (i = 0; chl->identities[i] != NULL; i++) {
+                if (chl->identities[i]->token_flags != -1) {
+                    printf("identity %u/%u: %s (flags=0x%lx)\n", i + 1, n,
+                           chl->identities[i]->identity,
+                           (long)chl->identities[i]->token_flags);
+                } else {
+                    printf("identity %u/%u: %s\n", i + 1, n,
+                           chl->identities[i]->identity);
+                }
+            }
+        }
+    }
+
+    /* Provide a particular response for the PKINIT challenge. */
+    if (data->pkinit_answer != NULL) {
+        /*
+         * In case order matters, if the identity starts with "FILE:", exercise
+         * the set_answer function, with the real answer second.
+         */
+        if (chl != NULL &&
+            chl->identities != NULL &&
+            chl->identities[0] != NULL) {
+            if (strncmp(chl->identities[0]->identity, "FILE:", 5) == 0)
+                krb5_responder_pkinit_set_answer(ctx, rctx, "foo", "bar");
+        }
+        /* Provide the real answer. */
+        key = strdup(data->pkinit_answer);
+        if (key == NULL)
+            exit(ENOMEM);
+        value = strrchr(key, '=');
+        if (value != NULL)
+            *value++ = '\0';
+        else
+            value = "";
+        err = krb5_responder_pkinit_set_answer(ctx, rctx, key, value);
+        if (err != 0) {
+            fprintf(stderr, "error setting response\n");
+            exit(1);
+        }
+        free(key);
+        /*
+         * In case order matters, if the identity starts with "PKCS12:",
+         * exercise the set_answer function, with the real answer first.
+         */
+        if (chl != NULL &&
+            chl->identities != NULL &&
+            chl->identities[0] != NULL) {
+            if (strncmp(chl->identities[0]->identity, "PKCS12:", 5) == 0)
+                krb5_responder_pkinit_set_answer(ctx, rctx, "foo", "bar");
+        }
+    }
+
+    krb5_responder_pkinit_challenge_free(ctx, rctx, chl);
+
+    /*
+     * Something we always check: read the PKINIT challenge, both as a
+     * structure and in JSON form, reconstruct the JSON form from the
+     * structure's contents, and check that they're the same.
+     */
+    challenge = krb5_responder_get_challenge(ctx, rctx,
+                                             KRB5_RESPONDER_QUESTION_PKINIT);
+    if (challenge != NULL) {
+        krb5_responder_pkinit_get_challenge(ctx, rctx, &chl);
+        if (chl == NULL) {
+            fprintf(stderr, "pkinit raw challenge set, "
+                    "but structure is NULL\n");
+            exit(1);
+        }
+        if (k5_json_object_create(&ids) != 0) {
+            fprintf(stderr, "error creating json objects\n");
+            exit(1);
+        }
+        for (i = 0; chl->identities[i] != NULL; i++) {
+            token_flags = chl->identities[i]->token_flags;
+            if (k5_json_number_create(token_flags, &val) != 0) {
+                fprintf(stderr, "error creating json number\n");
+                exit(1);
+            }
+            if (k5_json_object_set(ids, chl->identities[i]->identity,
+                                   val) != 0) {
+                fprintf(stderr, "error adding json number to object\n");
+                exit(1);
+            }
+            k5_json_release(val);
+        }
+        /* Encode the structure... */
+        err = k5_json_encode(ids, &encoded1);
+        if (err != 0) {
+            fprintf(stderr, "error encoding json data\n");
+            exit(1);
+        }
+        k5_json_release(ids);
+        /* ... and see if they look the same. */
+        if (strcmp(encoded1, challenge) != 0) {
+            fprintf(stderr, "\"%s\" != \"%s\"\n", encoded1, challenge);
+            exit(1);
+        }
+        krb5_responder_pkinit_challenge_free(ctx, rctx, chl);
+        free(encoded1);
+    }
+
+    return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+    krb5_context context;
+    krb5_ccache ccache;
+    krb5_get_init_creds_opt *opts;
+    krb5_principal principal;
+    krb5_creds creds;
+    krb5_error_code err;
+    char *opt, *val;
+    struct responder_data response;
+    int c;
+
+    err = krb5_init_context(&context);
+    if (err != 0) {
+        fprintf(stderr, "error starting Kerberos: %s\n", error_message(err));
+        return err;
+    }
+    err = krb5_get_init_creds_opt_alloc(context, &opts);
+    if (err != 0) {
+        fprintf(stderr, "error initializing options: %s\n",
+                error_message(err));
+        return err;
+    }
+    err = krb5_cc_default(context, &ccache);
+    if (err != 0) {
+        fprintf(stderr, "error resolving default ccache: %s\n",
+                error_message(err));
+        return err;
+    }
+    err = krb5_get_init_creds_opt_set_out_ccache(context, opts, ccache);
+    if (err != 0) {
+        fprintf(stderr, "error setting output ccache: %s\n",
+                error_message(err));
+        return err;
+    }
+
+    memset(&response, 0, sizeof(response));
+    while ((c = getopt(argc, argv, "X:x:cr:p:")) != -1) {
+        switch (c) {
+        case 'X':
+            /* Like kinit, set a generic preauth option. */
+            opt = strdup(optarg);
+            val = opt + strcspn(opt, "=");
+            if (*val != '\0') {
+                *val++ = '\0';
+            }
+            err = krb5_get_init_creds_opt_set_pa(context, opts, opt, val);
+            if (err != 0) {
+                fprintf(stderr, "error setting option \"%s\": %s\n", opt,
+                        error_message(err));
+                return err;
+            }
+            free(opt);
+            break;
+        case 'x':
+            /* Check that a particular question has a specific challenge. */
+            response.challenge = optarg;
+            break;
+        case 'c':
+            /* Note that we want a dump of the PKINIT challenge structure. */
+            response.print_pkinit_challenge = TRUE;
+            break;
+        case 'r':
+            /* Set a verbatim response for a verbatim challenge. */
+            response.response = optarg;
+            break;
+        case 'p':
+            /* Set a PKINIT answer for a specific PKINIT identity. */
+            response.pkinit_answer = optarg;
+            break;
+        }
+    }
+
+    if (argc > optind) {
+        err = krb5_parse_name(context, argv[optind], &principal);
+        if (err != 0) {
+            fprintf(stderr, "error parsing name \"%s\": %s", argv[optind],
+                    error_message(err));
+            return err;
+        }
+    } else {
+        fprintf(stderr, "error: no principal name provided\n");
+        return -1;
+    }
+
+    err = krb5_get_init_creds_opt_set_responder(context, opts,
+                                                responder, &response);
+    if (err != 0) {
+        fprintf(stderr, "error setting responder: %s\n", error_message(err));
+        return err;
+    }
+    memset(&creds, 0, sizeof(creds));
+    err = krb5_get_init_creds_password(context, &creds, principal, NULL,
+                                       NULL, NULL, 0, NULL, opts);
+    if (err == 0)
+        krb5_free_cred_contents(context, &creds);
+    krb5_free_principal(context, principal);
+    krb5_get_init_creds_opt_free(context, opts);
+    krb5_cc_close(context, ccache);
+
+    if (!response.called) {
+        fprintf(stderr, "error: responder callback wasn't called\n");
+        err = 1;
+    } else if (err) {
+        fprintf(stderr, "error: krb5_get_init_creds_password failed: %s\n",
+                krb5_get_error_message(context, err));
+        err = 2;
+    }
+    krb5_free_context(context);
+    return err;
+}


More information about the cvs-krb5 mailing list