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
53static int
54fetch_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 */
164static int
165exp_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
273out:
274
275	if (kmd->kcontext) {
276		krb5_free_context(kmd->kcontext);
277		kmd->kcontext = NULL;
278	}
279
280exit:
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 */
301int
302pam_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
471out:
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