xref: /freebsd/contrib/pam-krb5/module/password.c (revision bf6873c5786e333d679a7838d28812febf479a8a)
1 /*
2  * Kerberos password changing.
3  *
4  * Copyright 2005-2009, 2020 Russ Allbery <eagle@eyrie.org>
5  * Copyright 2011
6  *     The Board of Trustees of the Leland Stanford Junior University
7  * Copyright 2005 Andres Salomon <dilinger@debian.org>
8  * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
9  *
10  * SPDX-License-Identifier: BSD-3-clause or GPL-1+
11  */
12 
13 #include <config.h>
14 #include <portable/krb5.h>
15 #include <portable/pam.h>
16 #include <portable/system.h>
17 
18 #include <errno.h>
19 
20 #include <module/internal.h>
21 #include <pam-util/args.h>
22 #include <pam-util/logging.h>
23 
24 
25 /*
26  * Get the new password.  Store it in PAM_AUTHTOK if we obtain it and verify
27  * it successfully and return it in the pass parameter.  If pass is set to
28  * NULL, only store the new password in PAM_AUTHTOK.
29  *
30  * Returns a PAM error code, usually either PAM_AUTHTOK_ERR or PAM_SUCCESS.
31  */
32 int
pamk5_password_prompt(struct pam_args * args,char ** pass)33 pamk5_password_prompt(struct pam_args *args, char **pass)
34 {
35     int pamret = PAM_AUTHTOK_ERR;
36     char *pass1 = NULL;
37     char *pass2;
38     PAM_CONST void *tmp;
39 
40     /* Use the password from a previous module, if so configured. */
41     if (pass != NULL)
42         *pass = NULL;
43     if (args->config->use_authtok) {
44         pamret = pam_get_item(args->pamh, PAM_AUTHTOK, &tmp);
45         if (tmp == NULL) {
46             putil_debug_pam(args, pamret, "no stored password");
47             pamret = PAM_AUTHTOK_ERR;
48             goto done;
49         }
50         if (strlen(tmp) > PAM_MAX_RESP_SIZE - 1) {
51             putil_debug(args, "rejecting password longer than %d",
52                         PAM_MAX_RESP_SIZE - 1);
53             pamret = PAM_AUTHTOK_ERR;
54             goto done;
55         }
56         pass1 = strdup((const char *) tmp);
57     }
58 
59     /* Prompt for the new password if necessary. */
60     if (pass1 == NULL) {
61         pamret = pamk5_get_password(args, "Enter new", &pass1);
62         if (pamret != PAM_SUCCESS) {
63             putil_debug_pam(args, pamret, "error getting new password");
64             pamret = PAM_AUTHTOK_ERR;
65             goto done;
66         }
67         if (strlen(pass1) > PAM_MAX_RESP_SIZE - 1) {
68             putil_debug(args, "rejecting password longer than %d",
69                         PAM_MAX_RESP_SIZE - 1);
70             pamret = PAM_AUTHTOK_ERR;
71             explicit_bzero(pass1, strlen(pass1));
72             free(pass1);
73             goto done;
74         }
75         pamret = pamk5_get_password(args, "Retype new", &pass2);
76         if (pamret != PAM_SUCCESS) {
77             putil_debug_pam(args, pamret, "error getting new password");
78             pamret = PAM_AUTHTOK_ERR;
79             explicit_bzero(pass1, strlen(pass1));
80             free(pass1);
81             goto done;
82         }
83         if (strcmp(pass1, pass2) != 0) {
84             putil_debug(args, "new passwords don't match");
85             pamk5_conv(args, "Passwords don't match", PAM_ERROR_MSG, NULL);
86             explicit_bzero(pass1, strlen(pass1));
87             free(pass1);
88             explicit_bzero(pass2, strlen(pass2));
89             free(pass2);
90             pamret = PAM_AUTHTOK_ERR;
91             goto done;
92         }
93         explicit_bzero(pass2, strlen(pass2));
94         free(pass2);
95 
96         /* Save the new password for other modules. */
97         pamret = pam_set_item(args->pamh, PAM_AUTHTOK, pass1);
98         if (pamret != PAM_SUCCESS) {
99             putil_err_pam(args, pamret, "error storing password");
100             pamret = PAM_AUTHTOK_ERR;
101             explicit_bzero(pass1, strlen(pass1));
102             free(pass1);
103             goto done;
104         }
105     }
106     if (pass != NULL)
107         *pass = pass1;
108     else {
109         explicit_bzero(pass1, strlen(pass1));
110         free(pass1);
111     }
112 
113 done:
114     return pamret;
115 }
116 
117 
118 /*
119  * We've obtained credentials for the password changing interface and gotten
120  * the new password, so do the work of actually changing the password.
121  */
122 static int
change_password(struct pam_args * args,const char * pass)123 change_password(struct pam_args *args, const char *pass)
124 {
125     struct context *ctx;
126     int retval = PAM_SUCCESS;
127     int result_code;
128     krb5_data result_code_string, result_string;
129     const char *message;
130 
131     /* Sanity check. */
132     if (args == NULL || args->config == NULL || args->config->ctx == NULL
133         || args->config->ctx->creds == NULL)
134         return PAM_AUTHTOK_ERR;
135     ctx = args->config->ctx;
136 
137     /*
138      * The actual change.
139      *
140      * There are two password protocols in use: the change password protocol,
141      * which doesn't allow specification of the principal, and the newer set
142      * password protocol, which does.  For our purposes, either will do.
143      *
144      * Both Heimdal and MIT provide krb5_set_password.  With Heimdal,
145      * krb5_change_password is deprecated and krb5_set_password tries both
146      * protocols in turn, so will work with new and old servers.  With MIT,
147      * krb5_set_password will use the old protocol if the principal is NULL
148      * and the new protocol if it is not.
149      *
150      * We would like to just use krb5_set_password with a NULL principal
151      * argument, but Heimdal 1.5 uses the default principal for the local user
152      * rather than the principal from the credentials, so we need to pass in a
153      * principal for Heimdal.  So we're stuck with an #ifdef.
154      */
155 #ifdef HAVE_KRB5_MIT
156     retval =
157         krb5_set_password(ctx->context, ctx->creds, (char *) pass, NULL,
158                           &result_code, &result_code_string, &result_string);
159 #else
160     retval =
161         krb5_set_password(ctx->context, ctx->creds, (char *) pass, ctx->princ,
162                           &result_code, &result_code_string, &result_string);
163 #endif
164 
165     /* Everything from here on is just handling diagnostics and output. */
166     if (retval != 0) {
167         putil_debug_krb5(args, retval, "krb5_change_password failed");
168         message = krb5_get_error_message(ctx->context, retval);
169         pamk5_conv(args, message, PAM_ERROR_MSG, NULL);
170         krb5_free_error_message(ctx->context, message);
171         retval = PAM_AUTHTOK_ERR;
172         goto done;
173     }
174     if (result_code != 0) {
175         char *output;
176         int status;
177 
178         putil_debug(args, "krb5_change_password: %s",
179                     (char *) result_code_string.data);
180         retval = PAM_AUTHTOK_ERR;
181         status =
182             asprintf(&output, "%.*s%s%.*s", (int) result_code_string.length,
183                      (char *) result_code_string.data,
184                      result_string.length == 0 ? "" : ": ",
185                      (int) result_string.length, (char *) result_string.data);
186         if (status < 0)
187             putil_crit(args, "asprintf failed: %s", strerror(errno));
188         else {
189             pamk5_conv(args, output, PAM_ERROR_MSG, NULL);
190             free(output);
191         }
192     }
193     krb5_free_data_contents(ctx->context, &result_string);
194     krb5_free_data_contents(ctx->context, &result_code_string);
195 
196 done:
197     /*
198      * On failure, when clear_on_fail is set, we set the new password to NULL
199      * so that subsequent password change PAM modules configured with
200      * use_authtok will also fail.  Otherwise, since the order of the stack is
201      * fixed once the pre-check function runs, subsequent modules would
202      * continue even when we failed.
203      */
204     if (retval != PAM_SUCCESS && args->config->clear_on_fail) {
205         if (pam_set_item(args->pamh, PAM_AUTHTOK, NULL))
206             putil_err(args, "error clearing password");
207     }
208     return retval;
209 }
210 
211 
212 /*
213  * Change a user's password.  Returns a PAM status code for success or
214  * failure.  This does the work of pam_sm_chauthtok, but also needs to be
215  * called from pam_sm_authenticate if we're working around a library that
216  * can't handle password change during authentication.
217  *
218  * If the second argument is true, only do the authentication without actually
219  * doing the password change (PAM_PRELIM_CHECK).
220  */
221 int
pamk5_password_change(struct pam_args * args,bool only_auth)222 pamk5_password_change(struct pam_args *args, bool only_auth)
223 {
224     struct context *ctx = args->config->ctx;
225     int pamret = PAM_SUCCESS;
226     char *pass = NULL;
227 
228     /*
229      * Authenticate to the password changing service using the old password.
230      */
231     if (ctx->creds == NULL) {
232         pamret = pamk5_password_auth(args, "kadmin/changepw", &ctx->creds);
233         if (pamret == PAM_SERVICE_ERR || pamret == PAM_AUTH_ERR)
234             pamret = PAM_AUTHTOK_RECOVER_ERR;
235         if (pamret != PAM_SUCCESS)
236             goto done;
237     }
238 
239     /*
240      * Now, get the new password and change it unless we're just doing the
241      * first check.
242      */
243     if (only_auth)
244         goto done;
245     pamret = pamk5_password_prompt(args, &pass);
246     if (pamret != PAM_SUCCESS)
247         goto done;
248     pamret = change_password(args, pass);
249     if (pamret == PAM_SUCCESS)
250         pam_syslog(args->pamh, LOG_INFO, "user %s changed Kerberos password",
251                    ctx->name);
252 
253 done:
254     if (pass != NULL) {
255         explicit_bzero(pass, strlen(pass));
256         free(pass);
257     }
258     return pamret;
259 }
260 
261 
262 /*
263  * The function underlying the main PAM interface for password changing.
264  * Performs preliminary checks, user notification, and any reauthentication
265  * that's required.
266  *
267  * If the second argument is true, only do the authentication without actually
268  * doing the password change (PAM_PRELIM_CHECK).
269  */
270 int
pamk5_password(struct pam_args * args,bool only_auth)271 pamk5_password(struct pam_args *args, bool only_auth)
272 {
273     struct context *ctx = NULL;
274     int pamret, status;
275     PAM_CONST char *user;
276     char *pass = NULL;
277     bool set_context = false;
278 
279     /*
280      * Check whether we should ignore this user.
281      *
282      * If we do ignore this user, and we're not in the preliminary check
283      * phase, still prompt the user for the new password, but suppress our
284      * banner.  This is a little strange, but it allows another module to be
285      * stacked behind pam-krb5 with use_authtok and have it still work for
286      * ignored users.
287      *
288      * We ignore the return status when prompting for the new password in this
289      * case.  The worst thing that can happen is to fail to get the password,
290      * in which case the other module will fail (or might even not care).
291      */
292     if (args->config->ignore_root || args->config->minimum_uid > 0) {
293         status = pam_get_user(args->pamh, &user, NULL);
294         if (status == PAM_SUCCESS && pamk5_should_ignore(args, user)) {
295             if (!only_auth) {
296                 if (args->config->banner != NULL) {
297                     free(args->config->banner);
298                     args->config->banner = NULL;
299                 }
300                 pamk5_password_prompt(args, NULL);
301             }
302             pamret = PAM_IGNORE;
303             goto done;
304         }
305     }
306 
307     /*
308      * If we weren't able to find an existing context to use, we're going
309      * into this fresh and need to create a new context.
310      */
311     if (args->config->ctx == NULL) {
312         pamret = pamk5_context_new(args);
313         if (pamret != PAM_SUCCESS) {
314             putil_debug_pam(args, pamret, "creating context failed");
315             pamret = PAM_AUTHTOK_ERR;
316             goto done;
317         }
318         pamret = pam_set_data(args->pamh, "pam_krb5", args->config->ctx,
319                               pamk5_context_destroy);
320         if (pamret != PAM_SUCCESS) {
321             putil_err_pam(args, pamret, "cannot set context data");
322             pamret = PAM_AUTHTOK_ERR;
323             goto done;
324         }
325         set_context = true;
326     }
327     ctx = args->config->ctx;
328 
329     /*
330      * Tell the user what's going on if we're handling an expiration, but not
331      * if we were configured to use the same password as an earlier module in
332      * the stack.  The correct behavior here is not clear (what if the
333      * Kerberos password expired but the other one didn't?), but warning
334      * unconditionally leads to a strange message in the middle of doing the
335      * password change.
336      */
337     if (ctx->expired && ctx->creds == NULL)
338         if (!args->config->force_first_pass && !args->config->use_first_pass)
339             pamk5_conv(args, "Password expired.  You must change it now.",
340                        PAM_TEXT_INFO, NULL);
341 
342     /*
343      * Do the password change.  This may only get tickets if we're doing the
344      * preliminary check phase.
345      */
346     pamret = pamk5_password_change(args, only_auth);
347     if (only_auth)
348         goto done;
349 
350     /*
351      * If we were handling a forced password change for an expired password,
352      * now try to get a ticket cache with the new password.  If this succeeds,
353      * clear the expired flag in the context.
354      */
355     if (pamret == PAM_SUCCESS && ctx->expired) {
356         krb5_creds *creds = NULL;
357         char *principal;
358         krb5_error_code retval;
359 
360         putil_debug(args, "obtaining credentials with new password");
361         args->config->force_first_pass = 1;
362         pamret = pamk5_password_auth(args, NULL, &creds);
363         if (pamret != PAM_SUCCESS)
364             goto done;
365         retval = krb5_unparse_name(ctx->context, ctx->princ, &principal);
366         if (retval != 0) {
367             putil_err_krb5(args, retval, "krb5_unparse_name failed");
368             pam_syslog(args->pamh, LOG_INFO,
369                        "user %s authenticated as UNKNOWN", ctx->name);
370         } else {
371             pam_syslog(args->pamh, LOG_INFO, "user %s authenticated as %s",
372                        ctx->name, principal);
373             krb5_free_unparsed_name(ctx->context, principal);
374         }
375         ctx->expired = false;
376         pamret = pamk5_cache_init_random(args, creds);
377         krb5_free_cred_contents(ctx->context, creds);
378         free(creds);
379     }
380 
381 done:
382     if (pass != NULL) {
383         explicit_bzero(pass, strlen(pass));
384         free(pass);
385     }
386 
387     /*
388      * Don't free our Kerberos context if we set a context, since the context
389      * will take care of that.
390      */
391     if (set_context)
392         args->ctx = NULL;
393 
394     if (pamret != PAM_SUCCESS) {
395         if (pamret == PAM_SERVICE_ERR || pamret == PAM_AUTH_ERR)
396             pamret = PAM_AUTHTOK_ERR;
397         if (pamret == PAM_AUTHINFO_UNAVAIL)
398             pamret = PAM_AUTHTOK_ERR;
399     }
400     return pamret;
401 }
402