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