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