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