xref: /freebsd/contrib/pam-krb5/module/prompting.c (revision bf6873c5786e333d679a7838d28812febf479a8a)
1 /*
2  * Prompt users for information.
3  *
4  * Handles all interaction with the PAM conversation, either directly or
5  * indirectly through the Kerberos libraries.
6  *
7  * Copyright 2005-2007, 2009, 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org>
8  * Copyright 2011-2012
9  *     The Board of Trustees of the Leland Stanford Junior University
10  * Copyright 2005 Andres Salomon <dilinger@debian.org>
11  * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
12  *
13  * SPDX-License-Identifier: BSD-3-clause or GPL-1+
14  */
15 
16 #include <config.h>
17 #include <portable/krb5.h>
18 #include <portable/pam.h>
19 #include <portable/system.h>
20 
21 #include <assert.h>
22 #include <errno.h>
23 
24 #include <module/internal.h>
25 #include <pam-util/args.h>
26 #include <pam-util/logging.h>
27 
28 
29 /*
30  * Build a password prompt.
31  *
32  * The default prompt is simply "Password:".  Optionally, a string describing
33  * the type of password is passed in as prefix.  In this case, the prompts is:
34  *
35  *     <prefix> <banner> password:
36  *
37  * where <prefix> is the argument passed and <banner> is the value of
38  * args->banner (defaulting to "Kerberos").
39  *
40  * If args->config->expose_account is set, we append the principal name (taken
41  * from args->config->ctx->princ) before the colon, so the prompts are:
42  *
43  *     Password for <principal>:
44  *     <prefix> <banner> password for <principal>:
45  *
46  * Normally this is not done because it exposes the realm and possibly any
47  * username to principal mappings, plus may confuse some ssh clients if sshd
48  * passes the prompt back to the client.
49  *
50  * Returns newly-allocated memory or NULL on failure.  The caller is
51  * responsible for freeing.
52  */
53 static char *
build_password_prompt(struct pam_args * args,const char * prefix)54 build_password_prompt(struct pam_args *args, const char *prefix)
55 {
56     struct context *ctx = args->config->ctx;
57     char *principal = NULL;
58     const char *banner, *bspace;
59     char *prompt, *tmp;
60     bool expose_account;
61     krb5_error_code k5_errno;
62     int retval;
63 
64     /* If we're exposing the account, format the principal name. */
65     if (args->config->expose_account || prefix != NULL)
66         if (ctx != NULL && ctx->context != NULL && ctx->princ != NULL) {
67             k5_errno = krb5_unparse_name(ctx->context, ctx->princ, &principal);
68             if (k5_errno != 0)
69                 putil_debug_krb5(args, k5_errno, "krb5_unparse_name failed");
70         }
71 
72     /* Build the part of the prompt without the principal name. */
73     if (prefix == NULL)
74         tmp = strdup("Password");
75     else {
76         banner = (args->config->banner == NULL) ? "" : args->config->banner;
77         bspace = (args->config->banner == NULL) ? "" : " ";
78         retval = asprintf(&tmp, "%s%s%s password", prefix, bspace, banner);
79         if (retval < 0)
80             tmp = NULL;
81     }
82     if (tmp == NULL)
83         goto fail;
84 
85     /* Add the principal, if desired, and the colon and space. */
86     expose_account = args->config->expose_account && principal != NULL;
87     if (expose_account)
88         retval = asprintf(&prompt, "%s for %s: ", tmp, principal);
89     else
90         retval = asprintf(&prompt, "%s: ", tmp);
91     free(tmp);
92     if (retval < 0)
93         goto fail;
94 
95     /* Clean up and return. */
96     if (principal != NULL)
97         krb5_free_unparsed_name(ctx->context, principal);
98     return prompt;
99 
100 fail:
101     if (principal != NULL)
102         krb5_free_unparsed_name(ctx->context, principal);
103     return NULL;
104 }
105 
106 
107 /*
108  * Prompt for a password.
109  *
110  * The entered password is stored in password.  The memory is allocated by the
111  * application and returned as part of the PAM conversation.  It must be freed
112  * by the caller.
113  *
114  * Returns a PAM success or error code.
115  */
116 int
pamk5_get_password(struct pam_args * args,const char * prefix,char ** password)117 pamk5_get_password(struct pam_args *args, const char *prefix, char **password)
118 {
119     char *prompt;
120     int retval;
121 
122     prompt = build_password_prompt(args, prefix);
123     if (prompt == NULL)
124         return PAM_BUF_ERR;
125     retval = pamk5_conv(args, prompt, PAM_PROMPT_ECHO_OFF, password);
126     free(prompt);
127     return retval;
128 }
129 
130 
131 /*
132  * Get information from the user or display a message to the user, as
133  * determined by type.  If PAM_SILENT was given, don't pass any text or error
134  * messages to the application.
135  *
136  * The response variable is set to the response returned by the conversation
137  * function on a successful return if a response was desired.  Caller is
138  * responsible for freeing it.
139  */
140 int
pamk5_conv(struct pam_args * args,const char * message,int type,char ** response)141 pamk5_conv(struct pam_args *args, const char *message, int type,
142            char **response)
143 {
144     int pamret;
145     struct pam_message msg;
146     PAM_CONST struct pam_message *pmsg;
147     struct pam_response *resp = NULL;
148     struct pam_conv *conv;
149     int want_reply;
150 
151     if (args->silent && (type == PAM_ERROR_MSG || type == PAM_TEXT_INFO))
152         return PAM_SUCCESS;
153     pamret = pam_get_item(args->pamh, PAM_CONV, (PAM_CONST void **) &conv);
154     if (pamret != PAM_SUCCESS)
155         return pamret;
156     if (conv->conv == NULL)
157         return PAM_CONV_ERR;
158     pmsg = &msg;
159     msg.msg_style = type;
160     msg.msg = (PAM_CONST char *) message;
161     pamret = conv->conv(1, &pmsg, &resp, conv->appdata_ptr);
162     if (pamret != PAM_SUCCESS)
163         return pamret;
164 
165     /*
166      * Only expect a response for PAM_PROMPT_ECHO_OFF or PAM_PROMPT_ECHO_ON
167      * message types.  This mildly annoying logic makes sure that everything
168      * is freed properly (except the response itself, if wanted, which is
169      * returned for the caller to free) and that the success status is set
170      * based on whether the reply matched our expectations.
171      *
172      * If we got a reply even though we didn't want one, still overwrite the
173      * reply before freeing in case it was a password.
174      */
175     want_reply = (type == PAM_PROMPT_ECHO_OFF || type == PAM_PROMPT_ECHO_ON);
176     if (resp == NULL || resp->resp == NULL)
177         pamret = want_reply ? PAM_CONV_ERR : PAM_SUCCESS;
178     else if (want_reply && response != NULL) {
179         *response = resp->resp;
180         pamret = PAM_SUCCESS;
181     } else {
182         explicit_bzero(resp->resp, strlen(resp->resp));
183         free(resp->resp);
184         pamret = want_reply ? PAM_SUCCESS : PAM_CONV_ERR;
185     }
186     free(resp);
187     return pamret;
188 }
189 
190 
191 /*
192  * Allocate memory to copy all of the prompts into a pam_message.
193  *
194  * Linux PAM and Solaris PAM expect different things here.  Solaris PAM
195  * expects to receive a pointer to a pointer to an array of pam_message
196  * structs.  Linux PAM expects to receive a pointer to an array of pointers to
197  * pam_message structs.  In order for the module to work with either PAM
198  * implementation, we need to set up a structure that is valid either way you
199  * look at it.
200  *
201  * We do this by making msg point to the array of struct pam_message pointers
202  * (what Linux PAM expects), and then make the first one of those pointers
203  * point to the array of pam_message structs.  Solaris will then be happy,
204  * looking at only the first element of the outer array and finding it
205  * pointing to the inner array.  Then, for Linux, we point the other elements
206  * of the outer array to the storage allocated in the inner array.
207  *
208  * All this also means we have to be careful how we free the resulting
209  * structure since it's double-linked in a subtle way.  Thankfully, we get to
210  * free it ourselves.
211  */
212 static struct pam_message **
allocate_pam_message(size_t total_prompts)213 allocate_pam_message(size_t total_prompts)
214 {
215     struct pam_message **msg;
216     size_t i;
217 
218     msg = calloc(total_prompts, sizeof(struct pam_message *));
219     if (msg == NULL)
220         return NULL;
221     *msg = calloc(total_prompts, sizeof(struct pam_message));
222     if (*msg == NULL) {
223         free(msg);
224         return NULL;
225     }
226     for (i = 1; i < total_prompts; i++)
227         msg[i] = msg[0] + i;
228     return msg;
229 }
230 
231 
232 /*
233  * Free the structure created by allocate_pam_message.
234  */
235 static void
free_pam_message(struct pam_message ** msg,size_t total_prompts)236 free_pam_message(struct pam_message **msg, size_t total_prompts)
237 {
238     size_t i;
239 
240     for (i = 0; i < total_prompts; i++)
241         free((char *) msg[i]->msg);
242     free(*msg);
243     free(msg);
244 }
245 
246 
247 /*
248  * Free the responses returned by the conversation function.  These may
249  * contain passwords, so we overwrite them before we free them.
250  */
251 static void
free_pam_responses(struct pam_response * resp,size_t total_prompts)252 free_pam_responses(struct pam_response *resp, size_t total_prompts)
253 {
254     size_t i;
255 
256     if (resp == NULL)
257         return;
258     for (i = 0; i < total_prompts; i++) {
259         if (resp[i].resp != NULL) {
260             explicit_bzero(resp[i].resp, strlen(resp[i].resp));
261             free(resp[i].resp);
262         }
263     }
264     free(resp);
265 }
266 
267 
268 /*
269  * Format a Kerberos prompt into a PAM prompt.  Takes a krb5_prompt as input
270  * and writes the resulting PAM prompt into a struct pam_message.
271  */
272 static krb5_error_code
format_prompt(krb5_prompt * prompt,struct pam_message * message)273 format_prompt(krb5_prompt *prompt, struct pam_message *message)
274 {
275     size_t len = strlen(prompt->prompt);
276     bool has_colon;
277     const char *colon;
278     int retval, style;
279 
280     /*
281      * Heimdal adds the trailing colon and space, while MIT does not.
282      * Work around the difference by looking to see if there's a trailing
283      * colon and space already and only adding it if there is not.
284      */
285     has_colon = (len > 2 && memcmp(&prompt->prompt[len - 2], ": ", 2) == 0);
286     colon = has_colon ? "" : ": ";
287     retval = asprintf((char **) &message->msg, "%s%s", prompt->prompt, colon);
288     if (retval < 0)
289         return retval;
290     style = prompt->hidden ? PAM_PROMPT_ECHO_OFF : PAM_PROMPT_ECHO_ON;
291     message->msg_style = style;
292     return 0;
293 }
294 
295 
296 /*
297  * Given an array of struct pam_response elements, record the responses in the
298  * corresponding krb5_prompt structures.
299  */
300 static krb5_error_code
record_prompt_answers(struct pam_response * resp,int num_prompts,krb5_prompt * prompts)301 record_prompt_answers(struct pam_response *resp, int num_prompts,
302                       krb5_prompt *prompts)
303 {
304     int i;
305 
306     for (i = 0; i < num_prompts; i++) {
307         size_t len, allowed;
308 
309         if (resp[i].resp == NULL)
310             return KRB5_LIBOS_CANTREADPWD;
311         len = strlen(resp[i].resp);
312         allowed = prompts[i].reply->length;
313         if (allowed == 0 || len > allowed - 1)
314             return KRB5_LIBOS_CANTREADPWD;
315 
316         /*
317          * Since the first version of this module, it has copied a nul
318          * character into the prompt data buffer for MIT Kerberos with the
319          * note that "other applications expect it to be there."  I suspect
320          * this is incorrect and nothing cares about this nul, but have
321          * preserved this behavior out of an abundance of caution.
322          *
323          * Note that it shortens the maximum response length we're willing to
324          * accept by one (implemented above) and is the source of one prior
325          * security vulnerability.
326          */
327         memcpy(prompts[i].reply->data, resp[i].resp, len + 1);
328         prompts[i].reply->length = (unsigned int) len;
329     }
330     return 0;
331 }
332 
333 
334 /*
335  * This is the generic prompting function called by both MIT Kerberos and
336  * Heimdal prompting implementations.
337  *
338  * There are a lot of structures and different layers of code at work here,
339  * making this code quite confusing.  This function is a prompter function to
340  * pass into the Kerberos library, in particular krb5_get_init_creds_password.
341  * It is used by the Kerberos library to prompt for a password if need be, and
342  * also to prompt for password changes if the password was expired.
343  *
344  * The purpose of this function is to serve as glue between the Kerberos
345  * library and the application (by way of the PAM glue).  PAM expects us to
346  * pass back to the conversation function an array of prompts and receive from
347  * the application an array of responses to those prompts.  We pass the
348  * application an array of struct pam_message pointers, and the application
349  * passes us an array of struct pam_response pointers.
350  *
351  * Kerberos, meanwhile, passes us in an array of krb5_prompt structs.  This
352  * struct contains the prompt, a flag saying whether to suppress echoing of
353  * what the user types for that prompt, and a buffer into which to store the
354  * response.
355  *
356  * Therefore, what we're doing here is copying the prompts from the
357  * krb5_prompt structs into pam_message structs, calling the conversation
358  * function, and then copying the responses back out of pam_response structs
359  * into the krb5_prompt structs to return to the Kerberos library.
360  */
361 krb5_error_code
pamk5_prompter_krb5(krb5_context context UNUSED,void * data,const char * name,const char * banner,int num_prompts,krb5_prompt * prompts)362 pamk5_prompter_krb5(krb5_context context UNUSED, void *data, const char *name,
363                     const char *banner, int num_prompts, krb5_prompt *prompts)
364 {
365     struct pam_args *args = data;
366     int current_prompt, retval, pamret, i, offset;
367     int total_prompts = num_prompts;
368     struct pam_message **msg;
369     struct pam_response *resp = NULL;
370     struct pam_conv *conv;
371 
372     /* Treat the name and banner as prompts that doesn't need input. */
373     if (name != NULL && !args->silent)
374         total_prompts++;
375     if (banner != NULL && !args->silent)
376         total_prompts++;
377 
378     /* If we have zero prompts, do nothing, silently. */
379     if (total_prompts == 0)
380         return 0;
381 
382     /* Obtain the conversation function from the application. */
383     pamret = pam_get_item(args->pamh, PAM_CONV, (PAM_CONST void **) &conv);
384     if (pamret != 0)
385         return KRB5_LIBOS_CANTREADPWD;
386     if (conv->conv == NULL)
387         return KRB5_LIBOS_CANTREADPWD;
388 
389     /* Allocate memory to copy all of the prompts into a pam_message. */
390     msg = allocate_pam_message(total_prompts);
391     if (msg == NULL)
392         return ENOMEM;
393 
394     /* current_prompt is an index into msg and a count when we're done. */
395     current_prompt = 0;
396     if (name != NULL && !args->silent) {
397         msg[current_prompt]->msg = strdup(name);
398         if (msg[current_prompt]->msg == NULL) {
399             retval = ENOMEM;
400             goto cleanup;
401         }
402         msg[current_prompt]->msg_style = PAM_TEXT_INFO;
403         current_prompt++;
404     }
405     if (banner != NULL && !args->silent) {
406         assert(current_prompt < total_prompts);
407         msg[current_prompt]->msg = strdup(banner);
408         if (msg[current_prompt]->msg == NULL) {
409             retval = ENOMEM;
410             goto cleanup;
411         }
412         msg[current_prompt]->msg_style = PAM_TEXT_INFO;
413         current_prompt++;
414     }
415     for (i = 0; i < num_prompts; i++) {
416         assert(current_prompt < total_prompts);
417         retval = format_prompt(&prompts[i], msg[current_prompt]);
418         if (retval < 0)
419             goto cleanup;
420         current_prompt++;
421     }
422 
423     /* Call into the application conversation function. */
424     pamret = conv->conv(total_prompts, (PAM_CONST struct pam_message **) msg,
425                         &resp, conv->appdata_ptr);
426     if (pamret != 0 || resp == NULL) {
427         retval = KRB5_LIBOS_CANTREADPWD;
428         goto cleanup;
429     }
430 
431     /*
432      * Record the answers in the Kerberos data structure.  If name or banner
433      * were provided, skip over the initial PAM responses that correspond to
434      * those messages.
435      */
436     offset = 0;
437     if (name != NULL && !args->silent)
438         offset++;
439     if (banner != NULL && !args->silent)
440         offset++;
441     retval = record_prompt_answers(resp + offset, num_prompts, prompts);
442 
443 cleanup:
444     free_pam_message(msg, total_prompts);
445     free_pam_responses(resp, total_prompts);
446     return retval;
447 }
448 
449 
450 /*
451  * This is a special version of krb5_prompter_krb5 that returns an error if
452  * the Kerberos library asks for a password.  It is only used with MIT
453  * Kerberos as part of the implementation of try_pkinit and use_pkinit.
454  * (Heimdal has a different API for PKINIT authentication.)
455  */
456 #ifdef HAVE_KRB5_GET_PROMPT_TYPES
457 krb5_error_code
pamk5_prompter_krb5_no_password(krb5_context context,void * data,const char * name,const char * banner,int num_prompts,krb5_prompt * prompts)458 pamk5_prompter_krb5_no_password(krb5_context context, void *data,
459                                 const char *name, const char *banner,
460                                 int num_prompts, krb5_prompt *prompts)
461 {
462     krb5_prompt_type *ptypes;
463     int i;
464 
465     ptypes = krb5_get_prompt_types(context);
466     for (i = 0; i < num_prompts; i++)
467         if (ptypes != NULL && ptypes[i] == KRB5_PROMPT_TYPE_PASSWORD)
468             return KRB5_LIBOS_CANTREADPWD;
469     return pamk5_prompter_krb5(context, data, name, banner, num_prompts,
470                                prompts);
471 }
472 #else  /* !HAVE_KRB5_GET_PROMPT_TYPES */
473 krb5_error_code
pamk5_prompter_krb5_no_password(krb5_context context,void * data,const char * name,const char * banner,int num_prompts,krb5_prompt * prompts)474 pamk5_prompter_krb5_no_password(krb5_context context, void *data,
475                                 const char *name, const char *banner,
476                                 int num_prompts, krb5_prompt *prompts)
477 {
478     return pamk5_prompter_krb5(context, data, name, banner, num_prompts,
479                                prompts);
480 }
481 #endif /* !HAVE_KRB5_GET_PROMPT_TYPES */
482