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