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