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 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 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 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 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