xref: /titanic_44/usr/src/cmd/ssh/sshd/auth2-pam.c (revision cb56572868bfc488bbd3ab847b09db2a25554d44)
1 /*
2  * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
3  * Use is subject to license terms.
4  */
5 
6 #include "includes.h"
7 
8 RCSID("$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 
30 extern u_int utmp_len;
31 extern ServerOptions options;
32 
33 extern Authmethod method_kbdint;
34 extern 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 
40 static int do_pam_conv_kbd_int(int num_msg,
41     struct pam_message **msg, struct pam_response **resp,
42     void *appdata_ptr);
43 static void input_userauth_info_response_pam(int type,
44 					     u_int32_t seqnr,
45 					     void *ctxt);
46 
47 static struct pam_conv conv2 = {
48 	do_pam_conv_kbd_int,
49 	NULL,
50 };
51 
52 static void do_pam_kbdint_cleanup(pam_handle_t *pamh);
53 static void do_pam_kbdint(Authctxt *authctxt);
54 
55 void
56 auth2_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 
84 static void
85 do_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 
176 cleanup:
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 
217 static int
218 do_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 
346 static void
347 input_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
411 int
412 kbdint_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 
424 void
425 kbdint_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