1/*
2 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
3 * Use is subject to license terms.
4 */
5
6#include "includes.h"
7
8RCSID("$Id: auth2-pam.c,v 1.14 2002/06/28 16:48:12 mouring Exp $");
9
10#ifdef USE_PAM
11#include <security/pam_appl.h>
12
13#include "ssh.h"
14#include "ssh2.h"
15#include "auth.h"
16#include "auth-pam.h"
17#include "auth-options.h"
18#include "packet.h"
19#include "xmalloc.h"
20#include "dispatch.h"
21#include "canohost.h"
22#include "log.h"
23#include "servconf.h"
24#include "misc.h"
25
26#ifdef HAVE_BSM
27#include "bsmaudit.h"
28#endif /* HAVE_BSM */
29
30extern u_int utmp_len;
31extern ServerOptions options;
32
33extern Authmethod method_kbdint;
34extern Authmethod method_passwd;
35
36#define SSHD_PAM_KBDINT_SVC "sshd-kbdint"
37/* Maximum attempts for changing expired password */
38#define DEF_ATTEMPTS 3
39
40static int do_pam_conv_kbd_int(int num_msg,
41    struct pam_message **msg, struct pam_response **resp,
42    void *appdata_ptr);
43static void input_userauth_info_response_pam(int type,
44					     u_int32_t seqnr,
45					     void *ctxt);
46
47static struct pam_conv conv2 = {
48	do_pam_conv_kbd_int,
49	NULL,
50};
51
52static void do_pam_kbdint_cleanup(pam_handle_t *pamh);
53static void do_pam_kbdint(Authctxt *authctxt);
54
55void
56auth2_pam(Authctxt *authctxt)
57{
58	if (authctxt->user == NULL)
59		fatal("auth2_pam: internal error: no user");
60	if (authctxt->method == NULL)
61		fatal("auth2_pam: internal error: no method");
62
63	conv2.appdata_ptr = authctxt;
64	new_start_pam(authctxt, &conv2);
65
66	authctxt->method->method_data = NULL; /* freed in the conv func */
67	dispatch_set(SSH2_MSG_USERAUTH_INFO_RESPONSE,
68	    &input_userauth_info_response_pam);
69
70	/*
71	 * Since password userauth and keyboard-interactive userauth
72	 * both use PAM, and since keyboard-interactive is so much
73	 * better than password userauth, we should not allow the user
74	 * to try password userauth after trying keyboard-interactive.
75	 */
76	if (method_passwd.enabled)
77		*method_passwd.enabled = 0;
78
79	do_pam_kbdint(authctxt);
80
81	dispatch_set(SSH2_MSG_USERAUTH_INFO_RESPONSE, NULL);
82}
83
84static void
85do_pam_kbdint(Authctxt *authctxt)
86{
87	int		 retval, retval2;
88	pam_handle_t	*pamh = authctxt->pam->h;
89	const char	*where = "authenticating";
90	char		*text = NULL;
91
92	debug2("Calling pam_authenticate()");
93	retval = pam_authenticate(pamh,
94	    options.permit_empty_passwd ? 0 :
95	    PAM_DISALLOW_NULL_AUTHTOK);
96
97	if (retval != PAM_SUCCESS)
98		goto cleanup;
99
100	debug2("kbd-int: pam_authenticate() succeeded");
101	where = "authorizing";
102	retval = pam_acct_mgmt(pamh, 0);
103
104	if (retval == PAM_NEW_AUTHTOK_REQD) {
105		if (authctxt->valid && authctxt->pw != NULL) {
106			/* send password expiration warning */
107			message_cat(&text,
108			    gettext("Warning: Your password has expired,"
109			    " please change it now."));
110			packet_start(SSH2_MSG_USERAUTH_INFO_REQUEST);
111			packet_put_cstring("");		/* name */
112			packet_put_utf8_cstring(text);	/* instructions */
113			packet_put_cstring("");		/* language, unused */
114			packet_put_int(0);
115			packet_send();
116			packet_write_wait();
117			debug("expiration message sent");
118			if (text)
119				xfree(text);
120			/*
121			 * wait for the response so it does not mix
122			 * with the upcoming PAM conversation
123			 */
124			packet_read_expect(SSH2_MSG_USERAUTH_INFO_RESPONSE);
125			/*
126			 * Can't use temporarily_use_uid() and restore_uid()
127			 * here because we need (euid == 0 && ruid == pw_uid)
128			 * whereas temporarily_use_uid() arranges for
129			 * (suid = 0 && euid == pw_uid && ruid == pw_uid).
130			 */
131			(void) setreuid(authctxt->pw->pw_uid, -1);
132			debug2("kbd-int: changing expired password");
133			where = "changing authentication tokens (password)";
134			/*
135			 * Depending on error returned from pam_chauthtok, we
136			 * need to try to change password a few times before
137			 * we error out and return.
138			 */
139			int tries = 0;
140			while ((retval = pam_chauthtok(pamh,
141			    PAM_CHANGE_EXPIRED_AUTHTOK)) != PAM_SUCCESS) {
142				if (tries++ < DEF_ATTEMPTS) {
143					if ((retval == PAM_AUTHTOK_ERR) ||
144					    (retval == PAM_TRY_AGAIN)) {
145						continue;
146					}
147				}
148				break;
149			}
150			audit_sshd_chauthtok(retval, authctxt->pw->pw_uid,
151				authctxt->pw->pw_gid);
152			(void) setreuid(0, -1);
153		} else {
154			retval = PAM_PERM_DENIED;
155		}
156	}
157
158	if (retval != PAM_SUCCESS)
159		goto cleanup;
160
161	authctxt->pam->state |= PAM_S_DONE_ACCT_MGMT;
162
163	retval = finish_userauth_do_pam(authctxt);
164
165	if (retval != PAM_SUCCESS)
166		goto cleanup;
167
168	/*
169	 * PAM handle stays around so we can call pam_close_session()
170	 * on it later.
171	 */
172	authctxt->method->authenticated = 1;
173	debug2("kbd-int: success (pam->state == %x)", authctxt->pam->state);
174	return;
175
176cleanup:
177	/*
178	 * Check for abandonment and cleanup.  When kbdint is abandoned
179	 * authctxt->pam->h is NULLed and by this point a new handle may
180	 * be allocated.
181	 */
182	if (authctxt->pam->h != pamh) {
183		log("Keyboard-interactive (PAM) userauth abandoned "
184		    "while %s", where);
185		if ((retval2 = pam_end(pamh, retval)) != PAM_SUCCESS) {
186			log("Cannot close PAM handle after "
187			    "kbd-int userauth abandonment[%d]: %.200s",
188			    retval2, PAM_STRERROR(pamh, retval2));
189		}
190		authctxt->method->abandoned = 1;
191
192		/*
193		 * Avoid double counting; these are incremented in
194		 * kbdint_pam_abandon() so that they reflect the correct
195		 * count when userauth_finish() is called before
196		 * unwinding the dispatch_run() loop, but they are
197		 * incremented again in input_userauth_request() when
198		 * the loop is unwound, right here.
199		 */
200		if (authctxt->method->abandons)
201			authctxt->method->abandons--;
202		if (authctxt->method->attempts)
203			authctxt->method->attempts--;
204	}
205	else {
206		/* Save error value for pam_end() */
207		authctxt->pam->last_pam_retval = retval;
208		log("Keyboard-interactive (PAM) userauth failed[%d] "
209		    "while %s: %.200s", retval, where,
210		    PAM_STRERROR(pamh, retval));
211		/* pam handle can be reused elsewhere, so no pam_end() here */
212	}
213
214	return;
215}
216
217static int
218do_pam_conv_kbd_int(int num_msg, struct pam_message **msg,
219    struct pam_response **resp, void *appdata_ptr)
220{
221	int i, j;
222	char *text;
223	Convctxt *conv_ctxt;
224	Authctxt *authctxt = (Authctxt *)appdata_ptr;
225
226	if (!authctxt || !authctxt->method) {
227		debug("Missing state during PAM conversation");
228		return PAM_CONV_ERR;
229	}
230
231	conv_ctxt = xmalloc(sizeof(Convctxt));
232	(void) memset(conv_ctxt, 0, sizeof(Convctxt));
233	conv_ctxt->finished = 0;
234	conv_ctxt->num_received = 0;
235	conv_ctxt->num_expected = 0;
236	conv_ctxt->prompts = xmalloc(sizeof(int) * num_msg);
237	conv_ctxt->responses = xmalloc(sizeof(struct pam_response) * num_msg);
238	(void) memset(conv_ctxt->responses, 0, sizeof(struct pam_response) * num_msg);
239
240	text = NULL;
241	for (i = 0, conv_ctxt->num_expected = 0; i < num_msg; i++) {
242		int style = PAM_MSG_MEMBER(msg, i, msg_style);
243		switch (style) {
244		case PAM_PROMPT_ECHO_ON:
245			debug2("PAM echo on prompt: %s",
246				PAM_MSG_MEMBER(msg, i, msg));
247			conv_ctxt->num_expected++;
248			break;
249		case PAM_PROMPT_ECHO_OFF:
250			debug2("PAM echo off prompt: %s",
251				PAM_MSG_MEMBER(msg, i, msg));
252			conv_ctxt->num_expected++;
253			break;
254		case PAM_TEXT_INFO:
255			debug2("PAM text info prompt: %s",
256				PAM_MSG_MEMBER(msg, i, msg));
257			message_cat(&text, PAM_MSG_MEMBER(msg, i, msg));
258			break;
259		case PAM_ERROR_MSG:
260			debug2("PAM error prompt: %s",
261				PAM_MSG_MEMBER(msg, i, msg));
262			message_cat(&text, PAM_MSG_MEMBER(msg, i, msg));
263			break;
264		default:
265			/* Capture all these messages to be sent at once */
266			message_cat(&text, PAM_MSG_MEMBER(msg, i, msg));
267			break;
268		}
269	}
270
271	if (conv_ctxt->num_expected == 0 && text == NULL) {
272		xfree(conv_ctxt->prompts);
273		xfree(conv_ctxt->responses);
274		xfree(conv_ctxt);
275		return PAM_SUCCESS;
276	}
277
278	authctxt->method->method_data = (void *) conv_ctxt;
279
280	packet_start(SSH2_MSG_USERAUTH_INFO_REQUEST);
281	packet_put_cstring("");	/* Name */
282	packet_put_utf8_cstring(text ? text : "");	/* Instructions */
283	packet_put_cstring("");	/* Language */
284	packet_put_int(conv_ctxt->num_expected);
285
286	if (text)
287		xfree(text);
288
289	for (i = 0, j = 0; i < num_msg; i++) {
290		int style = PAM_MSG_MEMBER(msg, i, msg_style);
291
292		/* Skip messages which don't need a reply */
293		if (style != PAM_PROMPT_ECHO_ON && style != PAM_PROMPT_ECHO_OFF)
294			continue;
295
296		conv_ctxt->prompts[j++] = i;
297		packet_put_utf8_cstring(PAM_MSG_MEMBER(msg, i, msg));
298		packet_put_char(style == PAM_PROMPT_ECHO_ON);
299	}
300	packet_send();
301	packet_write_wait();
302
303	/*
304	 * Here the dispatch_run() loop is nested.  It should be unwound
305	 * if keyboard-interactive userauth is abandoned (or restarted;
306	 * same thing).
307	 *
308	 * The condition for breaking out of the nested dispatch_run() loop is
309	 *     ((got kbd-int info reponse) || (kbd-int abandoned))
310	 *
311	 * conv_ctxt->finished is set in either of those cases.
312	 *
313	 * When abandonment is detected the conv_ctxt->finished is set as
314	 * is conv_ctxt->abandoned, causing this function to signal
315	 * userauth nested dispatch_run() loop unwinding and to return
316	 * PAM_CONV_ERR;
317	 */
318	debug2("Nesting dispatch_run loop");
319	dispatch_run(DISPATCH_BLOCK, &conv_ctxt->finished, appdata_ptr);
320	debug2("Nested dispatch_run loop exited");
321
322	if (conv_ctxt->abandoned) {
323		authctxt->unwind_dispatch_loop = 1;
324		xfree(conv_ctxt->prompts);
325		xfree(conv_ctxt->responses);
326		xfree(conv_ctxt);
327		debug("PAM conv function returns PAM_CONV_ERR");
328		return PAM_CONV_ERR;
329	}
330
331	if (conv_ctxt->num_received == conv_ctxt->num_expected) {
332		*resp = conv_ctxt->responses;
333		xfree(conv_ctxt->prompts);
334		xfree(conv_ctxt);
335		debug("PAM conv function returns PAM_SUCCESS");
336		return PAM_SUCCESS;
337	}
338
339	debug("PAM conv function returns PAM_CONV_ERR");
340	xfree(conv_ctxt->prompts);
341	xfree(conv_ctxt->responses);
342	xfree(conv_ctxt);
343	return PAM_CONV_ERR;
344}
345
346static void
347input_userauth_info_response_pam(int type, u_int32_t seqnr, void *ctxt)
348{
349	Authctxt *authctxt = ctxt;
350	Convctxt *conv_ctxt;
351	unsigned int nresp = 0, rlen = 0, i = 0;
352	char *resp;
353
354	if (authctxt == NULL)
355		fatal("input_userauth_info_response_pam: no authentication context");
356
357	/* Check for spurious/unexpected info response */
358	if (method_kbdint.method_data == NULL) {
359		debug("input_userauth_info_response_pam: no method context");
360		return;
361	}
362
363	conv_ctxt = (Convctxt *) method_kbdint.method_data;
364
365	nresp = packet_get_int();	/* Number of responses. */
366	debug("got %d responses", nresp);
367
368
369#if 0
370	if (nresp != conv_ctxt->num_expected)
371		fatal("%s: Received incorrect number of responses "
372		    "(expected %d, received %u)", __func__,
373		    conv_ctxt->num_expected, nresp);
374#endif
375
376	if (nresp > 100)
377		fatal("%s: too many replies", __func__);
378
379	for (i = 0; i < nresp && i < conv_ctxt->num_expected ; i++) {
380		int j = conv_ctxt->prompts[i];
381
382		/*
383		 * We assume that ASCII charset is used for password
384		 * although the protocol requires UTF-8 encoding for the
385		 * password string. Therefore, we don't perform code
386		 * conversion for the string.
387		 */
388		resp = packet_get_string(&rlen);
389		if (i < conv_ctxt->num_expected) {
390			conv_ctxt->responses[j].resp_retcode = PAM_SUCCESS;
391			conv_ctxt->responses[j].resp = xstrdup(resp);
392			conv_ctxt->num_received++;
393		}
394		xfree(resp);
395	}
396
397	if (nresp < conv_ctxt->num_expected)
398		fatal("%s: too few replies (%d < %d)", __func__,
399		    nresp, conv_ctxt->num_expected);
400
401	/* XXX - This could make a covert channel... */
402	if (nresp > conv_ctxt->num_expected)
403		debug("Ignoring additional PAM replies");
404
405	conv_ctxt->finished = 1;
406
407	packet_check_eom();
408}
409
410#if 0
411int
412kbdint_pam_abandon_chk(Authctxt *authctxt, Authmethod *method)
413{
414	if (!method)
415		return 0; /* fatal(), really; it'll happen somewhere else */
416
417	if (!method->method_data)
418		return 0;
419
420	return 1;
421}
422#endif
423
424void
425kbdint_pam_abandon(Authctxt *authctxt, Authmethod *method)
426{
427	Convctxt *conv_ctxt;
428
429	/*
430	 * But, if it ever becomes desirable and possible to support
431	 * kbd-int userauth abandonment, here's what must be done.
432	 */
433	if (!method)
434		return;
435
436	if (!method->method_data)
437		return;
438
439	conv_ctxt = (Convctxt *) method->method_data;
440
441	/* dispatch_run() loop will exit */
442	conv_ctxt->abandoned = 1;
443	conv_ctxt->finished = 1;
444
445	/*
446	 * The method_data will be free in the corresponding, active
447	 * conversation function
448	 */
449	method->method_data = NULL;
450
451	/* update counts that can't be updated elsewhere */
452	method->abandons++;
453	method->attempts++;
454
455	/* Finally, we cannot re-use the current current PAM handle */
456	authctxt->pam->h = NULL;    /* Let the conv function cleanup */
457}
458#endif
459