1 /* 2 * Copyright (c) 1999, 2010, Oracle and/or its affiliates. All rights reserved. */ 3 4 /* 5 * Copyright 2019 Joyent, Inc. 6 */ 7 8 #include "k5-int.h" 9 #include "com_err.h" 10 #include <admin.h> 11 #include <locale.h> 12 #include <syslog.h> 13 14 /* Solaris Kerberos: 15 * 16 * Change Password functionality is handled by the libkadm5clnt.so.1 library in 17 * Solaris Kerberos. In order to avoid a circular dependency between that lib 18 * and the kerberos mech lib, we use the #pragma weak compiler directive. 19 * This way, when applications link with the libkadm5clnt.so.1 lib the circular 20 * dependancy between the two libs will be resolved. 21 */ 22 23 #pragma weak kadm5_get_cpw_host_srv_name 24 #pragma weak kadm5_init_with_password 25 #pragma weak kadm5_chpass_principal_util 26 27 extern kadm5_ret_t kadm5_get_cpw_host_srv_name(krb5_context, const char *, 28 char **); 29 extern kadm5_ret_t kadm5_init_with_password(char *, char *, char *, 30 kadm5_config_params *, krb5_ui_4, krb5_ui_4, char **, 31 void **); 32 extern kadm5_ret_t kadm5_chpass_principal_util(void *, krb5_principal, 33 char *, char **, char *, unsigned int); 34 35 /* 36 * Solaris Kerberos: 37 * See the function's definition for the description of this interface. 38 */ 39 krb5_error_code __krb5_get_init_creds_password(krb5_context, 40 krb5_creds *, krb5_principal, char *, krb5_prompter_fct, void *, 41 krb5_deltat, char *, krb5_get_init_creds_opt *, krb5_kdc_rep **); 42 43 static krb5_error_code 44 krb5_get_as_key_password( 45 krb5_context context, 46 krb5_principal client, 47 krb5_enctype etype, 48 krb5_prompter_fct prompter, 49 void *prompter_data, 50 krb5_data *salt, 51 krb5_data *params, 52 krb5_keyblock *as_key, 53 void *gak_data) 54 { 55 krb5_data *password; 56 krb5_error_code ret; 57 krb5_data defsalt; 58 char *clientstr; 59 char promptstr[1024]; 60 krb5_prompt prompt; 61 krb5_prompt_type prompt_type; 62 63 password = (krb5_data *) gak_data; 64 65 /* If there's already a key of the correct etype, we're done. 66 If the etype is wrong, free the existing key, and make 67 a new one. 68 69 XXX This was the old behavior, and was wrong in hw preauth 70 cases. Is this new behavior -- always asking -- correct in all 71 cases? */ 72 73 if (as_key->length) { 74 if (as_key->enctype != etype) { 75 krb5_free_keyblock_contents (context, as_key); 76 as_key->length = 0; 77 } 78 } 79 80 if (password->data[0] == '\0') { 81 if (prompter == NULL) 82 prompter = krb5_prompter_posix; /* Solaris Kerberos */ 83 84 if ((ret = krb5_unparse_name(context, client, &clientstr))) 85 return(ret); 86 87 strcpy(promptstr, "Password for "); 88 strncat(promptstr, clientstr, sizeof(promptstr)-strlen(promptstr)-1); 89 promptstr[sizeof(promptstr)-1] = '\0'; 90 91 free(clientstr); 92 93 prompt.prompt = promptstr; 94 prompt.hidden = 1; 95 prompt.reply = password; 96 prompt_type = KRB5_PROMPT_TYPE_PASSWORD; 97 98 /* PROMPTER_INVOCATION */ 99 krb5int_set_prompt_types(context, &prompt_type); 100 if ((ret = (((*prompter)(context, prompter_data, NULL, NULL, 101 1, &prompt))))) { 102 krb5int_set_prompt_types(context, 0); 103 return(ret); 104 } 105 krb5int_set_prompt_types(context, 0); 106 } 107 108 if ((salt->length == -1 || salt->length == SALT_TYPE_AFS_LENGTH) && (salt->data == NULL)) { 109 if ((ret = krb5_principal2salt(context, client, &defsalt))) 110 return(ret); 111 112 salt = &defsalt; 113 } else { 114 defsalt.length = 0; 115 } 116 117 ret = krb5_c_string_to_key_with_params(context, etype, password, salt, 118 params->data?params:NULL, as_key); 119 120 if (defsalt.length) 121 krb5_xfree(defsalt.data); 122 123 return(ret); 124 } 125 126 krb5_error_code KRB5_CALLCONV 127 krb5_get_init_creds_password(krb5_context context, 128 krb5_creds *creds, 129 krb5_principal client, 130 char *password, 131 krb5_prompter_fct prompter, 132 void *data, 133 krb5_deltat start_time, 134 char *in_tkt_service, 135 krb5_get_init_creds_opt *options) 136 { 137 /* 138 * Solaris Kerberos: 139 * We call our own private function that returns the as_reply back to 140 * the caller. This structure contains information, such as 141 * key-expiration and last-req fields. Entities such as pam_krb5 can 142 * use this information to provide account/password expiration warnings. 143 * The original "prompter" interface is not granular enough for PAM, 144 * as it will perform all passes w/o coordination with other modules. 145 */ 146 return (__krb5_get_init_creds_password(context, creds, client, password, 147 prompter, data, start_time, in_tkt_service, options, NULL)); 148 } 149 150 /* 151 * Solaris Kerberos: 152 * See krb5_get_init_creds_password()'s comments for the justification of this 153 * private function. Caller must free ptr_as_reply if non-NULL. 154 */ 155 krb5_error_code KRB5_CALLCONV 156 __krb5_get_init_creds_password( 157 krb5_context context, 158 krb5_creds *creds, 159 krb5_principal client, 160 char *password, 161 krb5_prompter_fct prompter, 162 void *data, 163 krb5_deltat start_time, 164 char *in_tkt_service, 165 krb5_get_init_creds_opt *options, 166 krb5_kdc_rep **ptr_as_reply) 167 { 168 krb5_error_code ret, ret2; 169 int use_master; 170 krb5_kdc_rep *as_reply; 171 int tries; 172 krb5_creds chpw_creds; 173 krb5_data pw0, pw1; 174 char banner[1024], pw0array[1024], pw1array[1024]; 175 krb5_prompt prompt[2]; 176 krb5_prompt_type prompt_types[sizeof(prompt)/sizeof(prompt[0])]; 177 krb5_gic_opt_ext *opte = NULL; 178 krb5_gic_opt_ext *chpw_opte = NULL; 179 180 char admin_realm[1024], *cpw_service=NULL, *princ_str=NULL; 181 kadm5_config_params params; 182 void *server_handle; 183 const char *err_msg_1 = NULL; 184 185 use_master = 0; 186 as_reply = NULL; 187 memset(&chpw_creds, 0, sizeof(chpw_creds)); 188 189 pw0.data = pw0array; 190 191 if (password && password[0]) { 192 if ((pw0.length = strlen(password)) > sizeof(pw0array)) { 193 ret = EINVAL; 194 goto cleanup; 195 } 196 strcpy(pw0.data, password); 197 } else { 198 pw0.data[0] = '\0'; 199 pw0.length = sizeof(pw0array); 200 } 201 202 pw1.data = pw1array; 203 pw1.data[0] = '\0'; 204 pw1.length = sizeof(pw1array); 205 206 ret = krb5int_gic_opt_to_opte(context, options, &opte, 1, 207 "krb5_get_init_creds_password"); 208 if (ret) 209 goto cleanup; 210 211 /* first try: get the requested tkt from any kdc */ 212 213 ret = krb5_get_init_creds(context, creds, client, prompter, data, 214 start_time, in_tkt_service, opte, 215 krb5_get_as_key_password, (void *) &pw0, 216 &use_master, &as_reply); 217 /* check for success */ 218 219 if (ret == 0) 220 goto cleanup; 221 222 /* If all the kdc's are unavailable, or if the error was due to a 223 user interrupt, or preauth errored out, fail */ 224 225 if ((ret == KRB5_KDC_UNREACH) || 226 (ret == KRB5_PREAUTH_FAILED) || 227 (ret == KRB5_LIBOS_PWDINTR) || 228 (ret == KRB5_REALM_CANT_RESOLVE)) 229 goto cleanup; 230 231 /* if the reply did not come from the master kdc, try again with 232 the master kdc */ 233 234 if (!use_master) { 235 use_master = 1; 236 237 if (as_reply) { 238 krb5_free_kdc_rep( context, as_reply); 239 as_reply = NULL; 240 } 241 242 err_msg_1 = krb5_get_error_message(context, ret); 243 244 ret2 = krb5_get_init_creds(context, creds, client, prompter, data, 245 start_time, in_tkt_service, opte, 246 krb5_get_as_key_password, (void *) &pw0, 247 &use_master, &as_reply); 248 249 if (ret2 == 0) { 250 ret = 0; 251 goto cleanup; 252 } 253 254 /* if the master is unreachable, return the error from the 255 slave we were able to contact or reset the use_master flag */ 256 257 if ((ret2 != KRB5_KDC_UNREACH) && 258 (ret2 != KRB5_REALM_CANT_RESOLVE) && 259 (ret2 != KRB5_REALM_UNKNOWN)) { 260 ret = ret2; 261 } else { 262 use_master = 0; 263 /* Solaris - if 2nd try failed, reset 1st err msg */ 264 if (ret2 && err_msg_1) { 265 krb5_set_error_message(context, ret, err_msg_1); 266 } 267 } 268 } 269 270 /* Solaris Kerberos: 163 resync */ 271 /* #ifdef USE_LOGIN_LIBRARY */ 272 if (ret == KRB5KDC_ERR_KEY_EXP) 273 goto cleanup; /* Login library will deal appropriately with this error */ 274 /* #endif */ 275 276 /* at this point, we have an error from the master. if the error 277 is not password expired, or if it is but there's no prompter, 278 return this error */ 279 280 if ((ret != KRB5KDC_ERR_KEY_EXP) || 281 (prompter == NULL)) 282 goto cleanup; 283 284 /* historically the default has been to prompt for password change. 285 * if the change password prompt option has not been set, we continue 286 * to prompt. Prompting is only disabled if the option has been set 287 * and the value has been set to false. 288 */ 289 if (!(options->flags & KRB5_GET_INIT_CREDS_OPT_CHG_PWD_PRMPT)) 290 goto cleanup; 291 292 /* ok, we have an expired password. Give the user a few chances 293 to change it */ 294 295 296 /* 297 * Solaris Kerberos: 298 * Get the correct change password service principal name to use. 299 * This is necessary because SEAM based admin servers require 300 * a slightly different service principal name than MIT/MS servers. 301 */ 302 303 memset((char *) ¶ms, 0, sizeof (params)); 304 305 snprintf(admin_realm, sizeof (admin_realm), 306 krb5_princ_realm(context, client)->data); 307 params.mask |= KADM5_CONFIG_REALM; 308 params.realm = admin_realm; 309 310 ret=kadm5_get_cpw_host_srv_name(context, admin_realm, &cpw_service); 311 312 if (ret != KADM5_OK) { 313 syslog(LOG_ERR, dgettext(TEXT_DOMAIN, 314 "Kerberos mechanism library: Unable to get change password " 315 "service name for realm %s\n"), admin_realm); 316 goto cleanup; 317 } else { 318 ret=0; 319 } 320 321 /* extract the string version of the principal */ 322 if ((ret = krb5_unparse_name(context, client, &princ_str))) 323 goto cleanup; 324 325 ret = kadm5_init_with_password(princ_str, pw0array, cpw_service, 326 ¶ms, KADM5_STRUCT_VERSION, KADM5_API_VERSION_2, NULL, 327 &server_handle); 328 329 if (ret != 0) { 330 goto cleanup; 331 } 332 333 prompt[0].prompt = "Enter new password"; 334 prompt[0].hidden = 1; 335 prompt[0].reply = &pw0; 336 prompt_types[0] = KRB5_PROMPT_TYPE_NEW_PASSWORD; 337 338 prompt[1].prompt = "Enter it again"; 339 prompt[1].hidden = 1; 340 prompt[1].reply = &pw1; 341 prompt_types[1] = KRB5_PROMPT_TYPE_NEW_PASSWORD_AGAIN; 342 343 strcpy(banner, "Password expired. You must change it now."); 344 345 for (tries = 3; tries; tries--) { 346 pw0.length = sizeof(pw0array); 347 pw1.length = sizeof(pw1array); 348 349 /* PROMPTER_INVOCATION */ 350 krb5int_set_prompt_types(context, prompt_types); 351 if ((ret = ((*prompter)(context, data, 0, banner, 352 sizeof(prompt)/sizeof(prompt[0]), prompt)))) 353 goto cleanup; 354 krb5int_set_prompt_types(context, 0); 355 356 357 if (strcmp(pw0.data, pw1.data) != 0) { 358 ret = KRB5_LIBOS_BADPWDMATCH; 359 sprintf(banner, "%s. Please try again.", error_message(ret)); 360 } else if (pw0.length == 0) { 361 ret = KRB5_CHPW_PWDNULL; 362 sprintf(banner, "%s. Please try again.", error_message(ret)); 363 } else { 364 int result_code; 365 krb5_data code_string; 366 krb5_data result_string; 367 368 if ((ret = krb5_change_password(context, &chpw_creds, pw0array, 369 &result_code, &code_string, 370 &result_string))) 371 goto cleanup; 372 373 /* the change succeeded. go on */ 374 375 if (result_code == 0) { 376 krb5_xfree(result_string.data); 377 break; 378 } 379 380 /* set this in case the retry loop falls through */ 381 382 ret = KRB5_CHPW_FAIL; 383 384 if (result_code != KRB5_KPASSWD_SOFTERROR) { 385 krb5_xfree(result_string.data); 386 goto cleanup; 387 } 388 389 /* the error was soft, so try again */ 390 391 /* 100 is I happen to know that no code_string will be longer 392 than 100 chars */ 393 394 if (result_string.length > (sizeof(banner)-100)) 395 result_string.length = sizeof(banner)-100; 396 397 sprintf(banner, "%.*s%s%.*s. Please try again.\n", 398 (int) code_string.length, code_string.data, 399 result_string.length ? ": " : "", 400 (int) result_string.length, 401 result_string.data ? result_string.data : ""); 402 403 krb5_xfree(code_string.data); 404 krb5_xfree(result_string.data); 405 } 406 } 407 408 if (ret) 409 goto cleanup; 410 411 /* the password change was successful. Get an initial ticket 412 from the master. this is the last try. the return from this 413 is final. */ 414 415 ret = krb5_get_init_creds(context, creds, client, prompter, data, 416 start_time, in_tkt_service, opte, 417 krb5_get_as_key_password, (void *) &pw0, 418 &use_master, &as_reply); 419 420 cleanup: 421 if (err_msg_1) 422 free((void *)err_msg_1); 423 424 krb5int_set_prompt_types(context, 0); 425 /* if getting the password was successful, then check to see if the 426 password is about to expire, and warn if so */ 427 428 if (ret == 0) { 429 krb5_timestamp now; 430 krb5_last_req_entry **last_req; 431 int hours; 432 433 /* XXX 7 days should be configurable. This is all pretty ad hoc, 434 and could probably be improved if I was willing to screw around 435 with timezones, etc. */ 436 437 if (prompter && 438 (in_tkt_service && cpw_service && 439 (strcmp(in_tkt_service, cpw_service) != 0)) && 440 ((ret = krb5_timeofday(context, &now)) == 0) && 441 as_reply->enc_part2->key_exp && 442 ((hours = ((as_reply->enc_part2->key_exp-now)/(60*60))) <= 7*24) && 443 (hours >= 0)) { 444 if (hours < 1) 445 sprintf(banner, 446 "Warning: Your password will expire in less than one hour."); 447 else if (hours <= 48) 448 sprintf(banner, "Warning: Your password will expire in %d hour%s.", 449 hours, (hours == 1)?"":"s"); 450 else 451 sprintf(banner, "Warning: Your password will expire in %d days.", 452 hours/24); 453 454 /* ignore an error here */ 455 /* PROMPTER_INVOCATION */ 456 (*prompter)(context, data, 0, banner, 0, 0); 457 } else if (prompter && 458 (!in_tkt_service || 459 (strcmp(in_tkt_service, "kadmin/changepw") != 0)) && 460 as_reply->enc_part2 && as_reply->enc_part2->last_req) { 461 /* 462 * Check the last_req fields 463 */ 464 465 for (last_req = as_reply->enc_part2->last_req; *last_req; last_req++) 466 if ((*last_req)->lr_type == KRB5_LRQ_ALL_PW_EXPTIME || 467 (*last_req)->lr_type == KRB5_LRQ_ONE_PW_EXPTIME) { 468 krb5_deltat delta; 469 char ts[256]; 470 471 if ((ret = krb5_timeofday(context, &now))) 472 break; 473 474 if ((ret = krb5_timestamp_to_string((*last_req)->value, 475 ts, sizeof(ts)))) 476 break; 477 478 delta = (*last_req)->value - now; 479 480 if (delta < 3600) 481 sprintf(banner, 482 "Warning: Your password will expire in less than one " 483 "hour on %s", ts); 484 else if (delta < 86400*2) 485 sprintf(banner, 486 "Warning: Your password will expire in %d hour%s on %s", 487 delta / 3600, delta < 7200 ? "" : "s", ts); 488 else 489 sprintf(banner, 490 "Warning: Your password will expire in %d days on %s", 491 delta / 86400, ts); 492 /* ignore an error here */ 493 /* PROMPTER_INVOCATION */ 494 (*prompter)(context, data, 0, banner, 0, 0); 495 } 496 } 497 } 498 499 free(cpw_service); 500 free(princ_str); 501 if (opte && krb5_gic_opt_is_shadowed(opte)) 502 krb5_get_init_creds_opt_free(context, (krb5_get_init_creds_opt *)opte); 503 memset(pw0array, 0, sizeof(pw0array)); 504 memset(pw1array, 0, sizeof(pw1array)); 505 krb5_free_cred_contents(context, &chpw_creds); 506 /* 507 * Solaris Kerberos: 508 * Argument, ptr_as_reply, being returned to caller if success and non-NULL. 509 */ 510 if (as_reply != NULL) { 511 if (ptr_as_reply == NULL) 512 krb5_free_kdc_rep(context, as_reply); 513 else 514 *ptr_as_reply = as_reply; 515 } 516 517 return(ret); 518 } 519 krb5_error_code krb5int_populate_gic_opt ( 520 krb5_context context, krb5_gic_opt_ext **opte, 521 krb5_flags options, krb5_address * const *addrs, krb5_enctype *ktypes, 522 krb5_preauthtype *pre_auth_types, krb5_creds *creds) 523 { 524 int i; 525 krb5_int32 starttime; 526 krb5_get_init_creds_opt opt; 527 528 529 krb5_get_init_creds_opt_init(&opt); 530 if (addrs) 531 krb5_get_init_creds_opt_set_address_list(&opt, (krb5_address **) addrs); 532 if (ktypes) { 533 for (i=0; ktypes[i]; i++); 534 if (i) 535 krb5_get_init_creds_opt_set_etype_list(&opt, ktypes, i); 536 } 537 if (pre_auth_types) { 538 for (i=0; pre_auth_types[i]; i++); 539 if (i) 540 krb5_get_init_creds_opt_set_preauth_list(&opt, pre_auth_types, i); 541 } 542 if (options&KDC_OPT_FORWARDABLE) 543 krb5_get_init_creds_opt_set_forwardable(&opt, 1); 544 else krb5_get_init_creds_opt_set_forwardable(&opt, 0); 545 if (options&KDC_OPT_PROXIABLE) 546 krb5_get_init_creds_opt_set_proxiable(&opt, 1); 547 else krb5_get_init_creds_opt_set_proxiable(&opt, 0); 548 if (creds && creds->times.endtime) { 549 krb5_timeofday(context, &starttime); 550 if (creds->times.starttime) starttime = creds->times.starttime; 551 krb5_get_init_creds_opt_set_tkt_life(&opt, creds->times.endtime - starttime); 552 } 553 return krb5int_gic_opt_to_opte(context, &opt, opte, 0, 554 "krb5int_populate_gic_opt"); 555 } 556 557 /* 558 Rewrites get_in_tkt in terms of newer get_init_creds API. 559 Attempts to get an initial ticket for creds->client to use server 560 creds->server, (realm is taken from creds->client), with options 561 options, and using creds->times.starttime, creds->times.endtime, 562 creds->times.renew_till as from, till, and rtime. 563 creds->times.renew_till is ignored unless the RENEWABLE option is requested. 564 565 If addrs is non-NULL, it is used for the addresses requested. If it is 566 null, the system standard addresses are used. 567 568 If password is non-NULL, it is converted using the cryptosystem entry 569 point for a string conversion routine, seeded with the client's name. 570 If password is passed as NULL, the password is read from the terminal, 571 and then converted into a key. 572 573 A succesful call will place the ticket in the credentials cache ccache. 574 575 returns system errors, encryption errors 576 */ 577 krb5_error_code KRB5_CALLCONV 578 krb5_get_in_tkt_with_password(krb5_context context, krb5_flags options, 579 krb5_address *const *addrs, krb5_enctype *ktypes, 580 krb5_preauthtype *pre_auth_types, 581 const char *password, krb5_ccache ccache, 582 krb5_creds *creds, krb5_kdc_rep **ret_as_reply) 583 { 584 krb5_error_code retval; 585 krb5_data pw0; 586 char pw0array[1024]; 587 char * server; 588 krb5_principal server_princ, client_princ; 589 int use_master = 0; 590 krb5_gic_opt_ext *opte = NULL; 591 592 pw0array[0] = '\0'; 593 pw0.data = pw0array; 594 if (password) { 595 pw0.length = strlen(password); 596 if (pw0.length > sizeof(pw0array)) 597 return EINVAL; 598 strncpy(pw0.data, password, sizeof(pw0array)); 599 if (pw0.length == 0) 600 pw0.length = sizeof(pw0array); 601 } else { 602 pw0.length = sizeof(pw0array); 603 } 604 retval = krb5int_populate_gic_opt(context, &opte, 605 options, addrs, ktypes, 606 pre_auth_types, creds); 607 if (retval) 608 return (retval); 609 retval = krb5_unparse_name( context, creds->server, &server); 610 if (retval) { 611 return (retval); 612 krb5_get_init_creds_opt_free(context, (krb5_get_init_creds_opt *)opte); 613 } 614 server_princ = creds->server; 615 client_princ = creds->client; 616 retval = krb5_get_init_creds (context, 617 creds, creds->client, 618 krb5_prompter_posix, NULL, 619 0, server, opte, 620 krb5_get_as_key_password, &pw0, 621 &use_master, ret_as_reply); 622 krb5_free_unparsed_name( context, server); 623 krb5_get_init_creds_opt_free(context, (krb5_get_init_creds_opt *)opte); 624 if (retval) { 625 return (retval); 626 } 627 if (creds->server) 628 krb5_free_principal( context, creds->server); 629 if (creds->client) 630 krb5_free_principal( context, creds->client); 631 creds->client = client_princ; 632 creds->server = server_princ; 633 /* store it in the ccache! */ 634 if (ccache) 635 if ((retval = krb5_cc_store_cred(context, ccache, creds))) 636 return (retval); 637 return retval; 638 } 639