1 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 /* plugins/preauth/otp/otp_state.c - Verify OTP token values using RADIUS */ 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 #include "otp_state.h" 31 32 #include <krad.h> 33 #include <k5-json.h> 34 35 #include <ctype.h> 36 37 #ifndef HOST_NAME_MAX 38 /* SUSv2 */ 39 #define HOST_NAME_MAX 255 40 #endif 41 42 #define DEFAULT_TYPE_NAME "DEFAULT" 43 #define DEFAULT_SOCKET_FMT KDC_RUN_DIR "/%s.socket" 44 #define DEFAULT_TIMEOUT 5 45 #define DEFAULT_RETRIES 3 46 #define MAX_SECRET_LEN 1024 47 48 typedef struct token_type_st { 49 char *name; 50 char *server; 51 char *secret; 52 int timeout; 53 size_t retries; 54 krb5_boolean strip_realm; 55 char **indicators; 56 } token_type; 57 58 typedef struct token_st { 59 const token_type *type; 60 krb5_data username; 61 char **indicators; 62 } token; 63 64 typedef struct request_st { 65 otp_state *state; 66 token *tokens; 67 ssize_t index; 68 otp_cb cb; 69 void *data; 70 krad_attrset *attrs; 71 } request; 72 73 struct otp_state_st { 74 krb5_context ctx; 75 token_type *types; 76 krad_client *radius; 77 krad_attrset *attrs; 78 }; 79 80 static void request_send(request *req); 81 82 static krb5_error_code 83 read_secret_file(const char *secret_file, char **secret) 84 { 85 char buf[MAX_SECRET_LEN]; 86 krb5_error_code retval; 87 char *filename = NULL; 88 FILE *file; 89 size_t i, j; 90 91 *secret = NULL; 92 93 retval = k5_path_join(KDC_DIR, secret_file, &filename); 94 if (retval != 0) { 95 com_err("otp", retval, "Unable to resolve secret file '%s'", filename); 96 goto cleanup; 97 } 98 99 file = fopen(filename, "r"); 100 if (file == NULL) { 101 retval = errno; 102 com_err("otp", retval, "Unable to open secret file '%s'", filename); 103 goto cleanup; 104 } 105 106 if (fgets(buf, sizeof(buf), file) == NULL) 107 retval = EIO; 108 fclose(file); 109 if (retval != 0) { 110 com_err("otp", retval, "Unable to read secret file '%s'", filename); 111 goto cleanup; 112 } 113 114 /* Strip whitespace. */ 115 for (i = 0; buf[i] != '\0'; i++) { 116 if (!isspace(buf[i])) 117 break; 118 } 119 for (j = strlen(buf); j > i; j--) { 120 if (!isspace(buf[j - 1])) 121 break; 122 } 123 124 *secret = k5memdup0(&buf[i], j - i, &retval); 125 126 cleanup: 127 free(filename); 128 return retval; 129 } 130 131 /* Free the contents of a single token type. */ 132 static void 133 token_type_free(token_type *type) 134 { 135 if (type == NULL) 136 return; 137 138 free(type->name); 139 free(type->server); 140 free(type->secret); 141 profile_free_list(type->indicators); 142 } 143 144 /* Construct the internal default token type. */ 145 static krb5_error_code 146 token_type_default(token_type *out) 147 { 148 char *name = NULL, *server = NULL, *secret = NULL; 149 150 memset(out, 0, sizeof(*out)); 151 152 name = strdup(DEFAULT_TYPE_NAME); 153 if (name == NULL) 154 goto oom; 155 if (asprintf(&server, DEFAULT_SOCKET_FMT, name) < 0) 156 goto oom; 157 secret = strdup(""); 158 if (secret == NULL) 159 goto oom; 160 161 out->name = name; 162 out->server = server; 163 out->secret = secret; 164 out->timeout = DEFAULT_TIMEOUT * 1000; 165 out->retries = DEFAULT_RETRIES; 166 out->strip_realm = FALSE; 167 return 0; 168 169 oom: 170 free(name); 171 free(server); 172 free(secret); 173 return ENOMEM; 174 } 175 176 /* Decode a single token type from the profile. */ 177 static krb5_error_code 178 token_type_decode(profile_t profile, const char *name, token_type *out) 179 { 180 char *server = NULL, *name_copy = NULL, *secret = NULL, *pstr = NULL; 181 char **indicators = NULL; 182 const char *keys[4]; 183 int strip_realm, timeout, retries; 184 krb5_error_code retval; 185 186 memset(out, 0, sizeof(*out)); 187 188 /* Set the name. */ 189 name_copy = strdup(name); 190 if (name_copy == NULL) 191 return ENOMEM; 192 193 /* Set strip_realm. */ 194 retval = profile_get_boolean(profile, "otp", name, "strip_realm", TRUE, 195 &strip_realm); 196 if (retval != 0) 197 goto cleanup; 198 199 /* Set the server. */ 200 retval = profile_get_string(profile, "otp", name, "server", NULL, &pstr); 201 if (retval != 0) 202 goto cleanup; 203 if (pstr != NULL) { 204 server = strdup(pstr); 205 profile_release_string(pstr); 206 if (server == NULL) { 207 retval = ENOMEM; 208 goto cleanup; 209 } 210 } else if (asprintf(&server, DEFAULT_SOCKET_FMT, name) < 0) { 211 retval = ENOMEM; 212 goto cleanup; 213 } 214 215 /* Get the secret (optional for Unix-domain sockets). */ 216 retval = profile_get_string(profile, "otp", name, "secret", NULL, &pstr); 217 if (retval != 0) 218 goto cleanup; 219 if (pstr != NULL) { 220 retval = read_secret_file(pstr, &secret); 221 profile_release_string(pstr); 222 if (retval != 0) 223 goto cleanup; 224 } else { 225 if (server[0] != '/') { 226 com_err("otp", EINVAL, "Secret missing (token type '%s')", name); 227 retval = EINVAL; 228 goto cleanup; 229 } 230 231 /* Use the default empty secret for UNIX domain stream sockets. */ 232 secret = strdup(""); 233 if (secret == NULL) { 234 retval = ENOMEM; 235 goto cleanup; 236 } 237 } 238 239 /* Get the timeout (profile value in seconds, result in milliseconds). */ 240 retval = profile_get_integer(profile, "otp", name, "timeout", 241 DEFAULT_TIMEOUT, &timeout); 242 if (retval != 0) 243 goto cleanup; 244 timeout *= 1000; 245 246 /* Get the number of retries. */ 247 retval = profile_get_integer(profile, "otp", name, "retries", 248 DEFAULT_RETRIES, &retries); 249 if (retval != 0) 250 goto cleanup; 251 252 /* Get the authentication indicators to assert if this token is used. */ 253 keys[0] = "otp"; 254 keys[1] = name; 255 keys[2] = "indicator"; 256 keys[3] = NULL; 257 retval = profile_get_values(profile, keys, &indicators); 258 if (retval == PROF_NO_RELATION) 259 retval = 0; 260 if (retval != 0) 261 goto cleanup; 262 263 out->name = name_copy; 264 out->server = server; 265 out->secret = secret; 266 out->timeout = timeout; 267 out->retries = retries; 268 out->strip_realm = strip_realm; 269 out->indicators = indicators; 270 name_copy = server = secret = NULL; 271 indicators = NULL; 272 273 cleanup: 274 free(name_copy); 275 free(server); 276 free(secret); 277 profile_free_list(indicators); 278 return retval; 279 } 280 281 /* Free an array of token types. */ 282 static void 283 token_types_free(token_type *types) 284 { 285 size_t i; 286 287 if (types == NULL) 288 return; 289 290 for (i = 0; types[i].server != NULL; i++) 291 token_type_free(&types[i]); 292 293 free(types); 294 } 295 296 /* Decode an array of token types from the profile. */ 297 static krb5_error_code 298 token_types_decode(profile_t profile, token_type **out) 299 { 300 const char *hier[2] = { "otp", NULL }; 301 token_type *types = NULL; 302 char **names = NULL; 303 krb5_error_code retval; 304 size_t i, pos; 305 krb5_boolean have_default = FALSE; 306 307 retval = profile_get_subsection_names(profile, hier, &names); 308 if (retval != 0) 309 return retval; 310 311 /* Check if any of the profile subsections overrides the default. */ 312 for (i = 0; names[i] != NULL; i++) { 313 if (strcmp(names[i], DEFAULT_TYPE_NAME) == 0) 314 have_default = TRUE; 315 } 316 317 /* Leave space for the default (possibly) and the terminator. */ 318 types = k5calloc(i + 2, sizeof(token_type), &retval); 319 if (types == NULL) 320 goto cleanup; 321 322 /* If no default has been specified, use our internal default. */ 323 pos = 0; 324 if (!have_default) { 325 retval = token_type_default(&types[pos++]); 326 if (retval != 0) 327 goto cleanup; 328 } 329 330 /* Decode each profile section into a token type element. */ 331 for (i = 0; names[i] != NULL; i++) { 332 retval = token_type_decode(profile, names[i], &types[pos++]); 333 if (retval != 0) 334 goto cleanup; 335 } 336 337 *out = types; 338 types = NULL; 339 340 cleanup: 341 profile_free_list(names); 342 token_types_free(types); 343 return retval; 344 } 345 346 /* Free a null-terminated array of strings. */ 347 static void 348 free_strings(char **list) 349 { 350 char **p; 351 352 for (p = list; p != NULL && *p != NULL; p++) 353 free(*p); 354 free(list); 355 } 356 357 /* Free the contents of a single token. */ 358 static void 359 token_free_contents(token *t) 360 { 361 if (t == NULL) 362 return; 363 free(t->username.data); 364 free_strings(t->indicators); 365 } 366 367 /* Decode a JSON array of strings into a null-terminated list of C strings. */ 368 static krb5_error_code 369 indicators_decode(krb5_context ctx, k5_json_value val, char ***indicators_out) 370 { 371 k5_json_array arr; 372 k5_json_value obj; 373 char **indicators; 374 size_t len, i; 375 376 *indicators_out = NULL; 377 378 if (k5_json_get_tid(val) != K5_JSON_TID_ARRAY) 379 return EINVAL; 380 arr = val; 381 len = k5_json_array_length(arr); 382 indicators = calloc(len + 1, sizeof(*indicators)); 383 if (indicators == NULL) 384 return ENOMEM; 385 386 for (i = 0; i < len; i++) { 387 obj = k5_json_array_get(arr, i); 388 if (k5_json_get_tid(obj) != K5_JSON_TID_STRING) { 389 free_strings(indicators); 390 return EINVAL; 391 } 392 indicators[i] = strdup(k5_json_string_utf8(obj)); 393 if (indicators[i] == NULL) { 394 free_strings(indicators); 395 return ENOMEM; 396 } 397 } 398 399 *indicators_out = indicators; 400 return 0; 401 } 402 403 /* Decode a single token from a JSON token object. */ 404 static krb5_error_code 405 token_decode(krb5_context ctx, krb5_const_principal princ, 406 const token_type *types, k5_json_object obj, token *out) 407 { 408 const char *typename = DEFAULT_TYPE_NAME; 409 const token_type *type = NULL; 410 char *username = NULL, **indicators = NULL; 411 krb5_error_code retval; 412 k5_json_value val; 413 size_t i; 414 int flags; 415 416 memset(out, 0, sizeof(*out)); 417 418 /* Find the token type. */ 419 val = k5_json_object_get(obj, "type"); 420 if (val != NULL && k5_json_get_tid(val) == K5_JSON_TID_STRING) 421 typename = k5_json_string_utf8(val); 422 for (i = 0; types[i].server != NULL; i++) { 423 if (strcmp(typename, types[i].name) == 0) 424 type = &types[i]; 425 } 426 if (type == NULL) 427 return EINVAL; 428 429 /* Get the username, either from obj or from unparsing the principal. */ 430 val = k5_json_object_get(obj, "username"); 431 if (val != NULL && k5_json_get_tid(val) == K5_JSON_TID_STRING) { 432 username = strdup(k5_json_string_utf8(val)); 433 if (username == NULL) 434 return ENOMEM; 435 } else { 436 flags = type->strip_realm ? KRB5_PRINCIPAL_UNPARSE_NO_REALM : 0; 437 retval = krb5_unparse_name_flags(ctx, princ, flags, &username); 438 if (retval != 0) 439 return retval; 440 } 441 442 /* Get the authentication indicators if specified. */ 443 val = k5_json_object_get(obj, "indicators"); 444 if (val != NULL) { 445 retval = indicators_decode(ctx, val, &indicators); 446 if (retval != 0) { 447 free(username); 448 return retval; 449 } 450 } 451 452 out->type = type; 453 out->username = string2data(username); 454 out->indicators = indicators; 455 return 0; 456 } 457 458 /* Free an array of tokens. */ 459 static void 460 tokens_free(token *tokens) 461 { 462 size_t i; 463 464 if (tokens == NULL) 465 return; 466 467 for (i = 0; tokens[i].type != NULL; i++) 468 token_free_contents(&tokens[i]); 469 470 free(tokens); 471 } 472 473 /* Decode a principal config string into a JSON array. Treat an empty string 474 * or array as if it were "[{}]" which uses the default token type. */ 475 static krb5_error_code 476 decode_config_json(const char *config, k5_json_array *out) 477 { 478 krb5_error_code retval; 479 k5_json_value val; 480 k5_json_object obj; 481 482 *out = NULL; 483 484 /* Decode the config string and make sure it's an array. */ 485 retval = k5_json_decode((config != NULL) ? config : "[{}]", &val); 486 if (retval != 0) 487 goto error; 488 if (k5_json_get_tid(val) != K5_JSON_TID_ARRAY) { 489 retval = EINVAL; 490 goto error; 491 } 492 493 /* If the array is empty, add in an empty object. */ 494 if (k5_json_array_length(val) == 0) { 495 retval = k5_json_object_create(&obj); 496 if (retval != 0) 497 goto error; 498 retval = k5_json_array_add(val, obj); 499 k5_json_release(obj); 500 if (retval != 0) 501 goto error; 502 } 503 504 *out = val; 505 return 0; 506 507 error: 508 k5_json_release(val); 509 return retval; 510 } 511 512 /* Decode an array of tokens from the configuration string. */ 513 static krb5_error_code 514 tokens_decode(krb5_context ctx, krb5_const_principal princ, 515 const token_type *types, const char *config, token **out) 516 { 517 krb5_error_code retval; 518 k5_json_array arr = NULL; 519 k5_json_value obj; 520 token *tokens = NULL; 521 size_t len, i; 522 523 retval = decode_config_json(config, &arr); 524 if (retval != 0) 525 return retval; 526 len = k5_json_array_length(arr); 527 528 tokens = k5calloc(len + 1, sizeof(token), &retval); 529 if (tokens == NULL) 530 goto cleanup; 531 532 for (i = 0; i < len; i++) { 533 obj = k5_json_array_get(arr, i); 534 if (k5_json_get_tid(obj) != K5_JSON_TID_OBJECT) { 535 retval = EINVAL; 536 goto cleanup; 537 } 538 retval = token_decode(ctx, princ, types, obj, &tokens[i]); 539 if (retval != 0) 540 goto cleanup; 541 } 542 543 *out = tokens; 544 tokens = NULL; 545 546 cleanup: 547 k5_json_release(arr); 548 tokens_free(tokens); 549 return retval; 550 } 551 552 static void 553 request_free(request *req) 554 { 555 if (req == NULL) 556 return; 557 558 krad_attrset_free(req->attrs); 559 tokens_free(req->tokens); 560 free(req); 561 } 562 563 krb5_error_code 564 otp_state_new(krb5_context ctx, otp_state **out) 565 { 566 char hostname[HOST_NAME_MAX + 1]; 567 krb5_error_code retval; 568 profile_t profile; 569 krb5_data hndata; 570 otp_state *self; 571 572 retval = gethostname(hostname, sizeof(hostname)); 573 if (retval != 0) 574 return retval; 575 576 self = calloc(1, sizeof(otp_state)); 577 if (self == NULL) 578 return ENOMEM; 579 580 retval = krb5_get_profile(ctx, &profile); 581 if (retval != 0) 582 goto error; 583 584 retval = token_types_decode(profile, &self->types); 585 profile_abandon(profile); 586 if (retval != 0) 587 goto error; 588 589 retval = krad_attrset_new(ctx, &self->attrs); 590 if (retval != 0) 591 goto error; 592 593 hndata = make_data(hostname, strlen(hostname)); 594 retval = krad_attrset_add(self->attrs, 595 krad_attr_name2num("NAS-Identifier"), &hndata); 596 if (retval != 0) 597 goto error; 598 599 retval = krad_attrset_add_number(self->attrs, 600 krad_attr_name2num("Service-Type"), 601 KRAD_SERVICE_TYPE_AUTHENTICATE_ONLY); 602 if (retval != 0) 603 goto error; 604 605 self->ctx = ctx; 606 *out = self; 607 return 0; 608 609 error: 610 otp_state_free(self); 611 return retval; 612 } 613 614 void 615 otp_state_free(otp_state *self) 616 { 617 if (self == NULL) 618 return; 619 620 krad_attrset_free(self->attrs); 621 krad_client_free(self->radius); 622 token_types_free(self->types); 623 free(self); 624 } 625 626 static void 627 callback(krb5_error_code retval, const krad_packet *rqst, 628 const krad_packet *resp, void *data) 629 { 630 request *req = data; 631 token *tok = &req->tokens[req->index]; 632 char *const *indicators; 633 634 req->index++; 635 636 if (retval != 0) 637 goto error; 638 639 /* If we received an accept packet, success! */ 640 if (krad_packet_get_code(resp) == 641 krad_code_name2num("Access-Accept")) { 642 indicators = tok->indicators; 643 if (indicators == NULL) 644 indicators = tok->type->indicators; 645 req->cb(req->data, retval, otp_response_success, indicators); 646 request_free(req); 647 return; 648 } 649 650 /* If we have no more tokens to try, failure! */ 651 if (req->tokens[req->index].type == NULL) 652 goto error; 653 654 /* Try the next token. */ 655 request_send(req); 656 return; 657 658 error: 659 req->cb(req->data, retval, otp_response_fail, NULL); 660 request_free(req); 661 } 662 663 static void 664 request_send(request *req) 665 { 666 krb5_error_code retval; 667 token *tok = &req->tokens[req->index]; 668 const token_type *t = tok->type; 669 670 retval = krad_attrset_add(req->attrs, krad_attr_name2num("User-Name"), 671 &tok->username); 672 if (retval != 0) 673 goto error; 674 675 retval = krad_client_send(req->state->radius, 676 krad_code_name2num("Access-Request"), req->attrs, 677 t->server, t->secret, t->timeout, t->retries, 678 callback, req); 679 krad_attrset_del(req->attrs, krad_attr_name2num("User-Name"), 0); 680 if (retval != 0) 681 goto error; 682 683 return; 684 685 error: 686 req->cb(req->data, retval, otp_response_fail, NULL); 687 request_free(req); 688 } 689 690 void 691 otp_state_verify(otp_state *state, verto_ctx *ctx, krb5_const_principal princ, 692 const char *config, const krb5_pa_otp_req *req, 693 otp_cb cb, void *data) 694 { 695 krb5_error_code retval; 696 request *rqst = NULL; 697 char *name; 698 699 if (state->radius == NULL) { 700 retval = krad_client_new(state->ctx, ctx, &state->radius); 701 if (retval != 0) 702 goto error; 703 } 704 705 rqst = calloc(1, sizeof(request)); 706 if (rqst == NULL) { 707 (*cb)(data, ENOMEM, otp_response_fail, NULL); 708 return; 709 } 710 rqst->state = state; 711 rqst->data = data; 712 rqst->cb = cb; 713 714 retval = krad_attrset_copy(state->attrs, &rqst->attrs); 715 if (retval != 0) 716 goto error; 717 718 retval = krad_attrset_add(rqst->attrs, krad_attr_name2num("User-Password"), 719 &req->otp_value); 720 if (retval != 0) 721 goto error; 722 723 retval = tokens_decode(state->ctx, princ, state->types, config, 724 &rqst->tokens); 725 if (retval != 0) { 726 if (krb5_unparse_name(state->ctx, princ, &name) == 0) { 727 com_err("otp", retval, 728 "Can't decode otp config string for principal '%s'", name); 729 krb5_free_unparsed_name(state->ctx, name); 730 } 731 goto error; 732 } 733 734 request_send(rqst); 735 return; 736 737 error: 738 (*cb)(data, retval, otp_response_fail, NULL); 739 request_free(rqst); 740 } 741