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