1 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 /* tests/responder.c - Test harness for responder callbacks and the like. */
3 /*
4 * Copyright 2013 Red Hat, Inc. All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in
14 * the documentation and/or other materials provided with the
15 * distribution.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
18 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
19 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
20 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
21 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30 /*
31 * A helper for testing PKINIT and responder callbacks.
32 *
33 * This test helper takes multiple options and one argument.
34 *
35 * responder [options] principal
36 * -X preauth_option -> preauth options, as for kinit
37 * -x challenge -> expected responder challenge, of the form
38 * "question=challenge"
39 * -r response -> provide a reponder answer, in the form
40 * "question=answer"
41 * -c -> print the pkinit challenge
42 * -p identity=pin -> provide a pkinit answer, in the form "identity=pin"
43 * -o index=value:pin -> provide an OTP answer, in the form "index=value:pin"
44 * principal -> client principal name
45 *
46 * If the responder callback isn't called, that's treated as an error.
47 *
48 * If an expected responder challenge is specified, when the responder
49 * callback is called, the challenge associated with the specified question is
50 * compared against the specified value. If the value provided to the
51 * callback doesn't parse as JSON, a literal string compare is performed,
52 * otherwise both values are parsed as JSON and then re-encoded before
53 * comparison. In either case, the comparison must succeed.
54 *
55 * Any missing data or mismatches are treated as errors.
56 */
57
58 #include <k5-platform.h>
59 #include <k5-json.h>
60 #include <sys/types.h>
61 #include <unistd.h>
62 #include <krb5.h>
63
64 struct responder_data {
65 krb5_boolean called;
66 krb5_boolean print_pkinit_challenge;
67 const char *challenge;
68 const char *response;
69 const char *pkinit_answer;
70 const char *otp_answer;
71 };
72
73 static krb5_error_code
responder(krb5_context ctx,void * rawdata,krb5_responder_context rctx)74 responder(krb5_context ctx, void *rawdata, krb5_responder_context rctx)
75 {
76 krb5_error_code err;
77 char *key, *value, *pin, *encoded1, *encoded2;
78 const char *challenge;
79 k5_json_value decoded1, decoded2;
80 k5_json_object ids;
81 k5_json_number val;
82 krb5_int32 token_flags;
83 struct responder_data *data = rawdata;
84 krb5_responder_pkinit_challenge *chl;
85 krb5_responder_otp_challenge *ochl;
86 unsigned int i, n;
87
88 data->called = TRUE;
89
90 /* Check that a particular challenge has the specified expected value. */
91 if (data->challenge != NULL) {
92 /* Separate the challenge name and its expected value. */
93 key = strdup(data->challenge);
94 if (key == NULL)
95 exit(ENOMEM);
96 value = key + strcspn(key, "=");
97 if (*value != '\0')
98 *value++ = '\0';
99 /* Read the challenge. */
100 challenge = krb5_responder_get_challenge(ctx, rctx, key);
101 err = k5_json_decode(value, &decoded1);
102 /* Check for "no challenge". */
103 if (challenge == NULL && *value == '\0') {
104 fprintf(stderr, "OK: (no challenge) == (no challenge)\n");
105 } else if (err != 0) {
106 /* It's not JSON, so assume we're just after a string compare. */
107 if (strcmp(challenge, value) == 0) {
108 fprintf(stderr, "OK: \"%s\" == \"%s\"\n", challenge, value);
109 } else {
110 fprintf(stderr, "ERROR: \"%s\" != \"%s\"\n", challenge, value);
111 exit(1);
112 }
113 } else {
114 /* Assume we're after a JSON compare - decode the actual value. */
115 err = k5_json_decode(challenge, &decoded2);
116 if (err != 0) {
117 fprintf(stderr, "error decoding \"%s\"\n", challenge);
118 exit(1);
119 }
120 /* Re-encode the expected challenge and the actual challenge... */
121 err = k5_json_encode(decoded1, &encoded1);
122 if (err != 0) {
123 fprintf(stderr, "error encoding json data\n");
124 exit(1);
125 }
126 err = k5_json_encode(decoded2, &encoded2);
127 if (err != 0) {
128 fprintf(stderr, "error encoding json data\n");
129 exit(1);
130 }
131 k5_json_release(decoded1);
132 k5_json_release(decoded2);
133 /* ... and see if they look the same. */
134 if (strcmp(encoded1, encoded2) == 0) {
135 fprintf(stderr, "OK: \"%s\" == \"%s\"\n", encoded1, encoded2);
136 } else {
137 fprintf(stderr, "ERROR: \"%s\" != \"%s\"\n", encoded1,
138 encoded2);
139 exit(1);
140 }
141 free(encoded1);
142 free(encoded2);
143 }
144 free(key);
145 }
146
147 /* Provide a particular response for a challenge. */
148 if (data->response != NULL) {
149 /* Separate the challenge and its data content... */
150 key = strdup(data->response);
151 if (key == NULL)
152 exit(ENOMEM);
153 value = key + strcspn(key, "=");
154 if (*value != '\0')
155 *value++ = '\0';
156 /* ... and pass it in. */
157 err = krb5_responder_set_answer(ctx, rctx, key, value);
158 if (err != 0) {
159 fprintf(stderr, "error setting response\n");
160 exit(1);
161 }
162 free(key);
163 }
164
165 if (data->print_pkinit_challenge) {
166 /* Read the PKINIT challenge, formatted as a structure. */
167 err = krb5_responder_pkinit_get_challenge(ctx, rctx, &chl);
168 if (err != 0) {
169 fprintf(stderr, "error getting pkinit challenge\n");
170 exit(1);
171 }
172 if (chl != NULL) {
173 for (n = 0; chl->identities[n] != NULL; n++)
174 continue;
175 for (i = 0; chl->identities[i] != NULL; i++) {
176 if (chl->identities[i]->token_flags != -1) {
177 printf("identity %u/%u: %s (flags=0x%lx)\n", i + 1, n,
178 chl->identities[i]->identity,
179 (long)chl->identities[i]->token_flags);
180 } else {
181 printf("identity %u/%u: %s\n", i + 1, n,
182 chl->identities[i]->identity);
183 }
184 }
185 }
186 krb5_responder_pkinit_challenge_free(ctx, rctx, chl);
187 }
188
189 /* Provide a particular response for the PKINIT challenge. */
190 if (data->pkinit_answer != NULL) {
191 /* Read the PKINIT challenge, formatted as a structure. */
192 err = krb5_responder_pkinit_get_challenge(ctx, rctx, &chl);
193 if (err != 0) {
194 fprintf(stderr, "error getting pkinit challenge\n");
195 exit(1);
196 }
197 /*
198 * In case order matters, if the identity starts with "FILE:", exercise
199 * the set_answer function, with the real answer second.
200 */
201 if (chl != NULL &&
202 chl->identities != NULL &&
203 chl->identities[0] != NULL) {
204 if (strncmp(chl->identities[0]->identity, "FILE:", 5) == 0)
205 krb5_responder_pkinit_set_answer(ctx, rctx, "foo", "bar");
206 }
207 /* Provide the real answer. */
208 key = strdup(data->pkinit_answer);
209 if (key == NULL)
210 exit(ENOMEM);
211 value = strrchr(key, '=');
212 if (value != NULL)
213 *value++ = '\0';
214 else
215 value = "";
216 err = krb5_responder_pkinit_set_answer(ctx, rctx, key, value);
217 if (err != 0) {
218 fprintf(stderr, "error setting response\n");
219 exit(1);
220 }
221 free(key);
222 /*
223 * In case order matters, if the identity starts with "PKCS12:",
224 * exercise the set_answer function, with the real answer first.
225 */
226 if (chl != NULL &&
227 chl->identities != NULL &&
228 chl->identities[0] != NULL) {
229 if (strncmp(chl->identities[0]->identity, "PKCS12:", 7) == 0)
230 krb5_responder_pkinit_set_answer(ctx, rctx, "foo", "bar");
231 }
232 krb5_responder_pkinit_challenge_free(ctx, rctx, chl);
233 }
234
235 /*
236 * Something we always check: read the PKINIT challenge, both as a
237 * structure and in JSON form, reconstruct the JSON form from the
238 * structure's contents, and check that they're the same.
239 */
240 challenge = krb5_responder_get_challenge(ctx, rctx,
241 KRB5_RESPONDER_QUESTION_PKINIT);
242 if (challenge != NULL) {
243 krb5_responder_pkinit_get_challenge(ctx, rctx, &chl);
244 if (chl == NULL) {
245 fprintf(stderr, "pkinit raw challenge set, "
246 "but structure is NULL\n");
247 exit(1);
248 }
249 if (k5_json_object_create(&ids) != 0) {
250 fprintf(stderr, "error creating json objects\n");
251 exit(1);
252 }
253 for (i = 0; chl->identities[i] != NULL; i++) {
254 token_flags = chl->identities[i]->token_flags;
255 if (k5_json_number_create(token_flags, &val) != 0) {
256 fprintf(stderr, "error creating json number\n");
257 exit(1);
258 }
259 if (k5_json_object_set(ids, chl->identities[i]->identity,
260 val) != 0) {
261 fprintf(stderr, "error adding json number to object\n");
262 exit(1);
263 }
264 k5_json_release(val);
265 }
266 /* Encode the structure... */
267 err = k5_json_encode(ids, &encoded1);
268 if (err != 0) {
269 fprintf(stderr, "error encoding json data\n");
270 exit(1);
271 }
272 k5_json_release(ids);
273 /* ... and see if they look the same. */
274 if (strcmp(encoded1, challenge) != 0) {
275 fprintf(stderr, "\"%s\" != \"%s\"\n", encoded1, challenge);
276 exit(1);
277 }
278 krb5_responder_pkinit_challenge_free(ctx, rctx, chl);
279 free(encoded1);
280 }
281
282 /* Provide a particular response for an OTP challenge. */
283 if (data->otp_answer != NULL) {
284 if (krb5_responder_otp_get_challenge(ctx, rctx, &ochl) == 0) {
285 key = strchr(data->otp_answer, '=');
286 if (key != NULL) {
287 /* Make a copy of the answer that we can chop up. */
288 key = strdup(data->otp_answer);
289 if (key == NULL)
290 return ENOMEM;
291 /* Isolate the ti value. */
292 value = strchr(key, '=');
293 *value++ = '\0';
294 n = atoi(key);
295 /* Break the value and PIN apart. */
296 pin = strchr(value, ':');
297 if (pin != NULL)
298 *pin++ = '\0';
299 err = krb5_responder_otp_set_answer(ctx, rctx, n, value, pin);
300 if (err != 0) {
301 fprintf(stderr, "error setting response\n");
302 exit(1);
303 }
304 free(key);
305 }
306 krb5_responder_otp_challenge_free(ctx, rctx, ochl);
307 }
308 }
309
310 return 0;
311 }
312
313 int
main(int argc,char ** argv)314 main(int argc, char **argv)
315 {
316 krb5_context context;
317 krb5_ccache ccache;
318 krb5_get_init_creds_opt *opts;
319 krb5_principal principal;
320 krb5_creds creds;
321 krb5_error_code err;
322 const char *errmsg;
323 char *opt, *val;
324 struct responder_data response;
325 int c;
326
327 err = krb5_init_context(&context);
328 if (err != 0) {
329 fprintf(stderr, "error starting Kerberos: %s\n", error_message(err));
330 return err;
331 }
332 err = krb5_get_init_creds_opt_alloc(context, &opts);
333 if (err != 0) {
334 fprintf(stderr, "error initializing options: %s\n",
335 error_message(err));
336 return err;
337 }
338 err = krb5_cc_default(context, &ccache);
339 if (err != 0) {
340 fprintf(stderr, "error resolving default ccache: %s\n",
341 error_message(err));
342 return err;
343 }
344 err = krb5_get_init_creds_opt_set_out_ccache(context, opts, ccache);
345 if (err != 0) {
346 fprintf(stderr, "error setting output ccache: %s\n",
347 error_message(err));
348 return err;
349 }
350
351 memset(&response, 0, sizeof(response));
352 while ((c = getopt(argc, argv, "X:x:cr:p:")) != -1) {
353 switch (c) {
354 case 'X':
355 /* Like kinit, set a generic preauth option. */
356 opt = strdup(optarg);
357 val = opt + strcspn(opt, "=");
358 if (*val != '\0') {
359 *val++ = '\0';
360 }
361 err = krb5_get_init_creds_opt_set_pa(context, opts, opt, val);
362 if (err != 0) {
363 fprintf(stderr, "error setting option \"%s\": %s\n", opt,
364 error_message(err));
365 return err;
366 }
367 free(opt);
368 break;
369 case 'x':
370 /* Check that a particular question has a specific challenge. */
371 response.challenge = optarg;
372 break;
373 case 'c':
374 /* Note that we want a dump of the PKINIT challenge structure. */
375 response.print_pkinit_challenge = TRUE;
376 break;
377 case 'r':
378 /* Set a verbatim response for a verbatim challenge. */
379 response.response = optarg;
380 break;
381 case 'p':
382 /* Set a PKINIT answer for a specific PKINIT identity. */
383 response.pkinit_answer = optarg;
384 break;
385 case 'o':
386 /* Set an OTP answer for a specific OTP tokeninfo. */
387 response.otp_answer = optarg;
388 break;
389 }
390 }
391
392 if (argc > optind) {
393 err = krb5_parse_name(context, argv[optind], &principal);
394 if (err != 0) {
395 fprintf(stderr, "error parsing name \"%s\": %s", argv[optind],
396 error_message(err));
397 return err;
398 }
399 } else {
400 fprintf(stderr, "error: no principal name provided\n");
401 return -1;
402 }
403
404 err = krb5_get_init_creds_opt_set_responder(context, opts,
405 responder, &response);
406 if (err != 0) {
407 fprintf(stderr, "error setting responder: %s\n", error_message(err));
408 return err;
409 }
410 memset(&creds, 0, sizeof(creds));
411 err = krb5_get_init_creds_password(context, &creds, principal, NULL,
412 NULL, NULL, 0, NULL, opts);
413 if (err == 0)
414 krb5_free_cred_contents(context, &creds);
415 krb5_free_principal(context, principal);
416 krb5_get_init_creds_opt_free(context, opts);
417 krb5_cc_close(context, ccache);
418
419 if (!response.called) {
420 fprintf(stderr, "error: responder callback wasn't called\n");
421 err = 1;
422 } else if (err) {
423 errmsg = krb5_get_error_message(context, err);
424 fprintf(stderr, "error: krb5_get_init_creds_password failed: %s\n",
425 errmsg);
426 krb5_free_error_message(context, errmsg);
427 err = 2;
428 }
429 krb5_free_context(context);
430 return err;
431 }
432