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