1 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 /* lib/krb5/krb/preauth_otp.c - OTP clpreauth module */
3 /*
4 * Copyright 2011 NORDUnet A/S. All rights reserved.
5 * Copyright 2011 Red Hat, Inc. All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions are met:
9 *
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 *
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in
15 * the documentation and/or other materials provided with the
16 * distribution.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
21 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
22 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31 #include "k5-int.h"
32 #include "k5-json.h"
33 #include "int-proto.h"
34 #include "os-proto.h"
35
36 #include <krb5/clpreauth_plugin.h>
37 #include <ctype.h>
38
39 static krb5_preauthtype otp_client_supported_pa_types[] =
40 { KRB5_PADATA_OTP_CHALLENGE, 0 };
41
42 /* Frees a tokeninfo. */
43 static void
free_tokeninfo(krb5_responder_otp_tokeninfo * ti)44 free_tokeninfo(krb5_responder_otp_tokeninfo *ti)
45 {
46 if (ti == NULL)
47 return;
48
49 free(ti->alg_id);
50 free(ti->challenge);
51 free(ti->token_id);
52 free(ti->vendor);
53 free(ti);
54 }
55
56 /* Converts a property of a json object into a char*. */
57 static krb5_error_code
codec_value_to_string(k5_json_object obj,const char * key,char ** string)58 codec_value_to_string(k5_json_object obj, const char *key, char **string)
59 {
60 k5_json_value val;
61 char *str;
62
63 val = k5_json_object_get(obj, key);
64 if (val == NULL)
65 return ENOENT;
66
67 if (k5_json_get_tid(val) != K5_JSON_TID_STRING)
68 return EINVAL;
69
70 str = strdup(k5_json_string_utf8(val));
71 if (str == NULL)
72 return ENOMEM;
73
74 *string = str;
75 return 0;
76 }
77
78 /* Converts a property of a json object into a krb5_data struct. */
79 static krb5_error_code
codec_value_to_data(k5_json_object obj,const char * key,krb5_data * data)80 codec_value_to_data(k5_json_object obj, const char *key, krb5_data *data)
81 {
82 krb5_error_code retval;
83 char *tmp;
84
85 retval = codec_value_to_string(obj, key, &tmp);
86 if (retval != 0)
87 return retval;
88
89 *data = string2data(tmp);
90 return 0;
91 }
92
93 /* Converts a krb5_data struct into a property of a JSON object. */
94 static krb5_error_code
codec_data_to_value(krb5_data * data,k5_json_object obj,const char * key)95 codec_data_to_value(krb5_data *data, k5_json_object obj, const char *key)
96 {
97 krb5_error_code retval;
98 k5_json_string str;
99
100 if (data->data == NULL)
101 return 0;
102
103 retval = k5_json_string_create_len(data->data, data->length, &str);
104 if (retval)
105 return retval;
106
107 retval = k5_json_object_set(obj, key, str);
108 k5_json_release(str);
109 return retval;
110 }
111
112 /* Converts a property of a json object into a krb5_int32. */
113 static krb5_error_code
codec_value_to_int32(k5_json_object obj,const char * key,krb5_int32 * int32)114 codec_value_to_int32(k5_json_object obj, const char *key, krb5_int32 *int32)
115 {
116 k5_json_value val;
117
118 val = k5_json_object_get(obj, key);
119 if (val == NULL)
120 return ENOENT;
121
122 if (k5_json_get_tid(val) != K5_JSON_TID_NUMBER)
123 return EINVAL;
124
125 *int32 = k5_json_number_value(val);
126 return 0;
127 }
128
129 /* Converts a krb5_int32 into a property of a JSON object. */
130 static krb5_error_code
codec_int32_to_value(krb5_int32 int32,k5_json_object obj,const char * key)131 codec_int32_to_value(krb5_int32 int32, k5_json_object obj, const char *key)
132 {
133 krb5_error_code retval;
134 k5_json_number num;
135
136 if (int32 == -1)
137 return 0;
138
139 retval = k5_json_number_create(int32, &num);
140 if (retval)
141 return retval;
142
143 retval = k5_json_object_set(obj, key, num);
144 k5_json_release(num);
145 return retval;
146 }
147
148 /* Converts a krb5_otp_tokeninfo into a JSON object. */
149 static krb5_error_code
codec_encode_tokeninfo(krb5_otp_tokeninfo * ti,k5_json_object * out)150 codec_encode_tokeninfo(krb5_otp_tokeninfo *ti, k5_json_object *out)
151 {
152 krb5_error_code retval;
153 k5_json_object obj;
154 krb5_flags flags;
155
156 retval = k5_json_object_create(&obj);
157 if (retval != 0)
158 goto error;
159
160 flags = KRB5_RESPONDER_OTP_FLAGS_COLLECT_TOKEN;
161 if (ti->flags & KRB5_OTP_FLAG_COLLECT_PIN) {
162 flags |= KRB5_RESPONDER_OTP_FLAGS_COLLECT_PIN;
163 if (ti->flags & KRB5_OTP_FLAG_SEPARATE_PIN)
164 flags |= KRB5_RESPONDER_OTP_FLAGS_NEXTOTP;
165 }
166 if (ti->flags & KRB5_OTP_FLAG_NEXTOTP)
167 flags |= KRB5_RESPONDER_OTP_FLAGS_NEXTOTP;
168
169 retval = codec_int32_to_value(flags, obj, "flags");
170 if (retval != 0)
171 goto error;
172
173 retval = codec_data_to_value(&ti->vendor, obj, "vendor");
174 if (retval != 0)
175 goto error;
176
177 retval = codec_data_to_value(&ti->challenge, obj, "challenge");
178 if (retval != 0)
179 goto error;
180
181 retval = codec_int32_to_value(ti->length, obj, "length");
182 if (retval != 0)
183 goto error;
184
185 if (ti->format != KRB5_OTP_FORMAT_BASE64 &&
186 ti->format != KRB5_OTP_FORMAT_BINARY) {
187 retval = codec_int32_to_value(ti->format, obj, "format");
188 if (retval != 0)
189 goto error;
190 }
191
192 retval = codec_data_to_value(&ti->token_id, obj, "tokenID");
193 if (retval != 0)
194 goto error;
195
196 retval = codec_data_to_value(&ti->alg_id, obj, "algID");
197 if (retval != 0)
198 goto error;
199
200 *out = obj;
201 return 0;
202
203 error:
204 k5_json_release(obj);
205 return retval;
206 }
207
208 /* Converts a krb5_pa_otp_challenge into a JSON object. */
209 static krb5_error_code
codec_encode_challenge(krb5_context ctx,krb5_pa_otp_challenge * chl,char ** json)210 codec_encode_challenge(krb5_context ctx, krb5_pa_otp_challenge *chl,
211 char **json)
212 {
213 k5_json_object obj = NULL, tmp = NULL;
214 k5_json_string str = NULL;
215 k5_json_array arr = NULL;
216 krb5_error_code retval;
217 size_t i;
218
219 retval = k5_json_object_create(&obj);
220 if (retval != 0)
221 goto cleanup;
222
223 if (chl->service.data) {
224 retval = k5_json_string_create_len(chl->service.data,
225 chl->service.length, &str);
226 if (retval != 0)
227 goto cleanup;
228 retval = k5_json_object_set(obj, "service", str);
229 k5_json_release(str);
230 if (retval != 0)
231 goto cleanup;
232 }
233
234 retval = k5_json_array_create(&arr);
235 if (retval != 0)
236 goto cleanup;
237
238 for (i = 0; chl->tokeninfo[i] != NULL ; i++) {
239 retval = codec_encode_tokeninfo(chl->tokeninfo[i], &tmp);
240 if (retval != 0)
241 goto cleanup;
242
243 retval = k5_json_array_add(arr, tmp);
244 k5_json_release(tmp);
245 if (retval != 0)
246 goto cleanup;
247 }
248
249 retval = k5_json_object_set(obj, "tokenInfo", arr);
250 if (retval != 0)
251 goto cleanup;
252
253 retval = k5_json_encode(obj, json);
254 if (retval)
255 goto cleanup;
256
257 cleanup:
258 k5_json_release(arr);
259 k5_json_release(obj);
260 return retval;
261 }
262
263 /* Converts a JSON object into a krb5_responder_otp_tokeninfo. */
264 static krb5_responder_otp_tokeninfo *
codec_decode_tokeninfo(k5_json_object obj)265 codec_decode_tokeninfo(k5_json_object obj)
266 {
267 krb5_responder_otp_tokeninfo *ti = NULL;
268 krb5_error_code retval;
269
270 ti = calloc(1, sizeof(krb5_responder_otp_tokeninfo));
271 if (ti == NULL)
272 goto error;
273
274 retval = codec_value_to_int32(obj, "flags", &ti->flags);
275 if (retval != 0)
276 goto error;
277
278 retval = codec_value_to_string(obj, "vendor", &ti->vendor);
279 if (retval != 0 && retval != ENOENT)
280 goto error;
281
282 retval = codec_value_to_string(obj, "challenge", &ti->challenge);
283 if (retval != 0 && retval != ENOENT)
284 goto error;
285
286 retval = codec_value_to_int32(obj, "length", &ti->length);
287 if (retval == ENOENT)
288 ti->length = -1;
289 else if (retval != 0)
290 goto error;
291
292 retval = codec_value_to_int32(obj, "format", &ti->format);
293 if (retval == ENOENT)
294 ti->format = -1;
295 else if (retval != 0)
296 goto error;
297
298 retval = codec_value_to_string(obj, "tokenID", &ti->token_id);
299 if (retval != 0 && retval != ENOENT)
300 goto error;
301
302 retval = codec_value_to_string(obj, "algID", &ti->alg_id);
303 if (retval != 0 && retval != ENOENT)
304 goto error;
305
306 return ti;
307
308 error:
309 free_tokeninfo(ti);
310 return NULL;
311 }
312
313 /* Converts a JSON object into a krb5_responder_otp_challenge. */
314 static krb5_responder_otp_challenge *
codec_decode_challenge(krb5_context ctx,const char * json)315 codec_decode_challenge(krb5_context ctx, const char *json)
316 {
317 krb5_responder_otp_challenge *chl = NULL;
318 k5_json_value obj = NULL, arr = NULL, tmp = NULL;
319 krb5_error_code retval;
320 size_t i;
321
322 retval = k5_json_decode(json, &obj);
323 if (retval != 0)
324 goto error;
325
326 if (k5_json_get_tid(obj) != K5_JSON_TID_OBJECT)
327 goto error;
328
329 arr = k5_json_object_get(obj, "tokenInfo");
330 if (arr == NULL)
331 goto error;
332
333 if (k5_json_get_tid(arr) != K5_JSON_TID_ARRAY)
334 goto error;
335
336 chl = calloc(1, sizeof(krb5_responder_otp_challenge));
337 if (chl == NULL)
338 goto error;
339
340 chl->tokeninfo = calloc(k5_json_array_length(arr) + 1,
341 sizeof(krb5_responder_otp_tokeninfo*));
342 if (chl->tokeninfo == NULL)
343 goto error;
344
345 retval = codec_value_to_string(obj, "service", &chl->service);
346 if (retval != 0 && retval != ENOENT)
347 goto error;
348
349 for (i = 0; i < k5_json_array_length(arr); i++) {
350 tmp = k5_json_array_get(arr, i);
351 if (k5_json_get_tid(tmp) != K5_JSON_TID_OBJECT)
352 goto error;
353
354 chl->tokeninfo[i] = codec_decode_tokeninfo(tmp);
355 if (chl->tokeninfo[i] == NULL)
356 goto error;
357 }
358
359 k5_json_release(obj);
360 return chl;
361
362 error:
363 if (chl != NULL) {
364 for (i = 0; chl->tokeninfo != NULL && chl->tokeninfo[i] != NULL; i++)
365 free_tokeninfo(chl->tokeninfo[i]);
366 free(chl->tokeninfo);
367 free(chl);
368 }
369 k5_json_release(obj);
370 return NULL;
371 }
372
373 /* Decode the responder answer into a tokeninfo, a value and a pin. */
374 static krb5_error_code
codec_decode_answer(krb5_context context,const char * answer,krb5_otp_tokeninfo ** tis,krb5_otp_tokeninfo ** ti,krb5_data * value,krb5_data * pin)375 codec_decode_answer(krb5_context context, const char *answer,
376 krb5_otp_tokeninfo **tis, krb5_otp_tokeninfo **ti,
377 krb5_data *value, krb5_data *pin)
378 {
379 krb5_error_code retval;
380 k5_json_value val = NULL;
381 krb5_int32 indx;
382 krb5_data tmp;
383 size_t i;
384
385 if (answer == NULL)
386 return EBADMSG;
387
388 retval = k5_json_decode(answer, &val);
389 if (retval != 0)
390 goto cleanup;
391
392 if (k5_json_get_tid(val) != K5_JSON_TID_OBJECT)
393 goto cleanup;
394
395 retval = codec_value_to_int32(val, "tokeninfo", &indx);
396 if (retval != 0)
397 goto cleanup;
398
399 for (i = 0; tis[i] != NULL; i++) {
400 if (i == (size_t)indx) {
401 retval = codec_value_to_data(val, "value", &tmp);
402 if (retval != 0 && retval != ENOENT)
403 goto cleanup;
404
405 retval = codec_value_to_data(val, "pin", pin);
406 if (retval != 0 && retval != ENOENT) {
407 krb5_free_data_contents(context, &tmp);
408 goto cleanup;
409 }
410
411 *value = tmp;
412 *ti = tis[i];
413 retval = 0;
414 goto cleanup;
415 }
416 }
417 retval = EINVAL;
418
419 cleanup:
420 k5_json_release(val);
421 return retval;
422 }
423
424 /* Takes the nonce from the challenge and encrypts it into the request. */
425 static krb5_error_code
encrypt_nonce(krb5_context ctx,krb5_keyblock * key,const krb5_pa_otp_challenge * chl,krb5_pa_otp_req * req)426 encrypt_nonce(krb5_context ctx, krb5_keyblock *key,
427 const krb5_pa_otp_challenge *chl, krb5_pa_otp_req *req)
428 {
429 krb5_error_code retval;
430 krb5_enc_data encdata;
431 krb5_data *er;
432
433 /* Encode the nonce. */
434 retval = encode_krb5_pa_otp_enc_req(&chl->nonce, &er);
435 if (retval != 0)
436 return retval;
437
438 /* Do the encryption. */
439 retval = krb5_encrypt_helper(ctx, key, KRB5_KEYUSAGE_PA_OTP_REQUEST,
440 er, &encdata);
441 krb5_free_data(ctx, er);
442 if (retval != 0)
443 return retval;
444
445 req->enc_data = encdata;
446 return 0;
447 }
448
449 /* Checks to see if the user-supplied otp value matches the length and format
450 * of the supplied tokeninfo. */
451 static int
otpvalue_matches_tokeninfo(const char * otpvalue,krb5_otp_tokeninfo * ti)452 otpvalue_matches_tokeninfo(const char *otpvalue, krb5_otp_tokeninfo *ti)
453 {
454 int (*table[])(int c) = { isdigit, isxdigit, isalnum };
455
456 if (otpvalue == NULL || ti == NULL)
457 return 0;
458
459 if (ti->length >= 0 && strlen(otpvalue) != (size_t)ti->length)
460 return 0;
461
462 if (ti->format >= 0 && ti->format < 3) {
463 while (*otpvalue) {
464 if (!(*table[ti->format])((unsigned char)*otpvalue++))
465 return 0;
466 }
467 }
468
469 return 1;
470 }
471
472 /* Performs a prompt and saves the response in the out parameter. */
473 static krb5_error_code
doprompt(krb5_context context,krb5_prompter_fct prompter,void * prompter_data,const char * banner,const char * prompttxt,char * out,size_t len)474 doprompt(krb5_context context, krb5_prompter_fct prompter, void *prompter_data,
475 const char *banner, const char *prompttxt, char *out, size_t len)
476 {
477 krb5_prompt prompt;
478 krb5_data prompt_reply;
479 krb5_error_code retval;
480 krb5_prompt_type prompt_type = KRB5_PROMPT_TYPE_PREAUTH;
481
482 if (prompttxt == NULL || out == NULL)
483 return EINVAL;
484
485 memset(out, 0, len);
486
487 prompt_reply = make_data(out, len);
488 prompt.reply = &prompt_reply;
489 prompt.prompt = (char *)prompttxt;
490 prompt.hidden = 1;
491
492 /* PROMPTER_INVOCATION */
493 k5_set_prompt_types(context, &prompt_type);
494 retval = (*prompter)(context, prompter_data, NULL, banner, 1, &prompt);
495 k5_set_prompt_types(context, NULL);
496 if (retval != 0)
497 return retval;
498
499 return 0;
500 }
501
502 /* Forces the user to choose a single tokeninfo via prompting. */
503 static krb5_error_code
prompt_for_tokeninfo(krb5_context context,krb5_prompter_fct prompter,void * prompter_data,krb5_otp_tokeninfo ** tis,krb5_otp_tokeninfo ** out_ti)504 prompt_for_tokeninfo(krb5_context context, krb5_prompter_fct prompter,
505 void *prompter_data, krb5_otp_tokeninfo **tis,
506 krb5_otp_tokeninfo **out_ti)
507 {
508 char response[1024], *prompt;
509 krb5_otp_tokeninfo *ti = NULL;
510 krb5_error_code retval = 0;
511 struct k5buf buf;
512 size_t i = 0, j = 0;
513
514 k5_buf_init_dynamic(&buf);
515 k5_buf_add(&buf, _("Please choose from the following:\n"));
516 for (i = 0; tis[i] != NULL; i++) {
517 k5_buf_add_fmt(&buf, "\t%ld. %s ", (long)(i + 1), _("Vendor:"));
518 k5_buf_add_len(&buf, tis[i]->vendor.data, tis[i]->vendor.length);
519 k5_buf_add(&buf, "\n");
520 }
521 prompt = k5_buf_cstring(&buf);
522 if (prompt == NULL)
523 return ENOMEM;
524
525 do {
526 retval = doprompt(context, prompter, prompter_data, prompt,
527 _("Enter #"), response, sizeof(response));
528 if (retval != 0)
529 goto cleanup;
530
531 errno = 0;
532 j = strtoul(response, NULL, 0);
533 if (errno != 0) {
534 retval = errno;
535 goto cleanup;
536 }
537 if (j < 1 || j > i)
538 continue;
539
540 ti = tis[--j];
541 } while (ti == NULL);
542
543 *out_ti = ti;
544
545 cleanup:
546 k5_buf_free(&buf);
547 return retval;
548 }
549
550 /* Builds a challenge string from the given tokeninfo. */
551 static krb5_error_code
make_challenge(const krb5_otp_tokeninfo * ti,char ** challenge)552 make_challenge(const krb5_otp_tokeninfo *ti, char **challenge)
553 {
554 if (challenge == NULL)
555 return EINVAL;
556
557 *challenge = NULL;
558
559 if (ti == NULL || ti->challenge.data == NULL)
560 return 0;
561
562 if (asprintf(challenge, "%s %.*s\n",
563 _("OTP Challenge:"),
564 ti->challenge.length,
565 ti->challenge.data) < 0)
566 return ENOMEM;
567
568 return 0;
569 }
570
571 /* Determines if a pin is required. If it is, it will be prompted for. */
572 static inline krb5_error_code
collect_pin(krb5_context context,krb5_prompter_fct prompter,void * prompter_data,const krb5_otp_tokeninfo * ti,krb5_data * out_pin)573 collect_pin(krb5_context context, krb5_prompter_fct prompter,
574 void *prompter_data, const krb5_otp_tokeninfo *ti,
575 krb5_data *out_pin)
576 {
577 krb5_error_code retval;
578 char otppin[1024];
579 krb5_flags collect;
580 krb5_data pin;
581
582 /* If no PIN will be collected, don't prompt. */
583 collect = ti->flags & (KRB5_OTP_FLAG_COLLECT_PIN |
584 KRB5_OTP_FLAG_SEPARATE_PIN);
585 if (collect == 0) {
586 *out_pin = empty_data();
587 return 0;
588 }
589
590 /* Collect the PIN. */
591 retval = doprompt(context, prompter, prompter_data, NULL,
592 _("OTP Token PIN"), otppin, sizeof(otppin));
593 if (retval != 0)
594 return retval;
595
596 /* Set the PIN. */
597 pin = make_data(strdup(otppin), strlen(otppin));
598 if (pin.data == NULL)
599 return ENOMEM;
600
601 *out_pin = pin;
602 return 0;
603 }
604
605 /* Builds a request using the specified tokeninfo, value and pin. */
606 static krb5_error_code
make_request(krb5_context ctx,krb5_otp_tokeninfo * ti,const krb5_data * value,const krb5_data * pin,krb5_pa_otp_req ** out_req)607 make_request(krb5_context ctx, krb5_otp_tokeninfo *ti, const krb5_data *value,
608 const krb5_data *pin, krb5_pa_otp_req **out_req)
609 {
610 krb5_pa_otp_req *req = NULL;
611 krb5_error_code retval = 0;
612
613 if (ti == NULL)
614 return 0;
615
616 if (ti->format == KRB5_OTP_FORMAT_BASE64)
617 return ENOTSUP;
618
619 req = calloc(1, sizeof(krb5_pa_otp_req));
620 if (req == NULL)
621 return ENOMEM;
622
623 req->flags = ti->flags & KRB5_OTP_FLAG_NEXTOTP;
624
625 retval = krb5int_copy_data_contents(ctx, &ti->vendor, &req->vendor);
626 if (retval != 0)
627 goto error;
628
629 req->format = ti->format;
630
631 retval = krb5int_copy_data_contents(ctx, &ti->token_id, &req->token_id);
632 if (retval != 0)
633 goto error;
634
635 retval = krb5int_copy_data_contents(ctx, &ti->alg_id, &req->alg_id);
636 if (retval != 0)
637 goto error;
638
639 retval = krb5int_copy_data_contents(ctx, value, &req->otp_value);
640 if (retval != 0)
641 goto error;
642
643 if (ti->flags & KRB5_OTP_FLAG_COLLECT_PIN) {
644 if (ti->flags & KRB5_OTP_FLAG_SEPARATE_PIN) {
645 if (pin == NULL || pin->data == NULL) {
646 retval = EINVAL; /* No pin found! */
647 goto error;
648 }
649
650 retval = krb5int_copy_data_contents(ctx, pin, &req->pin);
651 if (retval != 0)
652 goto error;
653 } else if (pin != NULL && pin->data != NULL) {
654 krb5_free_data_contents(ctx, &req->otp_value);
655 retval = asprintf(&req->otp_value.data, "%.*s%.*s",
656 pin->length, pin->data,
657 value->length, value->data);
658 if (retval < 0) {
659 retval = ENOMEM;
660 req->otp_value = empty_data();
661 goto error;
662 }
663 req->otp_value.length = req->pin.length + req->otp_value.length;
664 } /* Otherwise, the responder has already combined them. */
665 }
666
667 *out_req = req;
668 return 0;
669
670 error:
671 k5_free_pa_otp_req(ctx, req);
672 return retval;
673 }
674
675 /*
676 * Filters a set of tokeninfos given an otp value. If the set is reduced to
677 * a single tokeninfo, it will be set in out_ti. Otherwise, a new shallow copy
678 * will be allocated in out_filtered.
679 */
680 static inline krb5_error_code
filter_tokeninfos(krb5_context context,const char * otpvalue,krb5_otp_tokeninfo ** tis,krb5_otp_tokeninfo *** out_filtered,krb5_otp_tokeninfo ** out_ti)681 filter_tokeninfos(krb5_context context, const char *otpvalue,
682 krb5_otp_tokeninfo **tis,
683 krb5_otp_tokeninfo ***out_filtered,
684 krb5_otp_tokeninfo **out_ti)
685 {
686 krb5_otp_tokeninfo **filtered;
687 size_t i = 0, j = 0;
688
689 while (tis[i] != NULL)
690 i++;
691
692 filtered = calloc(i + 1, sizeof(const krb5_otp_tokeninfo *));
693 if (filtered == NULL)
694 return ENOMEM;
695
696 /* Make a list of tokeninfos that match the value. */
697 for (i = 0, j = 0; tis[i] != NULL; i++) {
698 if (otpvalue_matches_tokeninfo(otpvalue, tis[i]))
699 filtered[j++] = tis[i];
700 }
701
702 /* It is an error if we have no matching tokeninfos. */
703 if (filtered[0] == NULL) {
704 free(filtered);
705 k5_setmsg(context, KRB5_PREAUTH_FAILED,
706 _("OTP value doesn't match any token formats"));
707 return KRB5_PREAUTH_FAILED; /* We have no supported tokeninfos. */
708 }
709
710 /* Otherwise, if we have just one tokeninfo, choose it. */
711 if (filtered[1] == NULL) {
712 *out_ti = filtered[0];
713 *out_filtered = NULL;
714 free(filtered);
715 return 0;
716 }
717
718 /* Otherwise, we'll return the remaining list. */
719 *out_ti = NULL;
720 *out_filtered = filtered;
721 return 0;
722 }
723
724 /* Outputs the selected tokeninfo and possibly a value and pin.
725 * Prompting may occur. */
726 static krb5_error_code
prompt_for_token(krb5_context context,krb5_prompter_fct prompter,void * prompter_data,krb5_otp_tokeninfo ** tis,krb5_otp_tokeninfo ** out_ti,krb5_data * out_value,krb5_data * out_pin)727 prompt_for_token(krb5_context context, krb5_prompter_fct prompter,
728 void *prompter_data, krb5_otp_tokeninfo **tis,
729 krb5_otp_tokeninfo **out_ti, krb5_data *out_value,
730 krb5_data *out_pin)
731 {
732 krb5_otp_tokeninfo **filtered = NULL;
733 krb5_otp_tokeninfo *ti = NULL;
734 krb5_error_code retval;
735 size_t i, challengers = 0;
736 char *challenge = NULL;
737 char otpvalue[1024];
738 krb5_data value, pin;
739
740 memset(otpvalue, 0, sizeof(otpvalue));
741
742 if (tis == NULL || tis[0] == NULL || out_ti == NULL)
743 return EINVAL;
744
745 /* Count how many challenges we have. */
746 for (i = 0; tis[i] != NULL; i++) {
747 if (tis[i]->challenge.data != NULL)
748 challengers++;
749 }
750
751 /* If we have only one tokeninfo as input, choose it. */
752 if (i == 1)
753 ti = tis[0];
754
755 /* Setup our challenge, if present. */
756 if (challengers > 0) {
757 /* If we have multiple tokeninfos still, choose now. */
758 if (ti == NULL) {
759 retval = prompt_for_tokeninfo(context, prompter, prompter_data,
760 tis, &ti);
761 if (retval != 0)
762 return retval;
763 }
764
765 /* Create the challenge prompt. */
766 retval = make_challenge(ti, &challenge);
767 if (retval != 0)
768 return retval;
769 }
770
771 /* Prompt for token value. */
772 retval = doprompt(context, prompter, prompter_data, challenge,
773 _("Enter OTP Token Value"), otpvalue, sizeof(otpvalue));
774 free(challenge);
775 if (retval != 0)
776 return retval;
777
778 if (ti == NULL) {
779 /* Filter out tokeninfos that don't match our token value. */
780 retval = filter_tokeninfos(context, otpvalue, tis, &filtered, &ti);
781 if (retval != 0)
782 return retval;
783
784 /* If we still don't have a single tokeninfo, choose now. */
785 if (filtered != NULL) {
786 retval = prompt_for_tokeninfo(context, prompter, prompter_data,
787 filtered, &ti);
788 free(filtered);
789 if (retval != 0)
790 return retval;
791 }
792 }
793
794 assert(ti != NULL);
795
796 /* Set the value. */
797 value = make_data(strdup(otpvalue), strlen(otpvalue));
798 if (value.data == NULL)
799 return ENOMEM;
800
801 /* Collect the PIN, if necessary. */
802 retval = collect_pin(context, prompter, prompter_data, ti, &pin);
803 if (retval != 0) {
804 krb5_free_data_contents(context, &value);
805 return retval;
806 }
807
808 *out_value = value;
809 *out_pin = pin;
810 *out_ti = ti;
811 return 0;
812 }
813
814 /* Encode the OTP request into a krb5_pa_data buffer. */
815 static krb5_error_code
set_pa_data(const krb5_pa_otp_req * req,krb5_pa_data *** pa_data_out)816 set_pa_data(const krb5_pa_otp_req *req, krb5_pa_data ***pa_data_out)
817 {
818 krb5_pa_data **out = NULL;
819 krb5_data *tmp;
820
821 /* Allocate the preauth data array and one item. */
822 out = calloc(2, sizeof(krb5_pa_data *));
823 if (out == NULL)
824 goto error;
825 out[0] = calloc(1, sizeof(krb5_pa_data));
826 out[1] = NULL;
827 if (out[0] == NULL)
828 goto error;
829
830 /* Encode our request into the preauth data item. */
831 memset(out[0], 0, sizeof(krb5_pa_data));
832 out[0]->pa_type = KRB5_PADATA_OTP_REQUEST;
833 if (encode_krb5_pa_otp_req(req, &tmp) != 0)
834 goto error;
835 out[0]->contents = (krb5_octet *)tmp->data;
836 out[0]->length = tmp->length;
837 free(tmp);
838
839 *pa_data_out = out;
840 return 0;
841
842 error:
843 if (out != NULL) {
844 free(out[0]);
845 free(out);
846 }
847 return ENOMEM;
848 }
849
850 /* Tests krb5_data to see if it is printable. */
851 static krb5_boolean
is_printable_string(const krb5_data * data)852 is_printable_string(const krb5_data *data)
853 {
854 unsigned int i;
855
856 if (data == NULL)
857 return FALSE;
858
859 for (i = 0; i < data->length; i++) {
860 if (!isprint((unsigned char)data->data[i]))
861 return FALSE;
862 }
863
864 return TRUE;
865 }
866
867 /* Returns TRUE when the given tokeninfo contains the subset of features we
868 * support. */
869 static krb5_boolean
is_tokeninfo_supported(krb5_otp_tokeninfo * ti)870 is_tokeninfo_supported(krb5_otp_tokeninfo *ti)
871 {
872 krb5_flags supported_flags = KRB5_OTP_FLAG_COLLECT_PIN |
873 KRB5_OTP_FLAG_NO_COLLECT_PIN |
874 KRB5_OTP_FLAG_SEPARATE_PIN;
875
876 /* Flags we don't support... */
877 if (ti->flags & ~supported_flags)
878 return FALSE;
879
880 /* We don't currently support hashing. */
881 if (ti->supported_hash_alg != NULL || ti->iteration_count >= 0)
882 return FALSE;
883
884 /* Remove tokeninfos with invalid vendor strings. */
885 if (!is_printable_string(&ti->vendor))
886 return FALSE;
887
888 /* Remove tokeninfos with non-printable challenges. */
889 if (!is_printable_string(&ti->challenge))
890 return FALSE;
891
892 /* We don't currently support base64. */
893 if (ti->format == KRB5_OTP_FORMAT_BASE64)
894 return FALSE;
895
896 return TRUE;
897 }
898
899 /* Removes unsupported tokeninfos. Returns an error if no tokeninfos remain. */
900 static krb5_error_code
filter_supported_tokeninfos(krb5_context context,krb5_otp_tokeninfo ** tis)901 filter_supported_tokeninfos(krb5_context context, krb5_otp_tokeninfo **tis)
902 {
903 size_t i, j;
904
905 /* Filter out any tokeninfos we don't support. */
906 for (i = 0, j = 0; tis[i] != NULL; i++) {
907 if (!is_tokeninfo_supported(tis[i]))
908 k5_free_otp_tokeninfo(context, tis[i]);
909 else
910 tis[j++] = tis[i];
911 }
912
913 /* Terminate the array. */
914 tis[j] = NULL;
915
916 if (tis[0] != NULL)
917 return 0;
918
919 k5_setmsg(context, KRB5_PREAUTH_FAILED, _("No supported tokens"));
920 return KRB5_PREAUTH_FAILED; /* We have no supported tokeninfos. */
921 }
922
923 /*
924 * Try to find tokeninfos which match configuration data recorded in the input
925 * ccache, and if exactly one is found, drop the rest.
926 */
927 static krb5_error_code
filter_config_tokeninfos(krb5_context context,krb5_clpreauth_callbacks cb,krb5_clpreauth_rock rock,krb5_otp_tokeninfo ** tis)928 filter_config_tokeninfos(krb5_context context,
929 krb5_clpreauth_callbacks cb,
930 krb5_clpreauth_rock rock,
931 krb5_otp_tokeninfo **tis)
932 {
933 krb5_otp_tokeninfo *match = NULL;
934 size_t i, j;
935 const char *vendor, *alg_id, *token_id;
936
937 /* Pull up what we know about the token we want to use. */
938 vendor = cb->get_cc_config(context, rock, "vendor");
939 alg_id = cb->get_cc_config(context, rock, "algID");
940 token_id = cb->get_cc_config(context, rock, "tokenID");
941
942 /* Look for a single matching entry. */
943 for (i = 0; tis[i] != NULL; i++) {
944 if (vendor != NULL && tis[i]->vendor.length > 0 &&
945 !data_eq_string(tis[i]->vendor, vendor))
946 continue;
947 if (alg_id != NULL && tis[i]->alg_id.length > 0 &&
948 !data_eq_string(tis[i]->alg_id, alg_id))
949 continue;
950 if (token_id != NULL && tis[i]->token_id.length > 0 &&
951 !data_eq_string(tis[i]->token_id, token_id))
952 continue;
953 /* Oh, we already had a matching entry. More than one -> no change. */
954 if (match != NULL)
955 return 0;
956 match = tis[i];
957 }
958
959 /* No matching entry -> no change. */
960 if (match == NULL)
961 return 0;
962
963 /* Prune out everything except the best match. */
964 for (i = 0, j = 0; tis[i] != NULL; i++) {
965 if (tis[i] != match)
966 k5_free_otp_tokeninfo(context, tis[i]);
967 else
968 tis[j++] = tis[i];
969 }
970 tis[j] = NULL;
971
972 return 0;
973 }
974
975 static void
otp_client_request_init(krb5_context context,krb5_clpreauth_moddata moddata,krb5_clpreauth_modreq * modreq_out)976 otp_client_request_init(krb5_context context, krb5_clpreauth_moddata moddata,
977 krb5_clpreauth_modreq *modreq_out)
978 {
979 *modreq_out = calloc(1, sizeof(krb5_pa_otp_challenge *));
980 }
981
982 static krb5_error_code
otp_client_prep_questions(krb5_context context,krb5_clpreauth_moddata moddata,krb5_clpreauth_modreq modreq,krb5_get_init_creds_opt * opt,krb5_clpreauth_callbacks cb,krb5_clpreauth_rock rock,krb5_kdc_req * request,krb5_data * encoded_request_body,krb5_data * encoded_previous_request,krb5_pa_data * pa_data)983 otp_client_prep_questions(krb5_context context, krb5_clpreauth_moddata moddata,
984 krb5_clpreauth_modreq modreq,
985 krb5_get_init_creds_opt *opt,
986 krb5_clpreauth_callbacks cb,
987 krb5_clpreauth_rock rock, krb5_kdc_req *request,
988 krb5_data *encoded_request_body,
989 krb5_data *encoded_previous_request,
990 krb5_pa_data *pa_data)
991 {
992 krb5_pa_otp_challenge *chl;
993 krb5_error_code retval;
994 krb5_data tmp;
995 char *json;
996
997 if (modreq == NULL)
998 return ENOMEM;
999
1000 /* Decode the challenge. */
1001 tmp = make_data(pa_data->contents, pa_data->length);
1002 retval = decode_krb5_pa_otp_challenge(&tmp,
1003 (krb5_pa_otp_challenge **)modreq);
1004 if (retval != 0)
1005 return retval;
1006 chl = *(krb5_pa_otp_challenge **)modreq;
1007
1008 /* Remove unsupported tokeninfos. */
1009 retval = filter_supported_tokeninfos(context, chl->tokeninfo);
1010 if (retval != 0)
1011 return retval;
1012
1013 /* Remove tokeninfos that don't match the recorded description, if that
1014 * results in there being only one that does. */
1015 retval = filter_config_tokeninfos(context, cb, rock, chl->tokeninfo);
1016 if (retval != 0)
1017 return retval;
1018
1019 /* Make the JSON representation. */
1020 retval = codec_encode_challenge(context, chl, &json);
1021 if (retval != 0)
1022 return retval;
1023
1024 /* Ask the question. */
1025 retval = cb->ask_responder_question(context, rock,
1026 KRB5_RESPONDER_QUESTION_OTP,
1027 json);
1028 free(json);
1029 return retval;
1030 }
1031
1032 /*
1033 * Save the vendor, algID, and tokenID values for the selected token to the
1034 * out_ccache, so that later we can try to use them to select the right one
1035 * without having ot ask the user.
1036 */
1037 static void
save_config_tokeninfo(krb5_context context,krb5_clpreauth_callbacks cb,krb5_clpreauth_rock rock,krb5_otp_tokeninfo * ti)1038 save_config_tokeninfo(krb5_context context,
1039 krb5_clpreauth_callbacks cb,
1040 krb5_clpreauth_rock rock,
1041 krb5_otp_tokeninfo *ti)
1042 {
1043 char *tmp;
1044 if (ti->vendor.length > 0 &&
1045 asprintf(&tmp, "%.*s", ti->vendor.length, ti->vendor.data) >= 0) {
1046 cb->set_cc_config(context, rock, "vendor", tmp);
1047 free(tmp);
1048 }
1049 if (ti->alg_id.length > 0 &&
1050 asprintf(&tmp, "%.*s", ti->alg_id.length, ti->alg_id.data) >= 0) {
1051 cb->set_cc_config(context, rock, "algID", tmp);
1052 free(tmp);
1053 }
1054 if (ti->token_id.length > 0 &&
1055 asprintf(&tmp, "%.*s", ti->token_id.length, ti->token_id.data) >= 0) {
1056 cb->set_cc_config(context, rock, "tokenID", tmp);
1057 free(tmp);
1058 }
1059 }
1060
1061 static krb5_error_code
otp_client_process(krb5_context context,krb5_clpreauth_moddata moddata,krb5_clpreauth_modreq modreq,krb5_get_init_creds_opt * opt,krb5_clpreauth_callbacks cb,krb5_clpreauth_rock rock,krb5_kdc_req * request,krb5_data * encoded_request_body,krb5_data * encoded_previous_request,krb5_pa_data * pa_data,krb5_prompter_fct prompter,void * prompter_data,krb5_pa_data *** pa_data_out)1062 otp_client_process(krb5_context context, krb5_clpreauth_moddata moddata,
1063 krb5_clpreauth_modreq modreq, krb5_get_init_creds_opt *opt,
1064 krb5_clpreauth_callbacks cb, krb5_clpreauth_rock rock,
1065 krb5_kdc_req *request, krb5_data *encoded_request_body,
1066 krb5_data *encoded_previous_request, krb5_pa_data *pa_data,
1067 krb5_prompter_fct prompter, void *prompter_data,
1068 krb5_pa_data ***pa_data_out)
1069 {
1070 krb5_pa_otp_challenge *chl = NULL;
1071 krb5_otp_tokeninfo *ti = NULL;
1072 krb5_keyblock *as_key = NULL;
1073 krb5_pa_otp_req *req = NULL;
1074 krb5_error_code retval = 0;
1075 krb5_data value, pin;
1076 const char *answer;
1077
1078 if (modreq == NULL)
1079 return ENOMEM;
1080 chl = *(krb5_pa_otp_challenge **)modreq;
1081
1082 *pa_data_out = NULL;
1083
1084 /* Get FAST armor key. */
1085 as_key = cb->fast_armor(context, rock);
1086 if (as_key == NULL)
1087 return ENOENT;
1088
1089 /* Attempt to get token selection from the responder. */
1090 pin = empty_data();
1091 value = empty_data();
1092 answer = cb->get_responder_answer(context, rock,
1093 KRB5_RESPONDER_QUESTION_OTP);
1094 retval = codec_decode_answer(context, answer, chl->tokeninfo, &ti, &value,
1095 &pin);
1096 if (retval != 0) {
1097 /* If the responder doesn't have a token selection,
1098 * we need to select the token via prompting. */
1099 retval = prompt_for_token(context, prompter, prompter_data,
1100 chl->tokeninfo, &ti, &value, &pin);
1101 if (retval != 0)
1102 goto error;
1103 }
1104
1105 /* Make the request. */
1106 retval = make_request(context, ti, &value, &pin, &req);
1107 if (retval != 0)
1108 goto error;
1109
1110 /* Save information about the token which was used. */
1111 save_config_tokeninfo(context, cb, rock, ti);
1112
1113 /* Encrypt the challenge's nonce and set it in the request. */
1114 retval = encrypt_nonce(context, as_key, chl, req);
1115 if (retval != 0)
1116 goto error;
1117
1118 /* Use FAST armor key as response key. */
1119 retval = cb->set_as_key(context, rock, as_key);
1120 if (retval != 0)
1121 goto error;
1122
1123 /* Encode the request into the pa_data output. */
1124 retval = set_pa_data(req, pa_data_out);
1125 if (retval != 0)
1126 goto error;
1127 cb->disable_fallback(context, rock);
1128
1129 error:
1130 krb5_free_data_contents(context, &value);
1131 krb5_free_data_contents(context, &pin);
1132 k5_free_pa_otp_req(context, req);
1133 return retval;
1134 }
1135
1136 static void
otp_client_request_fini(krb5_context context,krb5_clpreauth_moddata moddata,krb5_clpreauth_modreq modreq)1137 otp_client_request_fini(krb5_context context, krb5_clpreauth_moddata moddata,
1138 krb5_clpreauth_modreq modreq)
1139 {
1140 if (modreq == NULL)
1141 return;
1142
1143 k5_free_pa_otp_challenge(context, *(krb5_pa_otp_challenge **)modreq);
1144 free(modreq);
1145 }
1146
1147 krb5_error_code
clpreauth_otp_initvt(krb5_context context,int maj_ver,int min_ver,krb5_plugin_vtable vtable)1148 clpreauth_otp_initvt(krb5_context context, int maj_ver, int min_ver,
1149 krb5_plugin_vtable vtable)
1150 {
1151 krb5_clpreauth_vtable vt;
1152
1153 if (maj_ver != 1)
1154 return KRB5_PLUGIN_VER_NOTSUPP;
1155
1156 vt = (krb5_clpreauth_vtable)vtable;
1157 vt->name = "otp";
1158 vt->pa_type_list = otp_client_supported_pa_types;
1159 vt->request_init = otp_client_request_init;
1160 vt->prep_questions = otp_client_prep_questions;
1161 vt->process = otp_client_process;
1162 vt->request_fini = otp_client_request_fini;
1163 vt->gic_opts = NULL;
1164
1165 return 0;
1166 }
1167
1168 krb5_error_code KRB5_CALLCONV
krb5_responder_otp_get_challenge(krb5_context ctx,krb5_responder_context rctx,krb5_responder_otp_challenge ** chl)1169 krb5_responder_otp_get_challenge(krb5_context ctx,
1170 krb5_responder_context rctx,
1171 krb5_responder_otp_challenge **chl)
1172 {
1173 const char *answer;
1174 krb5_responder_otp_challenge *challenge;
1175
1176 answer = krb5_responder_get_challenge(ctx, rctx,
1177 KRB5_RESPONDER_QUESTION_OTP);
1178 if (answer == NULL) {
1179 *chl = NULL;
1180 return 0;
1181 }
1182
1183 challenge = codec_decode_challenge(ctx, answer);
1184 if (challenge == NULL)
1185 return ENOMEM;
1186
1187 *chl = challenge;
1188 return 0;
1189 }
1190
1191 krb5_error_code KRB5_CALLCONV
krb5_responder_otp_set_answer(krb5_context ctx,krb5_responder_context rctx,size_t ti,const char * value,const char * pin)1192 krb5_responder_otp_set_answer(krb5_context ctx, krb5_responder_context rctx,
1193 size_t ti, const char *value, const char *pin)
1194 {
1195 krb5_error_code retval;
1196 k5_json_object obj = NULL;
1197 k5_json_number num;
1198 k5_json_string str;
1199 char *tmp;
1200
1201 retval = k5_json_object_create(&obj);
1202 if (retval != 0)
1203 goto error;
1204
1205 retval = k5_json_number_create(ti, &num);
1206 if (retval != 0)
1207 goto error;
1208
1209 retval = k5_json_object_set(obj, "tokeninfo", num);
1210 k5_json_release(num);
1211 if (retval != 0)
1212 goto error;
1213
1214 if (value != NULL) {
1215 retval = k5_json_string_create(value, &str);
1216 if (retval != 0)
1217 goto error;
1218
1219 retval = k5_json_object_set(obj, "value", str);
1220 k5_json_release(str);
1221 if (retval != 0)
1222 goto error;
1223 }
1224
1225 if (pin != NULL) {
1226 retval = k5_json_string_create(pin, &str);
1227 if (retval != 0)
1228 goto error;
1229
1230 retval = k5_json_object_set(obj, "pin", str);
1231 k5_json_release(str);
1232 if (retval != 0)
1233 goto error;
1234 }
1235
1236 retval = k5_json_encode(obj, &tmp);
1237 if (retval != 0)
1238 goto error;
1239 k5_json_release(obj);
1240
1241 retval = krb5_responder_set_answer(ctx, rctx, KRB5_RESPONDER_QUESTION_OTP,
1242 tmp);
1243 free(tmp);
1244 return retval;
1245
1246 error:
1247 k5_json_release(obj);
1248 return retval;
1249 }
1250
1251 void KRB5_CALLCONV
krb5_responder_otp_challenge_free(krb5_context ctx,krb5_responder_context rctx,krb5_responder_otp_challenge * chl)1252 krb5_responder_otp_challenge_free(krb5_context ctx,
1253 krb5_responder_context rctx,
1254 krb5_responder_otp_challenge *chl)
1255 {
1256 size_t i;
1257
1258 if (chl == NULL)
1259 return;
1260
1261 for (i = 0; chl->tokeninfo[i]; i++)
1262 free_tokeninfo(chl->tokeninfo[i]);
1263 free(chl->service);
1264 free(chl->tokeninfo);
1265 free(chl);
1266 }
1267