1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 
22 /*
23  * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
24  * Use is subject to license terms.
25  */
26 
27 #include <libintl.h>
28 #include <security/pam_appl.h>
29 #include <security/pam_modules.h>
30 #include <string.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <sys/types.h>
34 #include <pwd.h>
35 #include <syslog.h>
36 #include <libintl.h>
37 #include <k5-int.h>
38 #include <netdb.h>
39 #include <unistd.h>
40 #include <sys/stat.h>
41 #include <fcntl.h>
42 #include <errno.h>
43 #include <com_err.h>
44 
45 #include "utils.h"
46 #include "krb5_repository.h"
47 
48 #define	PAMTXD			"SUNW_OST_SYSOSPAM"
49 #define	KRB5_DEFAULT_LIFE	60*60*10  /* 10 hours */
50 
51 extern void krb5_cleanup(pam_handle_t *, void *, int);
52 
53 static int attempt_refresh_cred(krb5_module_data_t *, char *, int);
54 static int attempt_delete_initcred(krb5_module_data_t *);
55 static krb5_error_code krb5_renew_tgt(krb5_module_data_t *, krb5_principal,
56 		krb5_principal, int);
57 
58 extern uint_t kwarn_add_warning(char *, int);
59 extern uint_t kwarn_del_warning(char *);
60 
61 /*
62  * pam_sm_setcred
63  */
64 int
65 pam_sm_setcred(
66 	pam_handle_t *pamh,
67 	int	flags,
68 	int	argc,
69 	const char **argv)
70 {
71 	int	i;
72 	int	err = 0;
73 	int	debug = 0;
74 	krb5_module_data_t	*kmd = NULL;
75 	char			*user = NULL;
76 	krb5_repository_data_t	*krb5_data = NULL;
77 	pam_repository_t	*rep_data = NULL;
78 
79 	for (i = 0; i < argc; i++) {
80 		if (strcasecmp(argv[i], "debug") == 0)
81 			debug = 1;
82 		else if (strcasecmp(argv[i], "nowarn") == 0)
83 			flags = flags | PAM_SILENT;
84 	}
85 
86 	if (debug)
87 		__pam_log(LOG_AUTH | LOG_DEBUG,
88 		    "PAM-KRB5 (setcred): start: nowarn = %d, flags = 0x%x",
89 		    flags & PAM_SILENT ? 1 : 0, flags);
90 
91 	/* make sure flags are valid */
92 	if (flags &&
93 	    !(flags & PAM_ESTABLISH_CRED) &&
94 	    !(flags & PAM_REINITIALIZE_CRED) &&
95 	    !(flags & PAM_REFRESH_CRED) &&
96 	    !(flags & PAM_DELETE_CRED) &&
97 	    !(flags & PAM_SILENT)) {
98 		__pam_log(LOG_AUTH | LOG_ERR,
99 		    "PAM-KRB5 (setcred): illegal flag %d", flags);
100 		err = PAM_SYSTEM_ERR;
101 		goto out;
102 	}
103 
104 	(void) pam_get_item(pamh, PAM_USER, (void**) &user);
105 
106 	if (user == NULL || *user == '\0')
107 		return (PAM_USER_UNKNOWN);
108 
109 	if (pam_get_data(pamh, KRB5_DATA, (const void**)&kmd) != PAM_SUCCESS) {
110 		if (debug) {
111 			__pam_log(LOG_AUTH | LOG_DEBUG,
112 			    "PAM-KRB5 (setcred): kmd get failed, kmd=0x%p",
113 			    kmd);
114 		}
115 
116 		/*
117 		 * User  doesn't need to authenticate for PAM_REFRESH_CRED
118 		 * or for PAM_DELETE_CRED
119 		 */
120 		if (flags & (PAM_REFRESH_CRED|PAM_DELETE_CRED)) {
121 			__pam_log(LOG_AUTH | LOG_DEBUG,
122 			    "PAM-KRB5 (setcred): inst kmd structure");
123 
124 			kmd = calloc(1, sizeof (krb5_module_data_t));
125 
126 			if (kmd == NULL)
127 				return (PAM_BUF_ERR);
128 
129 
130 			/*
131 			 * Need to initialize auth_status here to
132 			 * PAM_AUTHINFO_UNAVAIL else there is a false positive
133 			 * of PAM_SUCCESS.
134 			 */
135 			kmd->auth_status = PAM_AUTHINFO_UNAVAIL;
136 
137 			if ((err = pam_set_data(pamh, KRB5_DATA,
138 			    kmd, &krb5_cleanup)) != PAM_SUCCESS) {
139 				free(kmd);
140 				return (PAM_SYSTEM_ERR);
141 			}
142 		} else {
143 			/*
144 			 * This could mean that we are not the account authority
145 			 * for the authenticated user.  Therefore we should
146 			 * return PAM_IGNORE in order to not affect the
147 			 * login process of said user.
148 			 */
149 			err = PAM_IGNORE;
150 			goto out;
151 		}
152 
153 	} else {  /* pam_get_data success */
154 		if (kmd == NULL) {
155 			if (debug) {
156 				__pam_log(LOG_AUTH | LOG_DEBUG,
157 				    "PAM-KRB5 (setcred): kmd structure"
158 				    " gotten but is NULL for user %s", user);
159 			}
160 			err = PAM_SYSTEM_ERR;
161 			goto out;
162 		}
163 
164 		if (debug)
165 			__pam_log(LOG_AUTH | LOG_DEBUG,
166 			    "PAM-KRB5 (setcred): kmd auth_status: %s",
167 			    pam_strerror(pamh, kmd->auth_status));
168 
169 		/*
170 		 * pam_auth has set status to ignore, so we also return ignore
171 		 */
172 		if (kmd->auth_status == PAM_IGNORE) {
173 			err = PAM_IGNORE;
174 			goto out;
175 		}
176 	}
177 
178 	kmd->debug = debug;
179 
180 	/*
181 	 * User must have passed pam_authenticate()
182 	 * in order to use PAM_ESTABLISH_CRED or PAM_REINITIALIZE_CRED
183 	 */
184 	if ((flags & (PAM_ESTABLISH_CRED|PAM_REINITIALIZE_CRED)) &&
185 	    (kmd->auth_status != PAM_SUCCESS)) {
186 		if (kmd->debug)
187 			__pam_log(LOG_AUTH | LOG_DEBUG,
188 			    "PAM-KRB5 (setcred): unable to "
189 			    "setcreds, not authenticated!");
190 		return (PAM_CRED_UNAVAIL);
191 	}
192 
193 	/*
194 	 * We cannot assume that kmd->kcontext being non-NULL
195 	 * means it is valid.  Other pam_krb5 mods may have
196 	 * freed it but not reset it to NULL.
197 	 * Log a message when debugging to track down memory
198 	 * leaks.
199 	 */
200 	if (kmd->kcontext != NULL && kmd->debug)
201 		__pam_log(LOG_AUTH | LOG_DEBUG,
202 		    "PAM-KRB5 (setcred): kcontext != NULL, "
203 		    "possible memory leak.");
204 
205 	/*
206 	 * Use the authenticated and validated user, if applicable.
207 	 */
208 	if (kmd->user != NULL)
209 		user = kmd->user;
210 
211 	/*
212 	 * If auth was short-circuited we will not have anything to
213 	 * renew, so just return here.
214 	 */
215 	(void) pam_get_item(pamh, PAM_REPOSITORY, (void **)&rep_data);
216 
217 	if (rep_data != NULL) {
218 		if (strcmp(rep_data->type, KRB5_REPOSITORY_NAME) != 0) {
219 			if (debug)
220 				__pam_log(LOG_AUTH | LOG_DEBUG,
221 				    "PAM-KRB5 (setcred): wrong"
222 				    "repository found (%s), returning "
223 				    "PAM_IGNORE", rep_data->type);
224 			return (PAM_IGNORE);
225 		}
226 		if (rep_data->scope_len == sizeof (krb5_repository_data_t)) {
227 			krb5_data = (krb5_repository_data_t *)rep_data->scope;
228 
229 			if (krb5_data->flags ==
230 			    SUNW_PAM_KRB5_ALREADY_AUTHENTICATED &&
231 			    krb5_data->principal != NULL &&
232 			    strlen(krb5_data->principal)) {
233 				if (debug)
234 					__pam_log(LOG_AUTH | LOG_DEBUG,
235 					    "PAM-KRB5 (setcred): "
236 					    "Principal %s already "
237 					    "authenticated, "
238 					    "cannot setcred",
239 					    krb5_data->principal);
240 				return (PAM_SUCCESS);
241 			}
242 		}
243 	}
244 
245 	if (flags & PAM_REINITIALIZE_CRED)
246 		err = attempt_refresh_cred(kmd, user, PAM_REINITIALIZE_CRED);
247 	else if (flags & PAM_REFRESH_CRED)
248 		err = attempt_refresh_cred(kmd, user, PAM_REFRESH_CRED);
249 	else if (flags & PAM_DELETE_CRED)
250 		err = attempt_delete_initcred(kmd);
251 	else {
252 		/*
253 		 * Default case:  PAM_ESTABLISH_CRED
254 		 */
255 		err = attempt_refresh_cred(kmd, user, PAM_ESTABLISH_CRED);
256 	}
257 
258 	if (err != PAM_SUCCESS)
259 		__pam_log(LOG_AUTH | LOG_ERR,
260 		    "PAM-KRB5 (setcred): pam_setcred failed "
261 		    "for %s (%s).", user, pam_strerror(pamh, err));
262 
263 out:
264 	if (kmd && kmd->kcontext) {
265 		/*
266 		 * free 'kcontext' field if it is allocated,
267 		 * kcontext is local to the operation being performed
268 		 * not considered global to the entire pam module.
269 		 */
270 		krb5_free_context(kmd->kcontext);
271 		kmd->kcontext = NULL;
272 	}
273 
274 	/*
275 	 * 'kmd' is not freed here, it is handled in krb5_cleanup
276 	 */
277 	if (debug)
278 		__pam_log(LOG_AUTH | LOG_DEBUG,
279 		    "PAM-KRB5 (setcred): end: %s",
280 		    pam_strerror(pamh, err));
281 	return (err);
282 }
283 
284 static int
285 attempt_refresh_cred(
286 	krb5_module_data_t	*kmd,
287 	char		*user,
288 	int	flag)
289 {
290 	krb5_principal	me;
291 	krb5_principal	server;
292 	krb5_error_code	code;
293 	char		kuser[2*MAXHOSTNAMELEN];
294 	krb5_data tgtname = {
295 		0,
296 		KRB5_TGS_NAME_SIZE,
297 		KRB5_TGS_NAME
298 	};
299 
300 	/* Create a new context here. */
301 	if (krb5_init_secure_context(&kmd->kcontext) != 0) {
302 		if (kmd->debug)
303 			__pam_log(LOG_AUTH | LOG_DEBUG,
304 			    "PAM-KRB5 (setcred): unable to "
305 			    "initialize krb5 context");
306 		return (PAM_SYSTEM_ERR);
307 	}
308 
309 	if (krb5_cc_default(kmd->kcontext, &kmd->ccache) != 0) {
310 		return (PAM_SYSTEM_ERR);
311 	}
312 
313 	if ((code = get_kmd_kuser(kmd->kcontext, (const char *)user, kuser,
314 	    2*MAXHOSTNAMELEN)) != 0) {
315 		return (code);
316 	}
317 
318 	if (krb5_parse_name(kmd->kcontext, kuser, &me) != 0) {
319 		return (PAM_SYSTEM_ERR);
320 	}
321 
322 	if (code = krb5_build_principal_ext(kmd->kcontext, &server,
323 	    krb5_princ_realm(kmd->kcontext, me)->length,
324 	    krb5_princ_realm(kmd->kcontext, me)->data,
325 	    tgtname.length, tgtname.data,
326 	    krb5_princ_realm(kmd->kcontext, me)->length,
327 	    krb5_princ_realm(kmd->kcontext, me)->data, 0)) {
328 		krb5_free_principal(kmd->kcontext, me);
329 		return (PAM_SYSTEM_ERR);
330 	}
331 
332 	code = krb5_renew_tgt(kmd, me, server, flag);
333 
334 	krb5_free_principal(kmd->kcontext, server);
335 	krb5_free_principal(kmd->kcontext, me);
336 
337 	if (code) {
338 		if (kmd->debug)
339 			__pam_log(LOG_AUTH | LOG_DEBUG,
340 			    "PAM-KRB5(setcred): krb5_renew_tgt() "
341 			    "failed: %s", error_message((errcode_t)code));
342 		return (PAM_CRED_ERR);
343 	} else {
344 		return (PAM_SUCCESS);
345 	}
346 }
347 
348 /*
349  * This code will update the credential matching "server" in the user's
350  * credential cache.  The flag may be set to one of:
351  * PAM_REINITIALIZE_CRED/PAM_ESTABLISH_CRED - If we have new credentials then
352  *     create a new cred cache with these credentials else return failure.
353  * PAM_REFRESH_CRED - If we have new credentials then create a new cred cache
354  *  with these credentials else attempt to renew the credentials.
355  *
356  * Note for any of the flags that if a new credential does exist from the
357  * previous auth pass then this will overwrite any existing credentials in the
358  * credential cache.
359  */
360 static krb5_error_code
361 krb5_renew_tgt(
362 	krb5_module_data_t *kmd,
363 	krb5_principal	me,
364 	krb5_principal	server,
365 	int	flag)
366 {
367 	krb5_error_code	retval;
368 	krb5_creds	creds;
369 	krb5_creds	*renewed_cred = NULL;
370 	char		*client_name = NULL;
371 	char		*username = NULL;
372 
373 #define	my_creds	(kmd->initcreds)
374 
375 	if ((flag != PAM_REFRESH_CRED) &&
376 	    (flag != PAM_REINITIALIZE_CRED) &&
377 	    (flag != PAM_ESTABLISH_CRED))
378 		return (KRB5KRB_ERR_GENERIC);
379 
380 	/* this is needed only for the ktkt_warnd */
381 	if ((retval = krb5_unparse_name(kmd->kcontext, me, &client_name)) != 0)
382 		return (retval);
383 
384 	(void) memset(&creds, 0, sizeof (krb5_creds));
385 	if ((retval = krb5_copy_principal(kmd->kcontext,
386 	    server, &creds.server))) {
387 		if (kmd->debug)
388 			__pam_log(LOG_AUTH | LOG_DEBUG,
389 			    "PAM-KRB5 (setcred): krb5_copy_principal "
390 			    "failed: %s",
391 			    error_message((errcode_t)retval));
392 		goto cleanup_creds;
393 	}
394 
395 	/* obtain ticket & session key */
396 	retval = krb5_cc_get_principal(kmd->kcontext,
397 	    kmd->ccache, &creds.client);
398 	if (retval && (kmd->debug))
399 		__pam_log(LOG_AUTH | LOG_DEBUG,
400 		    "PAM-KRB5 (setcred): User not in cred "
401 		    "cache (%s)", error_message((errcode_t)retval));
402 
403 	/*
404 	 * We got here either with the ESTABLISH | REINIT | REFRESH flag and
405 	 * auth_status returns SUCCESS or REFRESH and auth_status failure.
406 	 *
407 	 * Rules:
408 	 * - If the prior auth pass was successful then store the new
409 	 * credentials in the cache, regardless of which flag.
410 	 *
411 	 * - Else if REFRESH flag is used and there are no new
412 	 * credentials then attempt to refresh the existing credentials.
413 	 *
414 	 * - Note, refresh will not work if "R" flag is not set in
415 	 * original credential.  We don't want to 2nd guess the
416 	 * intention of the person who created the existing credential.
417 	 */
418 	if (kmd->auth_status == PAM_SUCCESS) {
419 		/*
420 		 * Create a fresh ccache, and store the credentials
421 		 * we got from pam_authenticate()
422 		 */
423 		if ((retval = krb5_cc_initialize(kmd->kcontext,
424 		    kmd->ccache, me)) != 0) {
425 			__pam_log(LOG_AUTH | LOG_DEBUG,
426 			    "PAM-KRB5 (setcred): krb5_cc_initialize "
427 			    "failed: %s",
428 			    error_message((errcode_t)retval));
429 		} else if ((retval = krb5_cc_store_cred(kmd->kcontext,
430 		    kmd->ccache, &my_creds)) != 0) {
431 			__pam_log(LOG_AUTH | LOG_DEBUG,
432 			    "PAM-KRB5 (setcred): krb5_cc_store_cred "
433 			    "failed: %s",
434 			    error_message((errcode_t)retval));
435 		}
436 	} else if ((retval == 0) && (flag & PAM_REFRESH_CRED)) {
437 		/*
438 		 * If we only wanted to refresh the creds but failed
439 		 * due to expiration, lack of "R" flag, or other
440 		 * problems, return an error.
441 		 */
442 		if (retval = krb5_get_credentials_renew(kmd->kcontext,
443 		    0, kmd->ccache, &creds, &renewed_cred)) {
444 			if (kmd->debug) {
445 				__pam_log(LOG_AUTH | LOG_DEBUG,
446 				    "PAM-KRB5 (setcred): "
447 				    "krb5_get_credentials"
448 				    "_renew(update) failed: %s",
449 				    error_message((errcode_t)retval));
450 			}
451 		}
452 	} else {
453 		/*
454 		 * We failed to get the user's credentials.
455 		 * This might be due to permission error on the cache,
456 		 * or maybe we are looking in the wrong cache file!
457 		 */
458 		__pam_log(LOG_AUTH | LOG_ERR,
459 		    "PAM-KRB5 (setcred): Cannot find creds"
460 		    " for %s (%s)",
461 		    client_name ? client_name : "(unknown)",
462 		    error_message((errcode_t)retval));
463 	}
464 
465 cleanup_creds:
466 
467 	if ((retval == 0) && (client_name != NULL)) {
468 		/*
469 		 * Credential update was successful!
470 		 *
471 		 * We now chown the ccache to the appropriate uid/gid
472 		 * combination, if its a FILE based ccache.
473 		 */
474 		if (!kmd->env || strstr(kmd->env, "FILE:")) {
475 			uid_t uuid;
476 			gid_t ugid;
477 			char *tmpname = NULL;
478 			char *filepath = NULL;
479 
480 			username = strdup(client_name);
481 			if (username == NULL) {
482 				__pam_log(LOG_AUTH | LOG_ERR,
483 				    "PAM-KRB5 (setcred): Out of memory");
484 				retval = KRB5KRB_ERR_GENERIC;
485 				goto error;
486 			}
487 			if ((tmpname = strchr(username, '@')))
488 				*tmpname = '\0';
489 
490 			if (get_pw_uid(username, &uuid) == 0 ||
491 			    get_pw_gid(username, &ugid) == 0) {
492 				__pam_log(LOG_AUTH | LOG_ERR,
493 				    "PAM-KRB5 (setcred): Unable to "
494 				    "find matching uid/gid pair for user `%s'",
495 				    username);
496 				retval = KRB5KRB_ERR_GENERIC;
497 				goto error;
498 			}
499 
500 			if (!kmd->env) {
501 				char buffer[512];
502 
503 				if (snprintf(buffer, sizeof (buffer),
504 				    "%s=FILE:/tmp/krb5cc_%d", KRB5_ENV_CCNAME,
505 				    (int)uuid) >= sizeof (buffer)) {
506 					retval = KRB5KRB_ERR_GENERIC;
507 					goto error;
508 				}
509 
510 				/*
511 				 * We MUST copy this to the heap for the putenv
512 				 * to work!
513 				 */
514 				kmd->env = strdup(buffer);
515 				if (!kmd->env) {
516 					retval = ENOMEM;
517 					goto error;
518 				} else {
519 					if (putenv(kmd->env)) {
520 						retval = ENOMEM;
521 						goto error;
522 					}
523 				}
524 			}
525 
526 			/*
527 			 * We know at this point that kmd->env must start
528 			 * with the literal string "FILE:".  Set filepath
529 			 * character string to point to ":"
530 			 */
531 
532 			filepath = strchr(kmd->env, ':');
533 
534 			/*
535 			 * Now check if first char after ":" is null char
536 			 */
537 			if (filepath[1] == '\0') {
538 				__pam_log(LOG_AUTH | LOG_ERR,
539 				    "PAM-KRB5 (setcred): Invalid pathname "
540 				    "for credential cache of user `%s'",
541 				    username);
542 				retval = KRB5KRB_ERR_GENERIC;
543 				goto error;
544 			}
545 			if (chown(filepath+1, uuid, ugid)) {
546 				if (kmd->debug)
547 					__pam_log(LOG_AUTH | LOG_DEBUG,
548 					    "PAM-KRB5 (setcred): chown to user "
549 					    "`%s' failed for FILE=%s",
550 					    username, filepath);
551 			}
552 		}
553 	}
554 
555 error:
556 	if (retval == 0) {
557 		krb5_timestamp endtime;
558 
559 		if (renewed_cred && renewed_cred->times.endtime != 0)
560 			endtime = renewed_cred->times.endtime;
561 		else
562 			endtime = my_creds.times.endtime;
563 
564 		if (kmd->debug)
565 			__pam_log(LOG_AUTH | LOG_DEBUG,
566 			    "PAM-KRB5 (setcred): delete/add warning");
567 
568 		if (kwarn_del_warning(client_name) != 0) {
569 			__pam_log(LOG_AUTH | LOG_NOTICE,
570 			    "PAM-KRB5 (setcred): kwarn_del_warning"
571 			    " failed: ktkt_warnd(1M) down?");
572 		}
573 
574 		if (kwarn_add_warning(client_name, endtime) != 0) {
575 			__pam_log(LOG_AUTH | LOG_NOTICE,
576 			    "PAM-KRB5 (setcred): kwarn_add_warning"
577 			    " failed: ktkt_warnd(1M) down?");
578 		}
579 	}
580 
581 	if (renewed_cred != NULL)
582 		krb5_free_creds(kmd->kcontext, renewed_cred);
583 
584 	if (client_name != NULL)
585 		free(client_name);
586 
587 	if (username)
588 		free(username);
589 
590 	krb5_free_cred_contents(kmd->kcontext, &creds);
591 
592 	return (retval);
593 }
594 
595 /*
596  * Delete the user's credentials for this session
597  */
598 static int
599 attempt_delete_initcred(krb5_module_data_t *kmd)
600 {
601 	if (kmd == NULL)
602 		return (PAM_SUCCESS);
603 
604 	if (kmd->debug) {
605 		__pam_log(LOG_AUTH | LOG_DEBUG,
606 		    "PAM-KRB5 (setcred): deleting user's "
607 		    "credentials (initcreds)");
608 	}
609 	krb5_free_cred_contents(kmd->kcontext, &kmd->initcreds);
610 	(void) memset((char *)&kmd->initcreds, 0, sizeof (krb5_creds));
611 	kmd->auth_status = PAM_AUTHINFO_UNAVAIL;
612 	return (PAM_SUCCESS);
613 }
614