pam_ssh.c revision 93804
1/*-
2 * Copyright (c) 1999, 2000 Andrew J. Korty
3 * All rights reserved.
4 * Copyright (c) 2001 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: head/lib/libpam/modules/pam_ssh/pam_ssh.c 93804 2002-04-04 18:45:21Z des $");
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_ACCOUNT
56#define PAM_SM_SESSION
57#define PAM_SM_PASSWORD
58
59#include <security/pam_appl.h>
60#include <security/pam_modules.h>
61#include <security/pam_mod_misc.h>
62
63#include <openssl/dsa.h>
64#include <openssl/evp.h>
65
66#include "key.h"
67#include "authfd.h"
68#include "authfile.h"
69#include "log.h"
70#include "pam_ssh.h"
71
72/*
73 * Generic cleanup function for OpenSSH "Key" type.
74 */
75
76void
77key_cleanup(pam_handle_t *pamh, void *data, int error_status)
78{
79	if (data)
80		key_free(data);
81}
82
83
84/*
85 * Generic PAM cleanup function for this module.
86 */
87
88void
89ssh_cleanup(pam_handle_t *pamh, void *data, int error_status)
90{
91	if (data)
92		free(data);
93}
94
95
96/*
97 * Authenticate a user's key by trying to decrypt it with the password
98 * provided.  The key and its comment are then stored for later
99 * retrieval by the session phase.  An increasing index is embedded in
100 * the PAM variable names so this function may be called multiple times
101 * for multiple keys.
102 */
103
104static int
105auth_via_key(pam_handle_t *pamh, const char *file, const char *dir,
106    const struct passwd *user, const char *pass)
107{
108	char *comment;		/* private key comment */
109	char *data_name;	/* PAM state */
110	static int index = 0;	/* for saved keys */
111	Key *key;		/* user's key */
112	char *path;		/* to key files */
113	int retval;		/* from calls */
114	uid_t saved_uid;	/* caller's uid */
115
116	/* locate the user's private key file */
117
118	if (!asprintf(&path, "%s/%s", dir, file)) {
119		openpam_log(PAM_LOG_ERROR, "%s: %m", MODULE_NAME);
120		return PAM_SERVICE_ERR;
121	}
122
123	saved_uid = getuid();
124
125	/* Try to decrypt the private key with the passphrase provided.  If
126	   success, the user is authenticated. */
127
128	comment = NULL;
129	(void) setreuid(user->pw_uid, saved_uid);
130	key = key_load_private(path, pass, &comment);
131	(void) setuid(saved_uid);
132	free(path);
133	if (!comment)
134		comment = strdup(file);
135	if (!key) {
136		free(comment);
137		return PAM_AUTH_ERR;
138	}
139
140	/* save the key and comment to pass to ssh-agent in the session
141	   phase */
142
143	if (!asprintf(&data_name, "ssh_private_key_%d", index)) {
144		openpam_log(PAM_LOG_ERROR, "%s: %m", MODULE_NAME);
145		free(comment);
146		return PAM_SERVICE_ERR;
147	}
148	retval = pam_set_data(pamh, data_name, key, key_cleanup);
149	free(data_name);
150	if (retval != PAM_SUCCESS) {
151		key_free(key);
152		free(comment);
153		return retval;
154	}
155	if (!asprintf(&data_name, "ssh_key_comment_%d", index)) {
156		openpam_log(PAM_LOG_ERROR, "%s: %m", MODULE_NAME);
157		free(comment);
158		return PAM_SERVICE_ERR;
159	}
160	retval = pam_set_data(pamh, data_name, comment, ssh_cleanup);
161	free(data_name);
162	if (retval != PAM_SUCCESS) {
163		free(comment);
164		return retval;
165	}
166
167	++index;
168	return PAM_SUCCESS;
169}
170
171
172/*
173 * Add the keys stored by auth_via_key() to the agent connected to the
174 * socket provided.
175 */
176
177static int
178add_keys(pam_handle_t *pamh, char *socket)
179{
180	AuthenticationConnection *ac;	/* connection to ssh-agent */
181	char *comment;			/* private key comment */
182	char *data_name;		/* PAM state */
183	int final;			/* final return value */
184	int index;			/* for saved keys */
185	Key *key;			/* user's private key */
186	int retval;			/* from calls */
187
188	/*
189	 * Connect to the agent.
190	 *
191	 * XXX Because ssh_get_authentication_connection() gets the
192	 * XXX agent parameters from the environment, we have to
193	 * XXX temporarily replace the environment with the PAM
194	 * XXX environment list.  This is a hack.
195	 */
196	{
197		extern char **environ;
198		char **saved, **evp;
199
200		saved = environ;
201		if ((environ = pam_getenvlist(pamh)) == NULL) {
202			environ = saved;
203			openpam_log(PAM_LOG_ERROR, "%s: %m", MODULE_NAME);
204			return (PAM_BUF_ERR);
205		}
206		ac = ssh_get_authentication_connection();
207		for (evp = environ; *evp; evp++)
208			free(*evp);
209		free(environ);
210		environ = saved;
211	}
212	if (!ac) {
213		openpam_log(PAM_LOG_ERROR, "%s: %s: %m", MODULE_NAME, socket);
214		return PAM_SESSION_ERR;
215	}
216
217	/* hand off each private key to the agent */
218
219	final = 0;
220	for (index = 0; ; index++) {
221		if (!asprintf(&data_name, "ssh_private_key_%d", index)) {
222			openpam_log(PAM_LOG_ERROR, "%s: %m", MODULE_NAME);
223			ssh_close_authentication_connection(ac);
224			return PAM_SERVICE_ERR;
225		}
226		retval = pam_get_data(pamh, data_name, (const void **)&key);
227		free(data_name);
228		if (retval != PAM_SUCCESS)
229			break;
230		if (!asprintf(&data_name, "ssh_key_comment_%d", index)) {
231			openpam_log(PAM_LOG_ERROR, "%s: %m", MODULE_NAME);
232			ssh_close_authentication_connection(ac);
233			return PAM_SERVICE_ERR;
234		}
235		retval = pam_get_data(pamh, data_name,
236		    (const void **)&comment);
237		free(data_name);
238		if (retval != PAM_SUCCESS)
239			break;
240		retval = ssh_add_identity(ac, key, comment);
241		if (!final)
242			final = retval;
243	}
244	ssh_close_authentication_connection(ac);
245
246	return final ? PAM_SUCCESS : PAM_SESSION_ERR;
247}
248
249
250PAM_EXTERN int
251pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc,
252    const char **argv)
253{
254	int authenticated;		/* user authenticated? */
255	char *dotdir;			/* .ssh dir name */
256	char *file;			/* current key file */
257	char *keyfiles;			/* list of key files to add */
258	int options;			/* options for pam_get_pass() */
259	const char *pass;		/* passphrase */
260	const struct passwd *pwent;	/* user's passwd entry */
261	struct passwd *pwent_keep;	/* our own copy */
262	int retval;			/* from calls */
263	const char *user;		/* username */
264
265	keyfiles = DEF_KEYFILES;
266	options = 0;
267	for (; argc; argc--, argv++)
268		if (strncmp(*argv, OPT_KEYFILES "=", sizeof OPT_KEYFILES)
269		    == 0) {
270			if (!(keyfiles = strchr(*argv, '=') + 1))
271				return PAM_AUTH_ERR;
272		} else if (strcmp(*argv, OPT_TRY_FIRST_PASS) == 0)
273			options |= PAM_OPT_TRY_FIRST_PASS;
274		else if (strcmp(*argv, OPT_USE_FIRST_PASS) == 0)
275			options |= PAM_OPT_USE_FIRST_PASS;
276
277
278	if ((retval = pam_get_user(pamh, &user, NULL)) != PAM_SUCCESS)
279		return retval;
280	if (!((pwent = getpwnam(user)) && pwent->pw_dir))
281		return PAM_AUTH_ERR;
282
283	/* pass prompt message to application and receive passphrase */
284
285	if ((retval = pam_get_pass(pamh, &pass, NEED_PASSPHRASE, options))
286	    != PAM_SUCCESS)
287		return retval;
288
289	OpenSSL_add_all_algorithms(); /* required for DSA */
290
291	/* any key will authenticate us, but if we can decrypt all of the
292	   specified keys, we'll do so here so we can cache them in the
293	   session phase */
294
295	if (!asprintf(&dotdir, "%s/%s", pwent->pw_dir, SSH_CLIENT_DIR)) {
296		openpam_log(PAM_LOG_ERROR, "%s: %m", MODULE_NAME);
297		return PAM_SERVICE_ERR;
298	}
299	authenticated = 0;
300	keyfiles = strdup(keyfiles);
301	for (file = strtok(keyfiles, SEP_KEYFILES); file;
302	     file = strtok(NULL, SEP_KEYFILES))
303		if (auth_via_key(pamh, file, dotdir, pwent, pass) ==
304		    PAM_SUCCESS)
305			authenticated++;
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
328PAM_EXTERN int
329pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv)
330{
331	return PAM_SUCCESS;
332}
333
334
335PAM_EXTERN int
336pam_sm_open_session(pam_handle_t *pamh, int flags, int argc,
337    const char **argv)
338{
339	char *agent_socket;		/* agent socket */
340	char *env_end;			/* end of env */
341	FILE *env_read;			/* env data source */
342	char env_string[BUFSIZ];	/* environment string */
343	char *env_value;		/* envariable value */
344	int env_write;			/* env file descriptor */
345	char hname[MAXHOSTNAMELEN];	/* local hostname */
346	int no_link;			/* link per-agent file? */
347	char *per_agent;		/* to store env */
348	char *per_session;		/* per-session filename */
349	const struct passwd *pwent;	/* user's passwd entry */
350	int retval;			/* from calls */
351	uid_t saved_uid;		/* caller's uid */
352	int start_agent;		/* start agent? */
353	const char *tty;		/* tty or display name */
354
355	/* dump output of ssh-agent in ~/.ssh */
356	if ((retval = pam_get_data(pamh, "ssh_passwd_entry",
357	    (const void **)&pwent)) != PAM_SUCCESS)
358		return retval;
359
360	/*
361	 * Use reference counts to limit agents to one per user per host.
362	 *
363	 * Technique: Create an environment file containing
364	 * information about the agent.  Only one file is created, but
365	 * it may be given many names.  One name is given for the
366	 * agent itself, agent-<host>.  Another name is given for each
367	 * session, agent-<host>-<display> or agent-<host>-<tty>.  We
368	 * delete the per-session filename on session close, and when
369	 * the link count goes to unity on the per-agent file, we
370	 * delete the file and kill the agent.
371	 */
372
373	/* the per-agent file contains just the hostname */
374
375	(void) gethostname(hname, sizeof hname);
376	if (asprintf(&per_agent, "%s/.ssh/agent-%s", pwent->pw_dir, hname)
377	    == -1) {
378		openpam_log(PAM_LOG_ERROR, "%s: %m", MODULE_NAME);
379		return PAM_SERVICE_ERR;
380	}
381
382	/* save the per-agent filename in case we want to delete it on
383	   session close */
384
385	if ((retval = pam_set_data(pamh, "ssh_agent_env_agent", per_agent,
386	    ssh_cleanup)) != PAM_SUCCESS) {
387		free(per_agent);
388		return retval;
389	}
390
391	/* take on the user's privileges for writing files and starting the
392	   agent */
393
394	saved_uid = geteuid();
395	(void) seteuid(pwent->pw_uid);
396
397	/* Try to create the per-agent file or open it for reading if it
398	   exists.  If we can't do either, we won't try to link a
399	   per-session filename later.  Start the agent if we can't open
400	   the file for reading. */
401
402	env_write = no_link = 0;
403	env_read = NULL;
404	if ((env_write = open(per_agent, O_CREAT | O_EXCL | O_WRONLY,
405	    S_IRUSR)) < 0 && !(env_read = fopen(per_agent, "r")))
406		no_link = 1;
407	if (env_read) {
408		start_agent = 0;
409		(void) seteuid(saved_uid);
410	} else {
411		start_agent = 1;
412		env_read = popen(SSH_AGENT, "r");
413		(void) seteuid(saved_uid);
414		if (!env_read) {
415			openpam_log(PAM_LOG_ERROR, "%s: %s: %m", MODULE_NAME,
416			    SSH_AGENT);
417			if (env_write >= 0)
418				(void) close(env_write);
419			free(per_agent);
420			return PAM_SESSION_ERR;
421		}
422	}
423
424	/* save environment for application with pam_putenv() */
425
426	agent_socket = NULL;
427	while (fgets(env_string, sizeof env_string, env_read)) {
428
429		/* parse environment definitions */
430
431		if (env_write >= 0)
432			(void) write(env_write, env_string,
433			    strlen(env_string));
434		if (!(env_value = strchr(env_string, '=')) ||
435		    !(env_end = strchr(env_value, ';')))
436			continue;
437		*env_end = '\0';
438
439		/* pass to the application */
440
441		if (!((retval = pam_putenv(pamh, env_string)) ==
442		    PAM_SUCCESS)) {
443			if (start_agent)
444				(void) pclose(env_read);
445			else
446				(void) fclose(env_read);
447			if (env_write >= 0)
448				(void) close(env_write);
449			if (agent_socket)
450				free(agent_socket);
451			free(per_agent);
452			return PAM_SERVICE_ERR;
453		}
454
455		*env_value++ = '\0';
456
457		/* save the agent socket so we can connect to it and add
458		   the keys as well as the PID so we can kill the agent on
459		   session close. */
460
461		if (strcmp(&env_string[strlen(env_string) -
462		    strlen(ENV_SOCKET_SUFFIX)], ENV_SOCKET_SUFFIX) == 0 &&
463		    !(agent_socket = strdup(env_value))) {
464			openpam_log(PAM_LOG_ERROR, "%s: %m", MODULE_NAME);
465			if (start_agent)
466				(void) pclose(env_read);
467			else
468				(void) fclose(env_read);
469			if (env_write >= 0)
470				(void) close(env_write);
471			if (agent_socket)
472				free(agent_socket);
473			free(per_agent);
474			return PAM_SERVICE_ERR;
475		} else if (strcmp(&env_string[strlen(env_string) -
476		    strlen(ENV_PID_SUFFIX)], ENV_PID_SUFFIX) == 0 &&
477		    (retval = pam_set_data(pamh, "ssh_agent_pid",
478		    env_value, ssh_cleanup)) != PAM_SUCCESS) {
479			if (start_agent)
480				(void) pclose(env_read);
481			else
482				(void) fclose(env_read);
483			if (env_write >= 0)
484				(void) close(env_write);
485			if (agent_socket)
486				free(agent_socket);
487			free(per_agent);
488			return retval;
489		}
490
491	}
492	if (env_write >= 0)
493		(void) close(env_write);
494
495	if (start_agent) {
496		switch (retval = pclose(env_read)) {
497		case -1:
498			openpam_log(PAM_LOG_ERROR, "%s: %s: %m", MODULE_NAME,
499			    SSH_AGENT);
500			if (agent_socket)
501				free(agent_socket);
502			free(per_agent);
503			return PAM_SESSION_ERR;
504		case 0:
505			break;
506		case 127:
507			openpam_log(PAM_LOG_ERROR, "%s: cannot execute %s",
508			    MODULE_NAME, SSH_AGENT);
509			if (agent_socket)
510				free(agent_socket);
511			free(per_agent);
512			return PAM_SESSION_ERR;
513		default:
514			openpam_log(PAM_LOG_ERROR, "%s: %s exited %s %d",
515			    MODULE_NAME,
516			    SSH_AGENT, WIFSIGNALED(retval) ? "on signal" :
517			    "with status", WIFSIGNALED(retval) ?
518			    WTERMSIG(retval) : WEXITSTATUS(retval));
519			if (agent_socket)
520				free(agent_socket);
521			free(per_agent);
522			return PAM_SESSION_ERR;
523		}
524	} else
525		(void) fclose(env_read);
526
527	if (!agent_socket) {
528		free(per_agent);
529		return PAM_SESSION_ERR;
530	}
531
532	if (start_agent && (retval = add_keys(pamh, agent_socket))
533	    != PAM_SUCCESS) {
534		free(per_agent);
535		return retval;
536	}
537	free(agent_socket);
538
539	/* if we couldn't access the per-agent file, don't link a
540	   per-session filename to it */
541
542	if (no_link)
543		return PAM_SUCCESS;
544
545	/* the per-session file contains the display name or tty name as
546	   well as the hostname */
547
548	if ((retval = pam_get_item(pamh, PAM_TTY, (const void **)&tty))
549	    != PAM_SUCCESS) {
550		free(per_agent);
551		return retval;
552	}
553	if (asprintf(&per_session, "%s/.ssh/agent-%s-%s", pwent->pw_dir,
554	    hname, tty) == -1) {
555		openpam_log(PAM_LOG_ERROR, "%s: %m", MODULE_NAME);
556		free(per_agent);
557		return PAM_SERVICE_ERR;
558	}
559
560	/* save the per-session filename so we can delete it on session
561	   close */
562
563	if ((retval = pam_set_data(pamh, "ssh_agent_env_session",
564	    per_session, ssh_cleanup)) != PAM_SUCCESS) {
565		free(per_session);
566		free(per_agent);
567		return retval;
568	}
569
570	(void) unlink(per_session);		/* remove cruft */
571	(void) link(per_agent, per_session);
572	free(per_agent);
573	free(per_session);
574
575	return PAM_SUCCESS;
576}
577
578
579PAM_EXTERN int
580pam_sm_close_session(pam_handle_t *pamh, int flags, int argc,
581    const char **argv)
582{
583	const char *env_file;		/* ssh-agent environment */
584	pid_t pid;			/* ssh-agent process id */
585	int retval;			/* from calls */
586	const char *ssh_agent_pid;	/* ssh-agent pid string */
587	struct stat sb;			/* to check st_nlink */
588
589	if ((retval = pam_get_data(pamh, "ssh_agent_env_session",
590	    (const void **)&env_file)) == PAM_SUCCESS && env_file)
591		(void) unlink(env_file);
592
593	/* Retrieve per-agent filename and check link count.  If it's
594	   greater than unity, other sessions are still using this
595	   agent. */
596
597	if ((retval = pam_get_data(pamh, "ssh_agent_env_agent",
598	    (const void **)&env_file)) == PAM_SUCCESS && env_file &&
599	    stat(env_file, &sb) == 0) {
600		if (sb.st_nlink > 1)
601			return PAM_SUCCESS;
602		(void) unlink(env_file);
603	}
604
605	/* retrieve the agent's process id */
606
607	if ((retval = pam_get_data(pamh, "ssh_agent_pid",
608	    (const void **)&ssh_agent_pid)) != PAM_SUCCESS)
609		return retval;
610
611	/* Kill the agent.  SSH's ssh-agent does not have a -k option, so
612	   just call kill(). */
613
614	pid = atoi(ssh_agent_pid);
615	if (ssh_agent_pid <= 0)
616		return PAM_SESSION_ERR;
617	if (kill(pid, SIGTERM) != 0) {
618		openpam_log(PAM_LOG_ERROR, "%s: %s: %m", MODULE_NAME,
619		    ssh_agent_pid);
620		return PAM_SESSION_ERR;
621	}
622
623	return PAM_SUCCESS;
624}
625
626PAM_EXTERN int
627pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc,
628    const char **argv)
629{
630	return (PAM_IGNORE);
631}
632
633PAM_EXTERN int
634pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc,
635    const char **argv)
636{
637	return (PAM_IGNORE);
638}
639
640PAM_MODULE_ENTRY(MODULE_NAME);
641