153874Sgreen/*-
2110598Sdes * Copyright (c) 2003 Networks Associates Technology, Inc.
3226101Sdes * Copyright (c) 2004-2011 Dag-Erling Sm��rgrav
453874Sgreen * All rights reserved.
553874Sgreen *
6110598Sdes * This software was developed for the FreeBSD Project by ThinkSec AS and
7110598Sdes * NAI Labs, the Security Research Division of Network Associates, Inc.
8110598Sdes * under DARPA/SPAWAR contract N66001-01-C-8035 ("CBOSS"), as part of the
9110598Sdes * DARPA CHATS research program.
1087398Sdes *
1153874Sgreen * Redistribution and use in source and binary forms, with or without
1253874Sgreen * modification, are permitted provided that the following conditions
1353874Sgreen * are met:
1453874Sgreen * 1. Redistributions of source code must retain the above copyright
1553874Sgreen *    notice, this list of conditions and the following disclaimer.
1653874Sgreen * 2. Redistributions in binary form must reproduce the above copyright
1753874Sgreen *    notice, this list of conditions and the following disclaimer in the
1853874Sgreen *    documentation and/or other materials provided with the distribution.
19110598Sdes * 3. The name of the author may not be used to endorse or promote
20110598Sdes *    products derived from this software without specific prior written
21110598Sdes *    permission.
2253874Sgreen *
2353874Sgreen * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
2453874Sgreen * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
2553874Sgreen * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
2653874Sgreen * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
2753874Sgreen * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
2853874Sgreen * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
2953874Sgreen * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
3053874Sgreen * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
3153874Sgreen * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
3253874Sgreen * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
3353874Sgreen * SUCH DAMAGE.
3453874Sgreen */
3553874Sgreen
3684218Sdillon#include <sys/cdefs.h>
3784218Sdillon__FBSDID("$FreeBSD: stable/11/lib/libpam/modules/pam_ssh/pam_ssh.c 306308 2016-09-25 09:36:52Z roberto $");
3853874Sgreen
3953874Sgreen#include <sys/param.h>
4080542Smarkm#include <sys/wait.h>
4153874Sgreen
42120231Sdes#include <errno.h>
4393804Sdes#include <fcntl.h>
44110598Sdes#include <paths.h>
4553874Sgreen#include <pwd.h>
4680542Smarkm#include <signal.h>
4753874Sgreen#include <stdio.h>
4853874Sgreen#include <string.h>
4953874Sgreen#include <unistd.h>
5053874Sgreen
5193804Sdes#define PAM_SM_AUTH
5287398Sdes#define PAM_SM_SESSION
5387398Sdes
5490229Sdes#include <security/pam_appl.h>
5553874Sgreen#include <security/pam_modules.h>
5694216Sdes#include <security/openpam.h>
5753874Sgreen
5880542Smarkm#include <openssl/evp.h>
5961087Skris
60296651Sdes#define __bounded__(x, y, z)
6161087Skris#include "key.h"
62162900Sru#include "buffer.h"
6353874Sgreen#include "authfd.h"
6461087Skris#include "authfile.h"
6553874Sgreen
66204917Sdes#define ssh_add_identity(auth, key, comment) \
67204917Sdes	ssh_add_identity_constrained(auth, key, comment, 0, 0)
68204917Sdes
69110598Sdesextern char **environ;
7093984Sdes
71110598Sdesstruct pam_ssh_key {
72110598Sdes	Key	*key;
73110598Sdes	char	*comment;
74110598Sdes};
7553874Sgreen
76110598Sdesstatic const char *pam_ssh_prompt = "SSH passphrase: ";
77110598Sdesstatic const char *pam_ssh_have_keys = "pam_ssh_have_keys";
7853874Sgreen
79110598Sdesstatic const char *pam_ssh_keyfiles[] = {
80110598Sdes	".ssh/id_rsa",		/* SSH2 RSA key */
81110598Sdes	".ssh/id_dsa",		/* SSH2 DSA key */
82226101Sdes	".ssh/id_ecdsa",	/* SSH2 ECDSA key */
83306308Sroberto	".ssh/id_ed25519",	/* SSH2 Ed25519 key */
84110598Sdes	NULL
85110598Sdes};
8653874Sgreen
87110598Sdesstatic const char *pam_ssh_agent = "/usr/bin/ssh-agent";
88296651Sdesstatic char str_ssh_agent[] = "ssh-agent";
89296651Sdesstatic char str_dash_s[] = "-s";
90296651Sdesstatic char *const pam_ssh_agent_argv[] = { str_ssh_agent, str_dash_s, NULL };
91125650Sdesstatic char *const pam_ssh_agent_envp[] = { NULL };
9280542Smarkm
9355166Sgreen/*
94110598Sdes * Attempts to load a private key from the specified file in the specified
95110598Sdes * directory, using the specified passphrase.  If successful, returns a
96110598Sdes * struct pam_ssh_key containing the key and its comment.
9755166Sgreen */
98110598Sdesstatic struct pam_ssh_key *
99227757Sdespam_ssh_load_key(const char *dir, const char *kfn, const char *passphrase,
100227757Sdes    int nullok)
10155166Sgreen{
102110598Sdes	struct pam_ssh_key *psk;
103110598Sdes	char fn[PATH_MAX];
104110598Sdes	char *comment;
105110598Sdes	Key *key;
10655166Sgreen
107110598Sdes	if (snprintf(fn, sizeof(fn), "%s/%s", dir, kfn) > (int)sizeof(fn))
108110598Sdes		return (NULL);
10993804Sdes	comment = NULL;
110227757Sdes	/*
111227757Sdes	 * If the key is unencrypted, OpenSSL ignores the passphrase, so
112227757Sdes	 * it will seem like the user typed in the right one.  This allows
113227757Sdes	 * a user to circumvent nullok by providing a dummy passphrase.
114227757Sdes	 * Verify that the key really *is* encrypted by trying to load it
115227757Sdes	 * with an empty passphrase, and if the key is not encrypted,
116227757Sdes	 * accept only an empty passphrase.
117227757Sdes	 */
118236106Sdes	key = key_load_private(fn, "", &comment);
119227757Sdes	if (key != NULL && !(*passphrase == '\0' && nullok)) {
120227757Sdes		key_free(key);
121227757Sdes		return (NULL);
122227757Sdes	}
123227757Sdes	if (key == NULL)
124227757Sdes		key = key_load_private(fn, passphrase, &comment);
125110598Sdes	if (key == NULL) {
126219426Sdes		openpam_log(PAM_LOG_DEBUG, "failed to load key from %s", fn);
127110598Sdes		return (NULL);
12893804Sdes	}
12993804Sdes
130219426Sdes	openpam_log(PAM_LOG_DEBUG, "loaded '%s' from %s", comment, fn);
131110598Sdes	if ((psk = malloc(sizeof(*psk))) == NULL) {
13280542Smarkm		key_free(key);
13380542Smarkm		free(comment);
134110598Sdes		return (NULL);
13580542Smarkm	}
136110598Sdes	psk->key = key;
137110598Sdes	psk->comment = comment;
138110598Sdes	return (psk);
13955166Sgreen}
14055166Sgreen
14193804Sdes/*
142110598Sdes * Wipes a private key and frees the associated resources.
14393804Sdes */
144110598Sdesstatic void
145110598Sdespam_ssh_free_key(pam_handle_t *pamh __unused,
146110598Sdes    void *data, int pam_err __unused)
14755166Sgreen{
148110598Sdes	struct pam_ssh_key *psk;
14955166Sgreen
150110598Sdes	psk = data;
151110598Sdes	key_free(psk->key);
152110598Sdes	free(psk->comment);
153110598Sdes	free(psk);
15453874Sgreen}
15553874Sgreen
15653874SgreenPAM_EXTERN int
15793984Sdespam_sm_authenticate(pam_handle_t *pamh, int flags __unused,
15894564Sdes    int argc __unused, const char *argv[] __unused)
15953874Sgreen{
160110598Sdes	const char **kfn, *passphrase, *user;
161150596Sdes	const void *item;
162110598Sdes	struct passwd *pwd;
163110598Sdes	struct pam_ssh_key *psk;
164150455Sdes	int nkeys, nullok, pam_err, pass;
16580542Smarkm
166150455Sdes	nullok = (openpam_get_option(pamh, "nullok") != NULL);
167150455Sdes
168110598Sdes	/* PEM is not loaded by default */
169110598Sdes	OpenSSL_add_all_algorithms();
170107934Sdes
171110598Sdes	/* get user name and home directory */
172110653Sdes	pam_err = pam_get_user(pamh, &user, NULL);
173110598Sdes	if (pam_err != PAM_SUCCESS)
174110598Sdes		return (pam_err);
175110598Sdes	pwd = getpwnam(user);
176110598Sdes	if (pwd == NULL)
177110598Sdes		return (PAM_USER_UNKNOWN);
178110598Sdes	if (pwd->pw_dir == NULL)
17994564Sdes		return (PAM_AUTH_ERR);
18053874Sgreen
181150455Sdes	nkeys = 0;
182150596Sdes	pass = (pam_get_item(pamh, PAM_AUTHTOK, &item) == PAM_SUCCESS &&
183150596Sdes	    item != NULL);
184110598Sdes load_keys:
185110598Sdes	/* get passphrase */
186110598Sdes	pam_err = pam_get_authtok(pamh, PAM_AUTHTOK,
187110598Sdes	    &passphrase, pam_ssh_prompt);
188150426Sdes	if (pam_err != PAM_SUCCESS)
189110598Sdes		return (pam_err);
19087398Sdes
191150426Sdes	/* switch to user credentials */
192150426Sdes	pam_err = openpam_borrow_cred(pamh, pwd);
193150426Sdes	if (pam_err != PAM_SUCCESS)
194150426Sdes		return (pam_err);
195150426Sdes
196110598Sdes	/* try to load keys from all keyfiles we know of */
197110598Sdes	for (kfn = pam_ssh_keyfiles; *kfn != NULL; ++kfn) {
198227757Sdes		psk = pam_ssh_load_key(pwd->pw_dir, *kfn, passphrase, nullok);
199110598Sdes		if (psk != NULL) {
200110598Sdes			pam_set_data(pamh, *kfn, psk, pam_ssh_free_key);
201110598Sdes			++nkeys;
202110598Sdes		}
203110598Sdes	}
20487398Sdes
205150426Sdes	/* switch back to arbitrator credentials */
206150426Sdes	openpam_restore_cred(pamh);
207150426Sdes
208110598Sdes	/*
209110598Sdes	 * If we tried an old token and didn't get anything, and
210110598Sdes	 * try_first_pass was specified, try again after prompting the
211110598Sdes	 * user for a new passphrase.
212110598Sdes	 */
213110598Sdes	if (nkeys == 0 && pass == 1 &&
214110598Sdes	    openpam_get_option(pamh, "try_first_pass") != NULL) {
215110598Sdes		pam_set_item(pamh, PAM_AUTHTOK, NULL);
216110598Sdes		pass = 0;
217110598Sdes		goto load_keys;
21893804Sdes	}
21987398Sdes
220110598Sdes	/* no keys? */
221110598Sdes	if (nkeys == 0)
222110598Sdes		return (PAM_AUTH_ERR);
22387398Sdes
224110598Sdes	pam_set_data(pamh, pam_ssh_have_keys, NULL, NULL);
22594564Sdes	return (PAM_SUCCESS);
22687398Sdes}
22787398Sdes
22853874SgreenPAM_EXTERN int
22993984Sdespam_sm_setcred(pam_handle_t *pamh __unused, int flags __unused,
23094564Sdes    int argc __unused, const char *argv[] __unused)
23153874Sgreen{
232110598Sdes
23394564Sdes	return (PAM_SUCCESS);
23493804Sdes}
23553874Sgreen
236110598Sdes/*
237110598Sdes * Parses a line from ssh-agent's output.
238110598Sdes */
239110598Sdesstatic void
240110598Sdespam_ssh_process_agent_output(pam_handle_t *pamh, FILE *f)
24193804Sdes{
242110598Sdes	char *line, *p, *key, *val;
243110598Sdes	size_t len;
24480542Smarkm
245110598Sdes	while ((line = fgetln(f, &len)) != NULL) {
246110598Sdes		if (len < 4 || strncmp(line, "SSH_", 4) != 0)
247110598Sdes			continue;
248107934Sdes
249110598Sdes		/* find equal sign at end of key */
250110598Sdes		for (p = key = line; p < line + len; ++p)
251110598Sdes			if (*p == '=')
252110598Sdes				break;
253110598Sdes		if (p == line + len || *p != '=')
254110598Sdes			continue;
255110598Sdes		*p = '\0';
25680542Smarkm
257110598Sdes		/* find semicolon at end of value */
258110598Sdes		for (val = ++p; p < line + len; ++p)
259110598Sdes			if (*p == ';')
260110598Sdes				break;
261110598Sdes		if (p == line + len || *p != ';')
262110598Sdes			continue;
263110598Sdes		*p = '\0';
26480542Smarkm
265110598Sdes		/* store key-value pair in environment */
266110598Sdes		openpam_log(PAM_LOG_DEBUG, "got %s: %s", key, val);
267110598Sdes		pam_setenv(pamh, key, val, 1);
26880542Smarkm	}
269110598Sdes}
27080542Smarkm
271110598Sdes/*
272110598Sdes * Starts an ssh agent and stores the environment variables derived from
273110598Sdes * its output.
274110598Sdes */
275110598Sdesstatic int
276110598Sdespam_ssh_start_agent(pam_handle_t *pamh)
277110598Sdes{
278110598Sdes	int agent_pipe[2];
279110598Sdes	pid_t pid;
280110598Sdes	FILE *f;
28180542Smarkm
282110598Sdes	/* get a pipe which we will use to read the agent's output */
283150426Sdes	if (pipe(agent_pipe) == -1)
284110598Sdes		return (PAM_SYSTEM_ERR);
28580542Smarkm
286110598Sdes	/* start the agent */
287110598Sdes	openpam_log(PAM_LOG_DEBUG, "starting an ssh agent");
288110598Sdes	pid = fork();
289110598Sdes	if (pid == (pid_t)-1) {
290110598Sdes		/* failed */
291110598Sdes		close(agent_pipe[0]);
292110598Sdes		close(agent_pipe[1]);
293110598Sdes		return (PAM_SYSTEM_ERR);
294110598Sdes	}
295110598Sdes	if (pid == 0) {
296110598Sdes		int fd;
29780542Smarkm
298110598Sdes		/* child: drop privs, close fds and start agent */
299110598Sdes		setgid(getegid());
300110598Sdes		setuid(geteuid());
301110598Sdes		close(STDIN_FILENO);
302110598Sdes		open(_PATH_DEVNULL, O_RDONLY);
303110598Sdes		dup2(agent_pipe[1], STDOUT_FILENO);
304110598Sdes		dup2(agent_pipe[1], STDERR_FILENO);
305110598Sdes		for (fd = 3; fd < getdtablesize(); ++fd)
306110598Sdes			close(fd);
307110598Sdes		execve(pam_ssh_agent, pam_ssh_agent_argv, pam_ssh_agent_envp);
308110598Sdes		_exit(127);
30953874Sgreen	}
31080542Smarkm
311110598Sdes	/* parent */
312110598Sdes	close(agent_pipe[1]);
313110598Sdes	if ((f = fdopen(agent_pipe[0], "r")) == NULL)
314110598Sdes		return (PAM_SYSTEM_ERR);
315110598Sdes	pam_ssh_process_agent_output(pamh, f);
316110598Sdes	fclose(f);
31780542Smarkm
318110598Sdes	return (PAM_SUCCESS);
319110598Sdes}
32093804Sdes
321110598Sdes/*
322110598Sdes * Adds previously stored keys to a running agent.
323110598Sdes */
324110598Sdesstatic int
325110598Sdespam_ssh_add_keys_to_agent(pam_handle_t *pamh)
326110598Sdes{
327174837Sdes	const struct pam_ssh_key *psk;
328110598Sdes	const char **kfn;
329174837Sdes	const void *item;
330110598Sdes	char **envlist, **env;
331294367Sjhb	int fd, pam_err;
33293804Sdes
333110598Sdes	/* switch to PAM environment */
334110598Sdes	envlist = environ;
335110598Sdes	if ((environ = pam_getenvlist(pamh)) == NULL) {
336110598Sdes		environ = envlist;
337110598Sdes		return (PAM_SYSTEM_ERR);
338110598Sdes	}
33993804Sdes
340110598Sdes	/* get a connection to the agent */
341294367Sjhb	if (ssh_get_authentication_socket(&fd) != 0) {
342226101Sdes		openpam_log(PAM_LOG_DEBUG, "failed to connect to the agent");
343110598Sdes		pam_err = PAM_SYSTEM_ERR;
344110598Sdes		goto end;
345110598Sdes	}
34693804Sdes
347110598Sdes	/* look for keys to add to it */
348110598Sdes	for (kfn = pam_ssh_keyfiles; *kfn != NULL; ++kfn) {
349150596Sdes		pam_err = pam_get_data(pamh, *kfn, &item);
350150596Sdes		if (pam_err == PAM_SUCCESS && item != NULL) {
351150596Sdes			psk = item;
352294367Sjhb			if (ssh_add_identity(fd, psk->key, psk->comment) == 0)
353110598Sdes				openpam_log(PAM_LOG_DEBUG,
354110598Sdes				    "added %s to ssh agent", psk->comment);
35593804Sdes			else
356110598Sdes				openpam_log(PAM_LOG_DEBUG, "failed "
357110598Sdes				    "to add %s to ssh agent", psk->comment);
358110598Sdes			/* we won't need the key again, so wipe it */
359110598Sdes			pam_set_data(pamh, *kfn, NULL, NULL);
36080542Smarkm		}
36193804Sdes	}
362110598Sdes	pam_err = PAM_SUCCESS;
363294367Sjhb
364110598Sdes	/* disconnect from agent */
365294367Sjhb	ssh_close_authentication_socket(fd);
36693804Sdes
367294367Sjhb end:
368110598Sdes	/* switch back to original environment */
369110598Sdes	for (env = environ; *env != NULL; ++env)
370110598Sdes		free(*env);
371110598Sdes	free(environ);
372110598Sdes	environ = envlist;
37393804Sdes
374110598Sdes	return (pam_err);
375110598Sdes}
37693804Sdes
377110598SdesPAM_EXTERN int
378110598Sdespam_sm_open_session(pam_handle_t *pamh, int flags __unused,
379110598Sdes    int argc __unused, const char *argv[] __unused)
380110598Sdes{
381110598Sdes	struct passwd *pwd;
382110598Sdes	const char *user;
383174837Sdes	const void *data;
384110598Sdes	int pam_err;
38580542Smarkm
386110598Sdes	/* no keys, no work */
387110598Sdes	if (pam_get_data(pamh, pam_ssh_have_keys, &data) != PAM_SUCCESS &&
388110598Sdes	    openpam_get_option(pamh, "want_agent") == NULL)
38994564Sdes		return (PAM_SUCCESS);
39093804Sdes
391110598Sdes	/* switch to user credentials */
392110653Sdes	pam_err = pam_get_user(pamh, &user, NULL);
393110598Sdes	if (pam_err != PAM_SUCCESS)
394110598Sdes		return (pam_err);
395110598Sdes	pwd = getpwnam(user);
396110598Sdes	if (pwd == NULL)
397110598Sdes		return (PAM_USER_UNKNOWN);
398110598Sdes	pam_err = openpam_borrow_cred(pamh, pwd);
399110598Sdes	if (pam_err != PAM_SUCCESS)
400110598Sdes		return (pam_err);
40193804Sdes
402110598Sdes	/* start the agent */
403110598Sdes	pam_err = pam_ssh_start_agent(pamh);
404110598Sdes	if (pam_err != PAM_SUCCESS) {
405110598Sdes		openpam_restore_cred(pamh);
406110598Sdes		return (pam_err);
40755166Sgreen	}
40880542Smarkm
409110598Sdes	/* we have an agent, see if we can add any keys to it */
410110598Sdes	pam_err = pam_ssh_add_keys_to_agent(pamh);
411110598Sdes	if (pam_err != PAM_SUCCESS) {
412110598Sdes		/* XXX ignore failures */
41353874Sgreen	}
41480542Smarkm
415110598Sdes	openpam_restore_cred(pamh);
41694564Sdes	return (PAM_SUCCESS);
41753874Sgreen}
41853874Sgreen
41953874SgreenPAM_EXTERN int
42093984Sdespam_sm_close_session(pam_handle_t *pamh, int flags __unused,
42194564Sdes    int argc __unused, const char *argv[] __unused)
42253874Sgreen{
423110598Sdes	const char *ssh_agent_pid;
424110598Sdes	char *end;
425110598Sdes	int status;
426110598Sdes	pid_t pid;
42753874Sgreen
428110598Sdes	if ((ssh_agent_pid = pam_getenv(pamh, "SSH_AGENT_PID")) == NULL) {
429110598Sdes		openpam_log(PAM_LOG_DEBUG, "no ssh agent");
430110598Sdes		return (PAM_SUCCESS);
43193804Sdes	}
432110598Sdes	pid = (pid_t)strtol(ssh_agent_pid, &end, 10);
433110598Sdes	if (*ssh_agent_pid == '\0' || *end != '\0') {
434110598Sdes		openpam_log(PAM_LOG_DEBUG, "invalid ssh agent pid");
43594564Sdes		return (PAM_SESSION_ERR);
43653874Sgreen	}
437110598Sdes	openpam_log(PAM_LOG_DEBUG, "killing ssh agent %d", (int)pid);
438110598Sdes	if (kill(pid, SIGTERM) == -1 ||
439120231Sdes	    (waitpid(pid, &status, 0) == -1 && errno != ECHILD))
440110598Sdes		return (PAM_SYSTEM_ERR);
44194564Sdes	return (PAM_SUCCESS);
44293804Sdes}
44380542Smarkm
444110598SdesPAM_MODULE_ENTRY("pam_ssh");
445