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
krb5_get_as_key_password(krb5_context context,krb5_principal client,krb5_enctype etype,krb5_prompter_fct prompter,void * prompter_data,krb5_data * salt,krb5_data * params,krb5_keyblock * as_key,void * gak_data)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
krb5_get_init_creds_password(krb5_context context,krb5_creds * creds,krb5_principal client,char * password,krb5_prompter_fct prompter,void * data,krb5_deltat start_time,char * in_tkt_service,krb5_get_init_creds_opt * options)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
__krb5_get_init_creds_password(krb5_context context,krb5_creds * creds,krb5_principal client,char * password,krb5_prompter_fct prompter,void * data,krb5_deltat start_time,char * in_tkt_service,krb5_get_init_creds_opt * options,krb5_kdc_rep ** ptr_as_reply)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 }
krb5int_populate_gic_opt(krb5_context context,krb5_gic_opt_ext ** opte,krb5_flags options,krb5_address * const * addrs,krb5_enctype * ktypes,krb5_preauthtype * pre_auth_types,krb5_creds * creds)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
krb5_get_in_tkt_with_password(krb5_context context,krb5_flags options,krb5_address * const * addrs,krb5_enctype * ktypes,krb5_preauthtype * pre_auth_types,const char * password,krb5_ccache ccache,krb5_creds * creds,krb5_kdc_rep ** ret_as_reply)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
640