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  * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
23  * Use is subject to license terms.
24  */
25 
26 #include <kadm5/admin.h>
27 #include <krb5.h>
28 
29 #include <security/pam_appl.h>
30 #include <security/pam_modules.h>
31 #include <security/pam_impl.h>
32 #include <syslog.h>
33 #include <string.h>
34 #include <stdio.h>
35 #include <stdlib.h>
36 #include <sys/types.h>
37 #include <pwd.h>
38 #include <libintl.h>
39 #include <netdb.h>
40 #include "utils.h"
41 #include <shadow.h>
42 
43 #include "krb5_repository.h"
44 
45 #define	KRB5_AUTOMIGRATE_DATA	"SUNW-KRB5-AUTOMIGRATE-DATA"
46 
47 #define	min(a, b) ((a) < (b) ? (a) : (b))
48 
49 /*
50  * pam_sm_acct_mgmt	  main account managment routine.
51  */
52 
53 static int
fetch_princ_entry(krb5_module_data_t * kmd,char * princ_str,kadm5_principal_ent_rec * prent,int debug)54 fetch_princ_entry(
55 	krb5_module_data_t *kmd,
56 	char *princ_str,
57 	kadm5_principal_ent_rec *prent,	/* out */
58 	int debug)
59 
60 {
61 	kadm5_ret_t		code;
62 	krb5_principal 		princ = 0;
63 	char 			admin_realm[1024];
64 	char			kprinc[2*MAXHOSTNAMELEN];
65 	char			*cpw_service, *password;
66 	void 			*server_handle;
67 	krb5_context		context;
68 	kadm5_config_params	params;
69 
70 	password = kmd->password;
71 	context = kmd->kcontext;
72 
73 	if ((code = get_kmd_kuser(context, (const char *)princ_str,
74 	    kprinc, 2*MAXHOSTNAMELEN)) != 0) {
75 		return (code);
76 	}
77 
78 	code = krb5_parse_name(context, kprinc, &princ);
79 	if (code != 0) {
80 		return (PAM_SYSTEM_ERR);
81 	}
82 
83 	if (strlen(password) == 0) {
84 		krb5_free_principal(context, princ);
85 		if (debug)
86 			__pam_log(LOG_AUTH | LOG_DEBUG,
87 			    "PAM-KRB5 (acct): fetch_princ_entry: pwlen=0");
88 		return (PAM_AUTH_ERR);
89 	}
90 
91 	(void) strlcpy(admin_realm,
92 		    krb5_princ_realm(context, princ)->data,
93 		    sizeof (admin_realm));
94 
95 	(void) memset((char *)&params, 0, sizeof (params));
96 	params.mask |= KADM5_CONFIG_REALM;
97 	params.realm = admin_realm;
98 
99 	if (kadm5_get_cpw_host_srv_name(context, admin_realm, &cpw_service)) {
100 		__pam_log(LOG_AUTH | LOG_ERR,
101 			"PAM-KRB5 (acct):  unable to get host based "
102 			"service name for realm '%s'",
103 			admin_realm);
104 		krb5_free_principal(context, princ);
105 		return (PAM_SYSTEM_ERR);
106 	}
107 
108 	code = kadm5_init_with_password(kprinc, password, cpw_service,
109 					&params, KADM5_STRUCT_VERSION,
110 					KADM5_API_VERSION_2, NULL,
111 					&server_handle);
112 	if (code != 0) {
113 		if (debug)
114 			__pam_log(LOG_AUTH | LOG_DEBUG,
115 			    "PAM-KRB5 (acct): fetch_princ_entry: "
116 			    "init_with_pw failed: code = %d", code);
117 		krb5_free_principal(context, princ);
118 		return ((code == KADM5_BAD_PASSWORD) ?
119 			PAM_AUTH_ERR : PAM_SYSTEM_ERR);
120 	}
121 
122 	if (_kadm5_get_kpasswd_protocol(server_handle) != KRB5_CHGPWD_RPCSEC) {
123 		if (debug)
124 			__pam_log(LOG_AUTH | LOG_DEBUG,
125 			    "PAM-KRB5 (acct): fetch_princ_entry: "
126 			    "non-RPCSEC_GSS chpw server, can't get "
127 			    "princ entry");
128 		(void) kadm5_destroy(server_handle);
129 		krb5_free_principal(context, princ);
130 		return (PAM_SYSTEM_ERR);
131 	}
132 
133 	code = kadm5_get_principal(server_handle, princ, prent,
134 				KADM5_PRINCIPAL_NORMAL_MASK);
135 
136 	if (code != 0) {
137 		(void) kadm5_destroy(server_handle);
138 		krb5_free_principal(context, princ);
139 		return ((code == KADM5_UNK_PRINC) ?
140 			PAM_USER_UNKNOWN : PAM_SYSTEM_ERR);
141 	}
142 
143 	(void) kadm5_destroy(server_handle);
144 	krb5_free_principal(context, princ);
145 
146 	return (PAM_SUCCESS);
147 }
148 
149 /*
150  * exp_warn
151  *
152  * Warn the user if their pw is set to expire.
153  *
154  * We first check to see if the KDC had set any account or password
155  * expiration information in the key expiration field.  If this was
156  * not set then we must assume that the KDC could be broken and revert
157  * to fetching pw/account expiration information from kadm.  We can not
158  * determine the difference between broken KDCs that do not send key-exp
159  * vs. principals that do not have an expiration policy.  The up-shot
160  * is that pam_krb5 will probably not be stacked for acct mgmt if the
161  * environment does not have an exp policy, avoiding the second exchange
162  * using the kadm protocol.
163  */
164 static int
exp_warn(pam_handle_t * pamh,char * user,krb5_module_data_t * kmd,int debug)165 exp_warn(
166 	pam_handle_t *pamh,
167 	char *user,
168 	krb5_module_data_t *kmd,
169 	int debug)
170 
171 {
172 	int err;
173 	kadm5_principal_ent_rec prent;
174 	krb5_timestamp  now, days, expiration;
175 	char    messages[PAM_MAX_NUM_MSG][PAM_MAX_MSG_SIZE], *password;
176 	krb5_error_code code;
177 
178 	if (debug)
179 		__pam_log(LOG_AUTH | LOG_DEBUG,
180 		    "PAM-KRB5 (acct): exp_warn start: user = '%s'",
181 		    user ? user : "<null>");
182 
183 	password = kmd->password;
184 
185 	if (!pamh || !user || !password) {
186 		err = PAM_SERVICE_ERR;
187 		goto exit;
188 	}
189 
190 	/*
191 	 * If we error out from krb5_init_secure_context, then just set error
192 	 * code, check to see about debug message and exit out of routine as the
193 	 * context could not possibly have been setup.
194 	 */
195 
196 	if (code = krb5_init_secure_context(&kmd->kcontext)) {
197 		err = PAM_SYSTEM_ERR;
198 		if (debug)
199 			__pam_log(LOG_AUTH | LOG_ERR, "PAM-KRB5 (acct): "
200 			    "krb5_init_secure_context failed: code=%d",
201 			    code);
202 		goto exit;
203 	}
204 	if (code = krb5_timeofday(kmd->kcontext, &now)) {
205 		err = PAM_SYSTEM_ERR;
206 		if (debug)
207 			__pam_log(LOG_AUTH | LOG_ERR,
208 			    "PAM-KRB5 (acct): krb5_timeofday failed: code=%d",
209 			    code);
210 		goto out;
211 	}
212 
213 	if (kmd->expiration != 0) {
214 		expiration = kmd->expiration;
215 	} else {
216 		(void) memset(&prent, 0, sizeof (prent));
217 		if ((err = fetch_princ_entry(kmd, user, &prent, debug))
218 		    != PAM_SUCCESS) {
219 			if (debug)
220 				__pam_log(LOG_AUTH | LOG_DEBUG,
221 				"PAM-KRB5 (acct): exp_warn: fetch_pr failed %d",
222 				err);
223 			goto out;
224 		}
225 		if (prent.princ_expire_time != 0 && prent.pw_expiration != 0)
226 			expiration = min(prent.princ_expire_time,
227 				prent.pw_expiration);
228 		else
229 			expiration = prent.princ_expire_time ?
230 				prent.princ_expire_time : prent.pw_expiration;
231 	}
232 
233 	if (debug)
234 		__pam_log(LOG_AUTH | LOG_DEBUG,
235 		    "PAM-KRB5 (acct): exp_warn: "
236 		    "princ/pw_exp exp=%ld, now =%ld, days=%ld",
237 		    expiration,
238 		    now,
239 		    expiration > 0
240 		    ? ((expiration - now) / DAY)
241 		    : 0);
242 
243 	/* warn user if principal's pw is set to expire */
244 	if (expiration > 0) {
245 		days = (expiration - now) / DAY;
246 		if (days <= 0)
247 			(void) snprintf(messages[0],
248 				sizeof (messages[0]),
249 				dgettext(TEXT_DOMAIN,
250 				"Your Kerberos account/password will expire "
251 				"within 24 hours.\n"));
252 		else if (days == 1)
253 			(void) snprintf(messages[0],
254 				sizeof (messages[0]),
255 				dgettext(TEXT_DOMAIN,
256 				"Your Kerberos account/password will expire "
257 				"in 1 day.\n"));
258 		else
259 			(void) snprintf(messages[0],
260 				sizeof (messages[0]),
261 				dgettext(TEXT_DOMAIN,
262 				"Your Kerberos account/password will expire in "
263 				"%d days.\n"),
264 				(int)days);
265 
266 		(void) __pam_display_msg(pamh, PAM_TEXT_INFO, 1,
267 					messages, NULL);
268 	}
269 
270 	/* things went smooth */
271 	err = PAM_SUCCESS;
272 
273 out:
274 
275 	if (kmd->kcontext) {
276 		krb5_free_context(kmd->kcontext);
277 		kmd->kcontext = NULL;
278 	}
279 
280 exit:
281 
282 	if (debug)
283 		__pam_log(LOG_AUTH | LOG_DEBUG,
284 		    "PAM-KRB5 (acct): exp_warn end: err = %d", err);
285 
286 	return (err);
287 }
288 
289 /*
290  * pam_krb5 acct_mgmt
291  *
292  * we do
293  *    - check if pw expired (flag set in auth)
294  *    - warn user if pw is set to expire
295  *
296  * notes
297  *    - we require the auth module to have already run (sets module data)
298  *    - we don't worry about an expired princ cuz if that's the case,
299  *      auth would have failed
300  */
301 int
pam_sm_acct_mgmt(pam_handle_t * pamh,int flags,int argc,const char ** argv)302 pam_sm_acct_mgmt(
303 	pam_handle_t *pamh,
304 	int	flags,
305 	int	argc,
306 	const char **argv)
307 
308 {
309 	char *user = NULL;
310 	char *userdata = NULL;
311 	int err;
312 	int i;
313 	krb5_module_data_t *kmd = NULL;
314 	char    messages[PAM_MAX_NUM_MSG][PAM_MAX_MSG_SIZE];
315 	int debug = 0;  /* pam.conf entry option */
316 	int nowarn = 0; /* pam.conf entry option, no expire warnings */
317 	pam_repository_t	*rep_data = NULL;
318 
319 	for (i = 0; i < argc; i++) {
320 		if (strcasecmp(argv[i], "debug") == 0)
321 			debug = 1;
322 		else if (strcasecmp(argv[i], "nowarn") == 0) {
323 			nowarn = 1;
324 			flags = flags | PAM_SILENT;
325 		} else {
326 			__pam_log(LOG_AUTH | LOG_ERR,
327 			    "PAM-KRB5 (acct): illegal option %s",
328 			    argv[i]);
329 		}
330 	}
331 
332 	if (debug)
333 		__pam_log(LOG_AUTH | LOG_DEBUG,
334 		    "PAM-KRB5 (acct): debug=%d, nowarn=%d",
335 		    debug, nowarn);
336 
337 	(void) pam_get_item(pamh, PAM_REPOSITORY, (void **)&rep_data);
338 
339 	if (rep_data != NULL) {
340 		/*
341 		 * If the repository is not ours,
342 		 * return PAM_IGNORE.
343 		 */
344 		if (strcmp(rep_data->type, KRB5_REPOSITORY_NAME) != 0) {
345 			if (debug)
346 				__pam_log(LOG_AUTH | LOG_DEBUG,
347 					"PAM-KRB5 (acct): wrong"
348 					"repository found (%s), returning "
349 					"PAM_IGNORE", rep_data->type);
350 			return (PAM_IGNORE);
351 		}
352 	}
353 
354 
355 	/* get user name */
356 	(void) pam_get_item(pamh, PAM_USER, (void **) &user);
357 
358 	if (user == NULL || *user == '\0') {
359 		err = PAM_USER_UNKNOWN;
360 		goto out;
361 	}
362 
363 	/* get pam_krb5_migrate specific data */
364 	err = pam_get_data(pamh, KRB5_AUTOMIGRATE_DATA,
365 					(const void **)&userdata);
366 	if (err != PAM_SUCCESS) {
367 		if (debug)
368 			__pam_log(LOG_AUTH | LOG_DEBUG, "PAM-KRB5 (acct): "
369 				"no module data for KRB5_AUTOMIGRATE_DATA");
370 	} else {
371 		/*
372 		 * We try and reauthenticate, since this user has a
373 		 * newly created krb5 principal via the pam_krb5_migrate
374 		 * auth module. That way, this new user will have fresh
375 		 * creds (assuming pam_sm_authenticate() succeeds).
376 		 */
377 		if (strcmp(user, userdata) == 0)
378 			(void) pam_sm_authenticate(pamh, flags, argc,
379 					(const char **)argv);
380 		else
381 			if (debug)
382 				__pam_log(LOG_AUTH | LOG_DEBUG,
383 				"PAM-KRB5 (acct): PAM_USER %s"
384 				"does not match user %s from pam_get_data()",
385 				user, (char *)userdata);
386 	}
387 
388 	/* get krb5 module data  */
389 	if ((err = pam_get_data(pamh, KRB5_DATA, (const void **)&kmd))
390 	    != PAM_SUCCESS) {
391 		if (err == PAM_NO_MODULE_DATA) {
392 			/*
393 			 * pam_auth never called (possible config
394 			 * error; no pam_krb5 auth entry in pam.conf),
395 			 */
396 			if (debug) {
397 				__pam_log(LOG_AUTH | LOG_DEBUG,
398 				    "PAM-KRB5 (acct): no module data");
399 			}
400 			err = PAM_IGNORE;
401 			goto out;
402 		} else {
403 			__pam_log(LOG_AUTH | LOG_ERR,
404 				    "PAM-KRB5 (acct): get module"
405 				    " data failed: err=%d",
406 			    err);
407 		}
408 		goto out;
409 	}
410 
411 	debug = debug || kmd->debug;
412 
413 	/*
414 	 * auth mod set status to ignore, most likely cuz root key is
415 	 * in keytab, so skip other checks and return ignore
416 	 */
417 	if (kmd->auth_status == PAM_IGNORE) {
418 		if (debug)
419 			__pam_log(LOG_AUTH | LOG_DEBUG,
420 			    "PAM-KRB5 (acct): kmd auth_status is IGNORE");
421 		err = PAM_IGNORE;
422 		goto out;
423 	}
424 
425 	/*
426 	 * If there is no Kerberos related user and there is authentication
427 	 * data, this means that while the user has successfully passed
428 	 * authentication, Kerberos is not the account authority because there
429 	 * is no valid Kerberos principal.  PAM_IGNORE is returned since
430 	 * Kerberos is not authoritative for this user.  Other modules in the
431 	 * account stack will need to determine the success or failure for this
432 	 * user.
433 	 */
434 	if (kmd->auth_status == PAM_USER_UNKNOWN) {
435 		if (debug)
436 			syslog(LOG_DEBUG,
437 			    "PAM-KRB5 (acct): kmd auth_status is USER UNKNOWN");
438 		err = PAM_IGNORE;
439 		goto out;
440 	}
441 
442 	/*
443 	 * age_status will be set to PAM_NEW_AUTHTOK_REQD in pam_krb5's
444 	 * 'auth' if the user's key/pw has expired and needs to be changed
445 	 */
446 	if (kmd->age_status == PAM_NEW_AUTHTOK_REQD) {
447 		if (!nowarn) {
448 			(void) snprintf(messages[0], sizeof (messages[0]),
449 				dgettext(TEXT_DOMAIN,
450 				"Your Kerberos password has expired.\n"));
451 			(void) __pam_display_msg(pamh, PAM_TEXT_INFO,
452 					1, messages, NULL);
453 		}
454 		err = PAM_NEW_AUTHTOK_REQD;
455 		goto out;
456 	}
457 
458 	if (kmd->auth_status == PAM_SUCCESS && !(flags & PAM_SILENT) &&
459 	    !nowarn && kmd->password) {
460 		/* if we fail, let it slide, it's only a warning brah */
461 		(void) exp_warn(pamh, user, kmd, debug);
462 	}
463 
464 	/*
465 	 * If Kerberos is treated as optional in the PAM stack, it is possible
466 	 * that there is a KRB5_DATA item and a non-Kerberos account authority.
467 	 * In that case, PAM_IGNORE is returned.
468 	 */
469 	err = kmd->auth_status != PAM_SUCCESS ? PAM_IGNORE : kmd->auth_status;
470 
471 out:
472 	if (debug)
473 		__pam_log(LOG_AUTH | LOG_DEBUG,
474 		    "PAM-KRB5 (acct): end: %s", pam_strerror(pamh, err));
475 
476 	return (err);
477 }
478