xref: /freebsd/contrib/pam-krb5/module/setcred.c (revision b670c9bafc0e31c7609969bf374b2e80bdc00211)
1 /*
2  * Ticket creation routines for pam-krb5.
3  *
4  * pam_setcred and pam_open_session need to do similar but not identical work
5  * to create the user's ticket cache.  The shared code is abstracted here into
6  * the pamk5_setcred function.
7  *
8  * Copyright 2005-2009, 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org>
9  * Copyright 2011
10  *     The Board of Trustees of the Leland Stanford Junior University
11  * Copyright 2005 Andres Salomon <dilinger@debian.org>
12  * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
13  *
14  * SPDX-License-Identifier: BSD-3-clause or GPL-1+
15  */
16 
17 #include <config.h>
18 #include <portable/krb5.h>
19 #include <portable/pam.h>
20 #include <portable/system.h>
21 
22 #include <assert.h>
23 #include <errno.h>
24 #include <pwd.h>
25 
26 #include <module/internal.h>
27 #include <pam-util/args.h>
28 #include <pam-util/logging.h>
29 
30 
31 /*
32  * Given a cache name and an existing cache, initialize a new cache, store the
33  * credentials from the existing cache in it, and return a pointer to the new
34  * cache in the cache argument.  Returns either PAM_SUCCESS or
35  * PAM_SERVICE_ERR.
36  */
37 static int
38 cache_init_from_cache(struct pam_args *args, const char *ccname,
39                       krb5_ccache old, krb5_ccache *cache)
40 {
41     struct context *ctx;
42     krb5_creds creds;
43     krb5_cc_cursor cursor;
44     int pamret;
45     krb5_error_code status;
46 
47     *cache = NULL;
48     memset(&creds, 0, sizeof(creds));
49     if (args == NULL || args->config == NULL || args->config->ctx == NULL
50         || args->config->ctx->context == NULL)
51         return PAM_SERVICE_ERR;
52     if (old == NULL)
53         return PAM_SERVICE_ERR;
54     ctx = args->config->ctx;
55     status = krb5_cc_start_seq_get(ctx->context, old, &cursor);
56     if (status != 0) {
57         putil_err_krb5(args, status, "cannot open new credentials");
58         return PAM_SERVICE_ERR;
59     }
60     status = krb5_cc_next_cred(ctx->context, old, &cursor, &creds);
61     if (status != 0) {
62         putil_err_krb5(args, status, "cannot read new credentials");
63         pamret = PAM_SERVICE_ERR;
64         goto done;
65     }
66     pamret = pamk5_cache_init(args, ccname, &creds, cache);
67     if (pamret != PAM_SUCCESS) {
68         krb5_free_cred_contents(ctx->context, &creds);
69         pamret = PAM_SERVICE_ERR;
70         goto done;
71     }
72     krb5_free_cred_contents(ctx->context, &creds);
73 
74     /*
75      * There probably won't be any additional credentials, but check for them
76      * and copy them just in case.
77      */
78     while (krb5_cc_next_cred(ctx->context, old, &cursor, &creds) == 0) {
79         status = krb5_cc_store_cred(ctx->context, *cache, &creds);
80         krb5_free_cred_contents(ctx->context, &creds);
81         if (status != 0) {
82             putil_err_krb5(args, status,
83                            "cannot store additional credentials"
84                            " in %s",
85                            ccname);
86             pamret = PAM_SERVICE_ERR;
87             goto done;
88         }
89     }
90     pamret = PAM_SUCCESS;
91 
92 done:
93     krb5_cc_end_seq_get(ctx->context, ctx->cache, &cursor);
94     if (pamret != PAM_SUCCESS && *cache != NULL) {
95         krb5_cc_destroy(ctx->context, *cache);
96         *cache = NULL;
97     }
98     return pamret;
99 }
100 
101 
102 /*
103  * Determine the name of a new ticket cache.  Handles ccache and ccache_dir
104  * PAM options and returns newly allocated memory.
105  *
106  * The ccache option, if set, contains a string with possible %u and %p
107  * escapes.  The former is replaced by the UID and the latter is replaced by
108  * the PID (a suitable unique string).
109  */
110 static char *
111 build_ccache_name(struct pam_args *args, uid_t uid)
112 {
113     char *cache_name = NULL;
114     int retval;
115 
116     if (args->config->ccache == NULL) {
117         retval = asprintf(&cache_name, "%s/krb5cc_%d_XXXXXX",
118                           args->config->ccache_dir, (int) uid);
119         if (retval < 0) {
120             putil_crit(args, "malloc failure: %s", strerror(errno));
121             return NULL;
122         }
123     } else {
124         size_t len = 0, delta;
125         char *p, *q;
126 
127         for (p = args->config->ccache; *p != '\0'; p++) {
128             if (p[0] == '%' && p[1] == 'u') {
129                 len += snprintf(NULL, 0, "%ld", (long) uid);
130                 p++;
131             } else if (p[0] == '%' && p[1] == 'p') {
132                 len += snprintf(NULL, 0, "%ld", (long) getpid());
133                 p++;
134             } else {
135                 len++;
136             }
137         }
138         len++;
139         cache_name = malloc(len);
140         if (cache_name == NULL) {
141             putil_crit(args, "malloc failure: %s", strerror(errno));
142             return NULL;
143         }
144         for (p = args->config->ccache, q = cache_name; *p != '\0'; p++) {
145             if (p[0] == '%' && p[1] == 'u') {
146                 delta = snprintf(q, len, "%ld", (long) uid);
147                 q += delta;
148                 len -= delta;
149                 p++;
150             } else if (p[0] == '%' && p[1] == 'p') {
151                 delta = snprintf(q, len, "%ld", (long) getpid());
152                 q += delta;
153                 len -= delta;
154                 p++;
155             } else {
156                 *q = *p;
157                 q++;
158                 len--;
159             }
160         }
161         *q = '\0';
162     }
163     return cache_name;
164 }
165 
166 
167 /*
168  * Create a new context for a session if we've lost the context created during
169  * authentication (such as when running under OpenSSH).  Return PAM_IGNORE if
170  * we're ignoring this user or if apparently our pam_authenticate never
171  * succeeded.
172  */
173 static int
174 create_session_context(struct pam_args *args)
175 {
176     struct context *ctx = NULL;
177     PAM_CONST char *user;
178     const char *tmpname;
179     int status, pamret;
180 
181     /* If we're going to ignore the user anyway, don't even bother. */
182     if (args->config->ignore_root || args->config->minimum_uid > 0) {
183         pamret = pam_get_user(args->pamh, &user, NULL);
184         if (pamret == PAM_SUCCESS && pamk5_should_ignore(args, user)) {
185             pamret = PAM_IGNORE;
186             goto fail;
187         }
188     }
189 
190     /*
191      * Create the context and locate the temporary ticket cache.  Load the
192      * ticket cache back into the context and flush out the other data that
193      * would have been set if we'd kept our original context.
194      */
195     pamret = pamk5_context_new(args);
196     if (pamret != PAM_SUCCESS) {
197         putil_crit_pam(args, pamret, "creating session context failed");
198         goto fail;
199     }
200     ctx = args->config->ctx;
201     tmpname = pamk5_get_krb5ccname(args, "PAM_KRB5CCNAME");
202     if (tmpname == NULL) {
203         putil_debug(args, "unable to get PAM_KRB5CCNAME, assuming"
204                           " non-Kerberos login");
205         pamret = PAM_IGNORE;
206         goto fail;
207     }
208     putil_debug(args, "found initial ticket cache at %s", tmpname);
209     status = krb5_cc_resolve(ctx->context, tmpname, &ctx->cache);
210     if (status != 0) {
211         putil_err_krb5(args, status, "cannot resolve cache %s", tmpname);
212         pamret = PAM_SERVICE_ERR;
213         goto fail;
214     }
215     status = krb5_cc_get_principal(ctx->context, ctx->cache, &ctx->princ);
216     if (status != 0) {
217         putil_err_krb5(args, status, "cannot retrieve principal");
218         pamret = PAM_SERVICE_ERR;
219         goto fail;
220     }
221 
222     /*
223      * We've rebuilt the context.  Push it back into the PAM state for any
224      * further calls to session or account management, which OpenSSH does keep
225      * the context for.
226      */
227     pamret = pam_set_data(args->pamh, "pam_krb5", ctx, pamk5_context_destroy);
228     if (pamret != PAM_SUCCESS) {
229         putil_err_pam(args, pamret, "cannot set context data");
230         goto fail;
231     }
232     return PAM_SUCCESS;
233 
234 fail:
235     pamk5_context_free(args);
236     return pamret;
237 }
238 
239 
240 /*
241  * Sets user credentials by creating the permanent ticket cache and setting
242  * the proper ownership.  This function may be called by either pam_sm_setcred
243  * or pam_sm_open_session.  The refresh flag should be set to true if we
244  * should reinitialize an existing ticket cache instead of creating a new one.
245  */
246 int
247 pamk5_setcred(struct pam_args *args, bool refresh)
248 {
249     struct context *ctx = NULL;
250     krb5_ccache cache = NULL;
251     char *cache_name = NULL;
252     bool set_context = false;
253     int status = 0;
254     int pamret;
255     struct passwd *pw = NULL;
256     uid_t uid;
257     gid_t gid;
258 
259     /* If configured not to create a cache, we have nothing to do. */
260     if (args->config->no_ccache) {
261         pamret = PAM_SUCCESS;
262         goto done;
263     }
264 
265     /*
266      * If we weren't able to obtain a context, we were probably run by OpenSSH
267      * with its weird PAM handling, so we're going to cobble up a new context
268      * for ourselves.
269      */
270     pamret = pamk5_context_fetch(args);
271     if (pamret != PAM_SUCCESS) {
272         putil_debug(args, "no context found, creating one");
273         pamret = create_session_context(args);
274         if (pamret != PAM_SUCCESS || args->config->ctx == NULL)
275             goto done;
276         set_context = true;
277     }
278     ctx = args->config->ctx;
279 
280     /*
281      * Some programs (xdm, for instance) appear to call setcred over and over
282      * again, so avoid doing useless work.
283      */
284     if (ctx->initialized) {
285         pamret = PAM_SUCCESS;
286         goto done;
287     }
288 
289     /*
290      * Get the uid.  The user is not required to be a local account for
291      * pam_authenticate, but for either pam_setcred (other than DELETE) or for
292      * pam_open_session, the user must be a local account.
293      */
294     pw = pam_modutil_getpwnam(args->pamh, ctx->name);
295     if (pw == NULL) {
296         putil_err(args, "getpwnam failed for %s", ctx->name);
297         pamret = PAM_USER_UNKNOWN;
298         goto done;
299     }
300     uid = pw->pw_uid;
301     gid = pw->pw_gid;
302 
303     /* Get the cache name.  If reinitializing, this is our existing cache. */
304     if (refresh) {
305         const char *name, *k5name;
306 
307         /*
308          * Solaris su calls pam_setcred as root with PAM_REINITIALIZE_CREDS,
309          * preserving the user-supplied environment.  An xlock program may
310          * also do this if it's setuid root and doesn't drop credentials
311          * before calling pam_setcred.
312          *
313          * There isn't any safe way of reinitializing the exiting ticket cache
314          * for the user if we're setuid without calling setreuid().  Calling
315          * setreuid() is possible, but if the calling application is threaded,
316          * it will change credentials for the whole application, with possibly
317          * bizarre and unintended (and insecure) results.  Trying to verify
318          * ownership of the existing ticket cache before using it fails under
319          * various race conditions (for example, having one of the elements of
320          * the path be a symlink and changing the target of that symlink
321          * between our check and the call to krb5_cc_resolve).  Without
322          * calling setreuid(), we run the risk of replacing a file owned by
323          * another user with a credential cache.
324          *
325          * We could fail with an error in the setuid case, which would be
326          * maximally safe, but it would prevent use of the module for
327          * authentication with programs such as Solaris su.  Failure to
328          * reinitialize the cache is normally not a serious problem, just a
329          * missing feature.  We therefore log an error and exit with
330          * PAM_SUCCESS for the setuid case.
331          *
332          * We do not use issetugid here since it always returns true if setuid
333          * was was involved anywhere in the process of running the binary.
334          * This would prevent a setuid screensaver that drops permissions from
335          * refreshing a credential cache.  The issetugid behavior is safer,
336          * since the environment should ideally not be trusted even if the
337          * binary completely changed users away from the original user, but in
338          * that case the binary needs to take some responsibility for either
339          * sanitizing the environment or being certain that the calling user
340          * is permitted to act as the target user.
341          */
342         if (getuid() != geteuid() || getgid() != getegid()) {
343             putil_err(args, "credential reinitialization in a setuid context"
344                             " ignored");
345             pamret = PAM_SUCCESS;
346             goto done;
347         }
348         name = pamk5_get_krb5ccname(args, "KRB5CCNAME");
349         if (name == NULL)
350             name = krb5_cc_default_name(ctx->context);
351         if (name == NULL) {
352             putil_err(args, "unable to get ticket cache name");
353             pamret = PAM_SERVICE_ERR;
354             goto done;
355         }
356         if (strncmp(name, "FILE:", strlen("FILE:")) == 0)
357             name += strlen("FILE:");
358 
359         /*
360          * If the cache we have in the context and the cache we're
361          * reinitializing are the same cache, don't do anything; otherwise,
362          * we'll end up destroying the cache.  This should never happen; this
363          * case triggering is a sign of a bug, probably in the calling
364          * application.
365          */
366         if (ctx->cache != NULL) {
367             k5name = krb5_cc_get_name(ctx->context, ctx->cache);
368             if (k5name != NULL) {
369                 if (strncmp(k5name, "FILE:", strlen("FILE:")) == 0)
370                     k5name += strlen("FILE:");
371                 if (strcmp(name, k5name) == 0) {
372                     pamret = PAM_SUCCESS;
373                     goto done;
374                 }
375             }
376         }
377 
378         cache_name = strdup(name);
379         if (cache_name == NULL) {
380             putil_crit(args, "malloc failure: %s", strerror(errno));
381             pamret = PAM_BUF_ERR;
382             goto done;
383         }
384         putil_debug(args, "refreshing ticket cache %s", cache_name);
385 
386         /*
387          * If we're refreshing the cache, we didn't really create it and the
388          * user's open session created by login is probably still managing
389          * it.  Thus, don't remove it when PAM is shut down.
390          */
391         ctx->dont_destroy_cache = 1;
392     } else {
393         char *cache_name_tmp;
394         size_t len;
395 
396         cache_name = build_ccache_name(args, uid);
397         if (cache_name == NULL) {
398             pamret = PAM_BUF_ERR;
399             goto done;
400         }
401         len = strlen(cache_name);
402         if (len > 6 && strncmp("XXXXXX", cache_name + len - 6, 6) == 0) {
403             if (strncmp(cache_name, "FILE:", strlen("FILE:")) == 0)
404                 cache_name_tmp = cache_name + strlen("FILE:");
405             else
406                 cache_name_tmp = cache_name;
407             pamret = pamk5_cache_mkstemp(args, cache_name_tmp);
408             if (pamret != PAM_SUCCESS)
409                 goto done;
410         }
411         putil_debug(args, "initializing ticket cache %s", cache_name);
412     }
413 
414     /*
415      * Initialize the new ticket cache and point the environment at it.  Only
416      * chown the cache if the cache is of type FILE or has no type (making the
417      * assumption that the default cache type is FILE; otherwise, due to the
418      * type prefix, we'd end up with an invalid path.
419      */
420     pamret = cache_init_from_cache(args, cache_name, ctx->cache, &cache);
421     if (pamret != PAM_SUCCESS)
422         goto done;
423     if (strncmp(cache_name, "FILE:", strlen("FILE:")) == 0)
424         status = chown(cache_name + strlen("FILE:"), uid, gid);
425     else if (strchr(cache_name, ':') == NULL)
426         status = chown(cache_name, uid, gid);
427     if (status == -1) {
428         putil_crit(args, "chown of ticket cache failed: %s", strerror(errno));
429         pamret = PAM_SERVICE_ERR;
430         goto done;
431     }
432     pamret = pamk5_set_krb5ccname(args, cache_name, "KRB5CCNAME");
433     if (pamret != PAM_SUCCESS) {
434         putil_crit(args, "setting KRB5CCNAME failed: %s", strerror(errno));
435         goto done;
436     }
437 
438     /*
439      * If we had a temporary ticket cache, delete the environment variable so
440      * that we won't get confused and think we still have a temporary ticket
441      * cache when called again.
442      *
443      * FreeBSD PAM, at least as of 7.2, doesn't support deleting environment
444      * variables using the syntax supported by Solaris and Linux.  Work
445      * around that by setting the variable to an empty value if deleting it
446      * fails.
447      */
448     if (pam_getenv(args->pamh, "PAM_KRB5CCNAME") != NULL) {
449         pamret = pam_putenv(args->pamh, "PAM_KRB5CCNAME");
450         if (pamret != PAM_SUCCESS)
451             pamret = pam_putenv(args->pamh, "PAM_KRB5CCNAME=");
452         if (pamret != PAM_SUCCESS)
453             goto done;
454     }
455 
456     /* Destroy the temporary cache and put the new cache in the context. */
457     krb5_cc_destroy(ctx->context, ctx->cache);
458     ctx->cache = cache;
459     cache = NULL;
460     ctx->initialized = 1;
461     if (args->config->retain_after_close)
462         ctx->dont_destroy_cache = 1;
463 
464 done:
465     if (ctx != NULL && cache != NULL)
466         krb5_cc_destroy(ctx->context, cache);
467     free(cache_name);
468 
469     /* If we stored our Kerberos context in PAM data, don't free it. */
470     if (set_context)
471         args->ctx = NULL;
472 
473     return pamret;
474 }
475