From: Nalin Dahyabhai Date: Mon, 15 Jul 2013 17:30:00 +0000 (-0400) Subject: Add a helper for testing PKINIT and responder cb X-Git-Tag: krb5-1.12-alpha1~89 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e87b20116528dd68299c7cb4783ba68bfe12a5f0;p=thirdparty%2Fkrb5.git Add a helper for testing PKINIT and responder cb ticket: 7680 --- diff --git a/src/tests/Makefile.in b/src/tests/Makefile.in index 3c61b18abe..204941bdc9 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 50e1e7b5a3..b0c84bfeba 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 0000000000..698e3975ac --- /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 +#include +#include +#include +#include +#include +#include + +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; +}