xref: /freebsd/lib/libpam/modules/pam_ssh/pam_ssh.c (revision ee2ea5ceafed78a5bd9810beb9e3ca927180c226)
1 /*-
2  * Copyright (c) 1999, 2000 Andrew J. Korty
3  * All rights reserved.
4  * Copyright (c) 2001,2002 Networks Associates Technology, Inc.
5  * All rights reserved.
6  *
7  * Portions of this software were developed for the FreeBSD Project by
8  * ThinkSec AS and NAI Labs, the Security Research Division of Network
9  * Associates, Inc.  under DARPA/SPAWAR contract N66001-01-C-8035
10  * ("CBOSS"), as part of the DARPA CHATS research program.
11  *
12  * Redistribution and use in source and binary forms, with or without
13  * modification, are permitted provided that the following conditions
14  * are met:
15  * 1. Redistributions of source code must retain the above copyright
16  *    notice, this list of conditions and the following disclaimer.
17  * 2. Redistributions in binary form must reproduce the above copyright
18  *    notice, this list of conditions and the following disclaimer in the
19  *    documentation and/or other materials provided with the distribution.
20  * 3. The name of the author may not be used to endorse or promote
21  *    products derived from this software without specific prior written
22  *    permission.
23  *
24  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
25  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
28  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
30  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
31  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
32  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
33  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
34  * SUCH DAMAGE.
35  *
36  * $Id: pam_ssh.c,v 1.23 2001/08/20 01:44:02 akorty Exp $
37  */
38 
39 #include <sys/cdefs.h>
40 __FBSDID("$FreeBSD$");
41 
42 #include <sys/param.h>
43 #include <sys/stat.h>
44 #include <sys/wait.h>
45 
46 #include <fcntl.h>
47 #include <pwd.h>
48 #include <signal.h>
49 #include <stdio.h>
50 #include <stdlib.h>
51 #include <string.h>
52 #include <unistd.h>
53 
54 #define PAM_SM_AUTH
55 #define PAM_SM_SESSION
56 
57 #include <security/pam_appl.h>
58 #include <security/pam_modules.h>
59 #include <security/openpam.h>
60 
61 #include <openssl/dsa.h>
62 #include <openssl/evp.h>
63 
64 #include "key.h"
65 #include "authfd.h"
66 #include "authfile.h"
67 #include "log.h"
68 #include "pam_ssh.h"
69 
70 static void key_cleanup(pam_handle_t *, void *, int);
71 static void ssh_cleanup(pam_handle_t *, void *, int);
72 
73 /*
74  * Generic cleanup function for OpenSSH "Key" type.
75  */
76 
77 static void
78 key_cleanup(pam_handle_t *pamh __unused, void *data, int err __unused)
79 {
80 	if (data)
81 		key_free(data);
82 }
83 
84 
85 /*
86  * Generic PAM cleanup function for this module.
87  */
88 
89 static void
90 ssh_cleanup(pam_handle_t *pamh __unused, void *data, int err __unused)
91 {
92 	if (data)
93 		free(data);
94 }
95 
96 
97 /*
98  * Authenticate a user's key by trying to decrypt it with the password
99  * provided.  The key and its comment are then stored for later
100  * retrieval by the session phase.  An increasing index is embedded in
101  * the PAM variable names so this function may be called multiple times
102  * for multiple keys.
103  */
104 
105 static int
106 auth_via_key(pam_handle_t *pamh, const char *file, const char *dir,
107     const struct passwd *user, const char *pass)
108 {
109 	char *comment;		/* private key comment */
110 	char *data_name;	/* PAM state */
111 	static int key_idx = 0;	/* for saved keys */
112 	Key *key;		/* user's key */
113 	char *path;		/* to key files */
114 	int retval;		/* from calls */
115 
116 	/* locate the user's private key file */
117 
118 	if (!asprintf(&path, "%s/%s", dir, file)) {
119 		openpam_log(PAM_LOG_ERROR, "%m");
120 		return (PAM_SERVICE_ERR);
121 	}
122 
123 	/* Try to decrypt the private key with the passphrase provided.  If
124 	   success, the user is authenticated. */
125 
126 	comment = NULL;
127 	if ((retval = openpam_borrow_cred(pamh, user)) != PAM_SUCCESS)
128 		return (retval);
129 	key = key_load_private(path, pass, &comment);
130 	openpam_restore_cred(pamh);
131 	free(path);
132 	if (!comment)
133 		comment = strdup(file);
134 	if (!key) {
135 		free(comment);
136 		return (PAM_AUTH_ERR);
137 	}
138 
139 	/* save the key and comment to pass to ssh-agent in the session
140 	   phase */
141 
142 	if (!asprintf(&data_name, "ssh_private_key_%d", key_idx)) {
143 		openpam_log(PAM_LOG_ERROR, "%m");
144 		free(comment);
145 		return (PAM_SERVICE_ERR);
146 	}
147 	retval = pam_set_data(pamh, data_name, key, key_cleanup);
148 	free(data_name);
149 	if (retval != PAM_SUCCESS) {
150 		key_free(key);
151 		free(comment);
152 		return (retval);
153 	}
154 	if (!asprintf(&data_name, "ssh_key_comment_%d", key_idx)) {
155 		openpam_log(PAM_LOG_ERROR, "%m");
156 		free(comment);
157 		return (PAM_SERVICE_ERR);
158 	}
159 	retval = pam_set_data(pamh, data_name, comment, ssh_cleanup);
160 	free(data_name);
161 	if (retval != PAM_SUCCESS) {
162 		free(comment);
163 		return (retval);
164 	}
165 
166 	++key_idx;
167 	return (PAM_SUCCESS);
168 }
169 
170 
171 /*
172  * Add the keys stored by auth_via_key() to the agent connected to the
173  * socket provided.
174  */
175 
176 static int
177 add_keys(pam_handle_t *pamh)
178 {
179 	AuthenticationConnection *ac;	/* connection to ssh-agent */
180 	char *comment;			/* private key comment */
181 	char *data_name;		/* PAM state */
182 	int final;			/* final return value */
183 	int key_idx;			/* for saved keys */
184 	Key *key;			/* user's private key */
185 	int retval;			/* from calls */
186 
187 	/*
188 	 * Connect to the agent.
189 	 *
190 	 * XXX Because ssh_get_authentication_connection() gets the
191 	 * XXX agent parameters from the environment, we have to
192 	 * XXX temporarily replace the environment with the PAM
193 	 * XXX environment list.  This is a hack.
194 	 */
195 	{
196 		extern char **environ;
197 		char **saved, **evp;
198 
199 		saved = environ;
200 		if ((environ = pam_getenvlist(pamh)) == NULL) {
201 			environ = saved;
202 			openpam_log(PAM_LOG_ERROR, "%m");
203 			return (PAM_BUF_ERR);
204 		}
205 		ac = ssh_get_authentication_connection();
206 		for (evp = environ; *evp; evp++)
207 			free(*evp);
208 		free(environ);
209 		environ = saved;
210 	}
211 	if (!ac) {
212 		openpam_log(PAM_LOG_ERROR, "%m");
213 		return (PAM_SESSION_ERR);
214 	}
215 
216 	/* hand off each private key to the agent */
217 
218 	final = 0;
219 	for (key_idx = 0; ; key_idx++) {
220 		if (!asprintf(&data_name, "ssh_private_key_%d", key_idx)) {
221 			openpam_log(PAM_LOG_ERROR, "%m");
222 			ssh_close_authentication_connection(ac);
223 			return (PAM_SERVICE_ERR);
224 		}
225 		retval = pam_get_data(pamh, data_name, (const void **)&key);
226 		free(data_name);
227 		if (retval != PAM_SUCCESS)
228 			break;
229 		if (!asprintf(&data_name, "ssh_key_comment_%d", key_idx)) {
230 			openpam_log(PAM_LOG_ERROR, "%m");
231 			ssh_close_authentication_connection(ac);
232 			return (PAM_SERVICE_ERR);
233 		}
234 		retval = pam_get_data(pamh, data_name,
235 		    (const void **)&comment);
236 		free(data_name);
237 		if (retval != PAM_SUCCESS)
238 			break;
239 		retval = ssh_add_identity(ac, key, comment);
240 		if (!final)
241 			final = retval;
242 	}
243 	ssh_close_authentication_connection(ac);
244 
245 	return (final ? PAM_SUCCESS : PAM_SESSION_ERR);
246 }
247 
248 
249 PAM_EXTERN int
250 pam_sm_authenticate(pam_handle_t *pamh, int flags __unused,
251     int argc __unused, const char *argv[] __unused)
252 {
253 	int authenticated;		/* user authenticated? */
254 	char *dotdir;			/* .ssh dir name */
255 	char *file;			/* current key file */
256 	const char *kfspec;		/* list of key files to add */
257 	char *keyfiles;
258 	const char *pass;		/* passphrase */
259 	const struct passwd *pwent;	/* user's passwd entry */
260 	struct passwd *pwent_keep;	/* our own copy */
261 	int retval;			/* from calls */
262 	const char *user;		/* username */
263 
264 	keyfiles = NULL;
265 	if ((kfspec = openpam_get_option(pamh, OPT_KEYFILES)) != NULL) {
266 		if ((kfspec = strchr(kfspec, '=')) == NULL) {
267 			openpam_log(PAM_LOG_ERROR, "invalid keyfile list");
268 			return (PAM_SERVICE_ERR);
269 		}
270 		++kfspec;
271 	} else {
272 		kfspec = DEF_KEYFILES;
273 	}
274 
275 	if ((retval = pam_get_user(pamh, &user, NULL)) != PAM_SUCCESS)
276 		return (retval);
277 	if (user == NULL || (pwent = getpwnam(user)) == NULL ||
278 	    pwent->pw_dir == NULL || pwent->pw_dir[0] == '\0')
279 		return (PAM_AUTH_ERR);
280 
281 	/* pass prompt message to application and receive passphrase */
282 
283 	retval = pam_get_authtok(pamh, PAM_AUTHTOK, &pass, NEED_PASSPHRASE);
284 	if (retval != PAM_SUCCESS)
285 		return (retval);
286 
287 	OpenSSL_add_all_algorithms(); /* required for DSA */
288 
289 	/* any key will authenticate us, but if we can decrypt all of the
290 	   specified keys, we'll do so here so we can cache them in the
291 	   session phase */
292 
293 	if (!asprintf(&dotdir, "%s/%s", pwent->pw_dir, SSH_CLIENT_DIR)) {
294 		openpam_log(PAM_LOG_ERROR, "%m");
295 		return (PAM_SERVICE_ERR);
296 	}
297 	authenticated = 0;
298 	keyfiles = strdup(kfspec);
299 	for (file = strtok(keyfiles, SEP_KEYFILES); file;
300 	     file = strtok(NULL, SEP_KEYFILES))
301 		if (auth_via_key(pamh, file, dotdir, pwent, pass) ==
302 		    PAM_SUCCESS)
303 			authenticated++;
304 	free(keyfiles);
305 	free(dotdir);
306 	if (!authenticated)
307 		return (PAM_AUTH_ERR);
308 
309 	/* copy the passwd entry (in case successive calls are made) and
310 	   save it for the session phase */
311 
312 	if (!(pwent_keep = malloc(sizeof *pwent))) {
313 		openpam_log(PAM_LOG_ERROR, "%m");
314 		return (PAM_SERVICE_ERR);
315 	}
316 	(void) memcpy(pwent_keep, pwent, sizeof *pwent_keep);
317 	if ((retval = pam_set_data(pamh, "ssh_passwd_entry", pwent_keep,
318 	    ssh_cleanup)) != PAM_SUCCESS) {
319 		free(pwent_keep);
320 		return (retval);
321 	}
322 
323 	return (PAM_SUCCESS);
324 }
325 
326 
327 PAM_EXTERN int
328 pam_sm_setcred(pam_handle_t *pamh __unused, int flags __unused,
329     int argc __unused, const char *argv[] __unused)
330 {
331 
332 	return (PAM_SUCCESS);
333 }
334 
335 
336 PAM_EXTERN int
337 pam_sm_open_session(pam_handle_t *pamh, int flags __unused,
338     int argc __unused, const char *argv[] __unused)
339 {
340 	char *agent_socket;		/* agent socket */
341 	char *env_end;			/* end of env */
342 	FILE *env_read;			/* env data source */
343 	char env_string[BUFSIZ];	/* environment string */
344 	char *env_value;		/* envariable value */
345 	int env_write;			/* env file descriptor */
346 	char hname[MAXHOSTNAMELEN];	/* local hostname */
347 	int no_link;			/* link per-agent file? */
348 	char *per_agent;		/* to store env */
349 	char *per_session;		/* per-session filename */
350 	char *agent_pid;		/* agent pid */
351 	const struct passwd *pwent;	/* user's passwd entry */
352 	int retval;			/* from calls */
353 	int start_agent;		/* start agent? */
354 	const char *tty;		/* tty or display name */
355 
356 	/* dump output of ssh-agent in ~/.ssh */
357 	if ((retval = pam_get_data(pamh, "ssh_passwd_entry",
358 	    (const void **)&pwent)) != PAM_SUCCESS)
359 		return (retval);
360 
361 	/*
362 	 * Use reference counts to limit agents to one per user per host.
363 	 *
364 	 * Technique: Create an environment file containing
365 	 * information about the agent.  Only one file is created, but
366 	 * it may be given many names.  One name is given for the
367 	 * agent itself, agent-<host>.  Another name is given for each
368 	 * session, agent-<host>-<display> or agent-<host>-<tty>.  We
369 	 * delete the per-session filename on session close, and when
370 	 * the link count goes to unity on the per-agent file, we
371 	 * delete the file and kill the agent.
372 	 */
373 
374 	/* the per-agent file contains just the hostname */
375 
376 	(void) gethostname(hname, sizeof hname);
377 	if (asprintf(&per_agent, "%s/.ssh/agent-%s", pwent->pw_dir, hname)
378 	    == -1) {
379 		openpam_log(PAM_LOG_ERROR, "%m");
380 		return (PAM_SERVICE_ERR);
381 	}
382 
383 	/* save the per-agent filename in case we want to delete it on
384 	   session close */
385 
386 	if ((retval = pam_set_data(pamh, "ssh_agent_env_agent", per_agent,
387 	    ssh_cleanup)) != PAM_SUCCESS) {
388 		free(per_agent);
389 		return (retval);
390 	}
391 
392 	/* take on the user's privileges for writing files and starting the
393 	   agent */
394 
395 	if ((retval = openpam_borrow_cred(pamh, pwent)) != PAM_SUCCESS)
396 		return (retval);
397 
398 	/* Try to create the per-agent file or open it for reading if it
399 	   exists.  If we can't do either, we won't try to link a
400 	   per-session filename later.  Start the agent if we can't open
401 	   the file for reading. */
402 
403 	env_write = no_link = 0;
404 	env_read = NULL;
405 	if ((env_write = open(per_agent, O_CREAT | O_EXCL | O_WRONLY,
406 	    S_IRUSR)) < 0 && !(env_read = fopen(per_agent, "r")))
407 		no_link = 1;
408 	if (env_read) {
409 		start_agent = 0;
410 		openpam_restore_cred(pamh);
411 	} else {
412 		start_agent = 1;
413 		env_read = popen(SSH_AGENT, "r");
414 		openpam_restore_cred(pamh);
415 		if (!env_read) {
416 			openpam_log(PAM_LOG_ERROR, "%s: %m", SSH_AGENT);
417 			if (env_write >= 0)
418 				(void) close(env_write);
419 			return (PAM_SESSION_ERR);
420 		}
421 	}
422 
423 	/* save environment for application with pam_putenv() */
424 
425 	agent_socket = NULL;
426 	while (fgets(env_string, sizeof env_string, env_read)) {
427 
428 		/* parse environment definitions */
429 
430 		if (env_write >= 0)
431 			(void) write(env_write, env_string,
432 			    strlen(env_string));
433 		if (!(env_value = strchr(env_string, '=')) ||
434 		    !(env_end = strchr(env_value, ';')))
435 			continue;
436 		*env_end = '\0';
437 
438 		/* pass to the application */
439 
440 		if (!((retval = pam_putenv(pamh, env_string)) ==
441 		    PAM_SUCCESS)) {
442 			if (start_agent)
443 				(void) pclose(env_read);
444 			else
445 				(void) fclose(env_read);
446 			if (env_write >= 0)
447 				(void) close(env_write);
448 			if (agent_socket)
449 				free(agent_socket);
450 			return (PAM_SERVICE_ERR);
451 		}
452 
453 		*env_value++ = '\0';
454 
455 		/* save the agent socket so we can connect to it and add
456 		   the keys as well as the PID so we can kill the agent on
457 		   session close. */
458 
459 		if (strcmp(&env_string[strlen(env_string) -
460 		    strlen(ENV_SOCKET_SUFFIX)], ENV_SOCKET_SUFFIX) == 0 &&
461 		    !(agent_socket = strdup(env_value))) {
462 			openpam_log(PAM_LOG_ERROR, "%m");
463 			if (start_agent)
464 				(void) pclose(env_read);
465 			else
466 				(void) fclose(env_read);
467 			if (env_write >= 0)
468 				(void) close(env_write);
469 			if (agent_socket)
470 				free(agent_socket);
471 			return (PAM_SERVICE_ERR);
472 		} else if (strcmp(&env_string[strlen(env_string) -
473 		    strlen(ENV_PID_SUFFIX)], ENV_PID_SUFFIX) == 0 &&
474 		    ((agent_pid = strdup(env_value)) == NULL ||
475 		    (retval = pam_set_data(pamh, "ssh_agent_pid",
476 		    agent_pid, ssh_cleanup)) != PAM_SUCCESS)) {
477 			if (start_agent)
478 				(void) pclose(env_read);
479 			else
480 				(void) fclose(env_read);
481 			if (env_write >= 0)
482 				(void) close(env_write);
483 			if (agent_socket)
484 				free(agent_socket);
485 			if (agent_pid)
486 				free(agent_pid);
487 			return (retval);
488 		}
489 
490 	}
491 	if (env_write >= 0)
492 		(void) close(env_write);
493 
494 	if (start_agent) {
495 		switch (retval = pclose(env_read)) {
496 		case -1:
497 			openpam_log(PAM_LOG_ERROR, "%s: %m", SSH_AGENT);
498 			if (agent_socket)
499 				free(agent_socket);
500 			return (PAM_SESSION_ERR);
501 		case 0:
502 			break;
503 		case 127:
504 			openpam_log(PAM_LOG_ERROR, "cannot execute %s",
505 			    SSH_AGENT);
506 			if (agent_socket)
507 				free(agent_socket);
508 			return (PAM_SESSION_ERR);
509 		default:
510 			openpam_log(PAM_LOG_ERROR, "%s exited %s %d",
511 			    SSH_AGENT, WIFSIGNALED(retval) ? "on signal" :
512 			    "with status", WIFSIGNALED(retval) ?
513 			    WTERMSIG(retval) : WEXITSTATUS(retval));
514 			if (agent_socket)
515 				free(agent_socket);
516 			return (PAM_SESSION_ERR);
517 		}
518 	} else
519 		(void) fclose(env_read);
520 
521 	if (!agent_socket)
522 		return (PAM_SESSION_ERR);
523 
524 	if (start_agent && (retval = add_keys(pamh))
525 	    != PAM_SUCCESS)
526 		return (retval);
527 	free(agent_socket);
528 
529 	/* if we couldn't access the per-agent file, don't link a
530 	   per-session filename to it */
531 
532 	if (no_link)
533 		return (PAM_SUCCESS);
534 
535 	/* the per-session file contains the display name or tty name as
536 	   well as the hostname */
537 
538 	if ((retval = pam_get_item(pamh, PAM_TTY, (const void **)&tty))
539 	    != PAM_SUCCESS)
540 		return (retval);
541 	if (asprintf(&per_session, "%s/.ssh/agent-%s-%s", pwent->pw_dir,
542 	    hname, tty) == -1) {
543 		openpam_log(PAM_LOG_ERROR, "%m");
544 		return (PAM_SERVICE_ERR);
545 	}
546 
547 	/* save the per-session filename so we can delete it on session
548 	   close */
549 
550 	if ((retval = pam_set_data(pamh, "ssh_agent_env_session",
551 	    per_session, ssh_cleanup)) != PAM_SUCCESS) {
552 		free(per_session);
553 		return (retval);
554 	}
555 
556 	(void) unlink(per_session);		/* remove cruft */
557 	(void) link(per_agent, per_session);
558 
559 	return (PAM_SUCCESS);
560 }
561 
562 
563 PAM_EXTERN int
564 pam_sm_close_session(pam_handle_t *pamh, int flags __unused,
565     int argc __unused, const char *argv[] __unused)
566 {
567 	const char *env_file;		/* ssh-agent environment */
568 	pid_t pid;			/* ssh-agent process id */
569 	int retval;			/* from calls */
570 	const char *ssh_agent_pid;	/* ssh-agent pid string */
571 	struct stat sb;			/* to check st_nlink */
572 
573 	if ((retval = pam_get_data(pamh, "ssh_agent_env_session",
574 	    (const void **)&env_file)) == PAM_SUCCESS && env_file)
575 		(void) unlink(env_file);
576 
577 	/* Retrieve per-agent filename and check link count.  If it's
578 	   greater than unity, other sessions are still using this
579 	   agent. */
580 
581 	if ((retval = pam_get_data(pamh, "ssh_agent_env_agent",
582 	    (const void **)&env_file)) == PAM_SUCCESS && env_file &&
583 	    stat(env_file, &sb) == 0) {
584 		if (sb.st_nlink > 1)
585 			return (PAM_SUCCESS);
586 		(void) unlink(env_file);
587 	}
588 
589 	/* retrieve the agent's process id */
590 
591 	if ((retval = pam_get_data(pamh, "ssh_agent_pid",
592 	    (const void **)&ssh_agent_pid)) != PAM_SUCCESS)
593 		return (retval);
594 
595 	/* Kill the agent.  SSH's ssh-agent does not have a -k option, so
596 	   just call kill(). */
597 
598 	pid = atoi(ssh_agent_pid);
599 	if (pid <= 0)
600 		return (PAM_SESSION_ERR);
601 	if (kill(pid, SIGTERM) != 0) {
602 		openpam_log(PAM_LOG_ERROR, "%s: %m", ssh_agent_pid);
603 		return (PAM_SESSION_ERR);
604 	}
605 
606 	return (PAM_SUCCESS);
607 }
608 
609 PAM_MODULE_ENTRY(MODULE_NAME);
610