Annotation of src/lib/libpam/modules/pam_ssh/pam_ssh.c, Revision 1.25
1.25 ! christos 1: /* $NetBSD: pam_ssh.c,v 1.24 2018/04/07 13:57:12 christos Exp $ */
1.2 christos 2:
1.1 christos 3: /*-
4: * Copyright (c) 2003 Networks Associates Technology, Inc.
5: * All rights reserved.
6: *
7: * This software was developed for the FreeBSD Project by ThinkSec AS and
8: * NAI Labs, the Security Research Division of Network Associates, Inc.
9: * under DARPA/SPAWAR contract N66001-01-C-8035 ("CBOSS"), as part of the
10: * 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:
37: #include <sys/cdefs.h>
1.3 lukem 38: #ifdef __FreeBSD__
1.1 christos 39: __FBSDID("$FreeBSD: src/lib/libpam/modules/pam_ssh/pam_ssh.c,v 1.40 2004/02/10 10:13:21 des Exp $");
1.2 christos 40: #else
1.25 ! christos 41: __RCSID("$NetBSD: pam_ssh.c,v 1.24 2018/04/07 13:57:12 christos Exp $");
1.2 christos 42: #endif
1.1 christos 43:
44: #include <sys/param.h>
45: #include <sys/wait.h>
46:
47: #include <errno.h>
48: #include <fcntl.h>
49: #include <paths.h>
50: #include <pwd.h>
51: #include <signal.h>
52: #include <stdio.h>
53: #include <string.h>
54: #include <unistd.h>
55:
56: #define PAM_SM_AUTH
57: #define PAM_SM_SESSION
58:
59: #include <security/pam_appl.h>
60: #include <security/pam_modules.h>
61: #include <security/openpam.h>
62:
63: #include <openssl/evp.h>
64:
65: #include "key.h"
1.13 dogcow 66: #include "buffer.h"
1.1 christos 67: #include "authfd.h"
68: #include "authfile.h"
69:
1.18 drochner 70: #define ssh_add_identity(auth, key, comment) \
1.25 ! christos 71: ssh_add_identity_constrained(auth, key, comment, 0, 0, 0)
1.18 drochner 72:
1.1 christos 73: extern char **environ;
74:
75: struct pam_ssh_key {
76: Key *key;
77: char *comment;
78: };
79:
80: static const char *pam_ssh_prompt = "SSH passphrase: ";
81: static const char *pam_ssh_have_keys = "pam_ssh_have_keys";
82:
83: static const char *pam_ssh_keyfiles[] = {
84: ".ssh/identity", /* SSH1 RSA key */
85: ".ssh/id_rsa", /* SSH2 RSA key */
86: ".ssh/id_dsa", /* SSH2 DSA key */
1.20 drochner 87: ".ssh/id_ecdsa", /* SSH2 ECDSA key */
1.1 christos 88: NULL
89: };
90:
91: static const char *pam_ssh_agent = "/usr/bin/ssh-agent";
1.18 drochner 92: static const char *const pam_ssh_agent_argv[] = { "ssh_agent", "-s", NULL };
93: static const char *const pam_ssh_agent_envp[] = { NULL };
1.1 christos 94:
95: /*
96: * Attempts to load a private key from the specified file in the specified
97: * directory, using the specified passphrase. If successful, returns a
98: * struct pam_ssh_key containing the key and its comment.
99: */
100: static struct pam_ssh_key *
1.19 drochner 101: pam_ssh_load_key(const char *dir, const char *kfn, const char *passphrase,
102: int nullok)
1.1 christos 103: {
104: struct pam_ssh_key *psk;
105: char fn[PATH_MAX];
106: char *comment;
107: Key *key;
108:
1.18 drochner 109: if (snprintf(fn, sizeof(fn), "%s/%s", dir, kfn) > (int)sizeof(fn))
1.1 christos 110: return (NULL);
111: comment = NULL;
1.19 drochner 112: /*
113: * If the key is unencrypted, OpenSSL ignores the passphrase, so
114: * it will seem like the user typed in the right one. This allows
115: * a user to circumvent nullok by providing a dummy passphrase.
116: * Verify that the key really *is* encrypted by trying to load it
117: * with an empty passphrase, and if the key is not encrypted,
118: * accept only an empty passphrase.
119: */
120: key = key_load_private(fn, "", &comment);
121: if (key != NULL && !(*passphrase == '\0' && nullok)) {
122: key_free(key);
123: free(comment);
124: return (NULL);
125: }
126: if (key == NULL)
127: key = key_load_private(fn, passphrase, &comment);
1.1 christos 128: if (key == NULL) {
1.17 drochner 129: openpam_log(PAM_LOG_DEBUG, "failed to load key from %s", fn);
1.12 jnemeth 130: if (comment != NULL)
131: free(comment);
1.1 christos 132: return (NULL);
133: }
134:
1.17 drochner 135: openpam_log(PAM_LOG_DEBUG, "loaded '%s' from %s", comment, fn);
1.1 christos 136: if ((psk = malloc(sizeof(*psk))) == NULL) {
137: key_free(key);
138: free(comment);
139: return (NULL);
140: }
141: psk->key = key;
142: psk->comment = comment;
143: return (psk);
144: }
145:
146: /*
147: * Wipes a private key and frees the associated resources.
148: */
149: static void
150: pam_ssh_free_key(pam_handle_t *pamh __unused,
151: void *data, int pam_err __unused)
152: {
153: struct pam_ssh_key *psk;
154:
155: psk = data;
156: key_free(psk->key);
157: free(psk->comment);
158: free(psk);
159: }
160:
161: PAM_EXTERN int
162: pam_sm_authenticate(pam_handle_t *pamh, int flags __unused,
163: int argc __unused, const char *argv[] __unused)
164: {
165: const char **kfn, *passphrase, *user;
1.18 drochner 166: const void *item;
1.10 thorpej 167: struct passwd *pwd, pwres;
1.1 christos 168: struct pam_ssh_key *psk;
1.19 drochner 169: int nkeys, nullok, pam_err, pass;
1.10 thorpej 170: char pwbuf[1024];
1.1 christos 171:
1.19 drochner 172: nullok = (openpam_get_option(pamh, "nullok") != NULL);
173:
1.1 christos 174: /* PEM is not loaded by default */
175: OpenSSL_add_all_algorithms();
176:
177: /* get user name and home directory */
178: pam_err = pam_get_user(pamh, &user, NULL);
179: if (pam_err != PAM_SUCCESS)
180: return (pam_err);
1.11 christos 181: if (getpwnam_r(user, &pwres, pwbuf, sizeof(pwbuf), &pwd) != 0 ||
182: pwd == NULL)
1.1 christos 183: return (PAM_USER_UNKNOWN);
184: if (pwd->pw_dir == NULL)
185: return (PAM_AUTH_ERR);
186:
1.19 drochner 187: nkeys = 0;
1.18 drochner 188: pass = (pam_get_item(pamh, PAM_AUTHTOK, &item) == PAM_SUCCESS &&
189: item != NULL);
1.1 christos 190: load_keys:
191: /* get passphrase */
192: pam_err = pam_get_authtok(pamh, PAM_AUTHTOK,
193: &passphrase, pam_ssh_prompt);
1.22 drochner 194: if (pam_err != PAM_SUCCESS)
195: return (pam_err);
196:
197: /* switch to user credentials */
198: pam_err = openpam_borrow_cred(pamh, pwd);
199: if (pam_err != PAM_SUCCESS)
1.1 christos 200: return (pam_err);
201:
202: /* try to load keys from all keyfiles we know of */
203: for (kfn = pam_ssh_keyfiles; *kfn != NULL; ++kfn) {
1.19 drochner 204: psk = pam_ssh_load_key(pwd->pw_dir, *kfn, passphrase, nullok);
1.1 christos 205: if (psk != NULL) {
206: pam_set_data(pamh, *kfn, psk, pam_ssh_free_key);
207: ++nkeys;
208: }
209: }
210:
1.22 drochner 211: /* switch back to arbitrator credentials */
212: openpam_restore_cred(pamh);
213:
1.1 christos 214: /*
215: * If we tried an old token and didn't get anything, and
216: * try_first_pass was specified, try again after prompting the
217: * user for a new passphrase.
218: */
219: if (nkeys == 0 && pass == 1 &&
220: openpam_get_option(pamh, "try_first_pass") != NULL) {
221: pam_set_item(pamh, PAM_AUTHTOK, NULL);
222: pass = 0;
223: goto load_keys;
224: }
225:
226: /* no keys? */
227: if (nkeys == 0)
228: return (PAM_AUTH_ERR);
229:
230: pam_set_data(pamh, pam_ssh_have_keys, NULL, NULL);
231: return (PAM_SUCCESS);
232: }
233:
234: PAM_EXTERN int
235: pam_sm_setcred(pam_handle_t *pamh __unused, int flags __unused,
236: int argc __unused, const char *argv[] __unused)
237: {
238:
239: return (PAM_SUCCESS);
240: }
241:
242: /*
243: * Parses a line from ssh-agent's output.
244: */
245: static void
246: pam_ssh_process_agent_output(pam_handle_t *pamh, FILE *f)
247: {
248: char *line, *p, *key, *val;
249: size_t len;
250:
251: while ((line = fgetln(f, &len)) != NULL) {
252: if (len < 4 || strncmp(line, "SSH_", 4) != 0)
253: continue;
254:
255: /* find equal sign at end of key */
256: for (p = key = line; p < line + len; ++p)
257: if (*p == '=')
258: break;
259: if (p == line + len || *p != '=')
260: continue;
261: *p = '\0';
262:
263: /* find semicolon at end of value */
264: for (val = ++p; p < line + len; ++p)
265: if (*p == ';')
266: break;
267: if (p == line + len || *p != ';')
268: continue;
269: *p = '\0';
270:
271: /* store key-value pair in environment */
272: openpam_log(PAM_LOG_DEBUG, "got %s: %s", key, val);
273: pam_setenv(pamh, key, val, 1);
274: }
275: }
276:
277: /*
278: * Starts an ssh agent and stores the environment variables derived from
279: * its output.
280: */
281: static int
1.4 christos 282: pam_ssh_start_agent(pam_handle_t *pamh, struct passwd *pwd)
1.1 christos 283: {
284: int agent_pipe[2];
285: pid_t pid;
286: FILE *f;
287:
288: /* get a pipe which we will use to read the agent's output */
1.4 christos 289: if (pipe(agent_pipe) == -1)
1.1 christos 290: return (PAM_SYSTEM_ERR);
291:
292: /* start the agent */
293: openpam_log(PAM_LOG_DEBUG, "starting an ssh agent");
294: pid = fork();
295: if (pid == (pid_t)-1) {
296: /* failed */
297: close(agent_pipe[0]);
298: close(agent_pipe[1]);
299: return (PAM_SYSTEM_ERR);
300: }
301: if (pid == 0) {
1.2 christos 302: #ifndef F_CLOSEM
1.1 christos 303: int fd;
1.2 christos 304: #endif
1.1 christos 305: /* child: drop privs, close fds and start agent */
1.4 christos 306: if (setgid(pwd->pw_gid) == -1) {
1.21 christos 307: openpam_log(PAM_LOG_DEBUG, "%s: Cannot setgid %d (%s)",
308: __func__, (int)pwd->pw_gid, strerror(errno));
1.4 christos 309: goto done;
310: }
311: if (initgroups(pwd->pw_name, pwd->pw_gid) == -1) {
312: openpam_log(PAM_LOG_DEBUG,
1.21 christos 313: "%s: Cannot initgroups for %s (%s)",
314: __func__, pwd->pw_name, strerror(errno));
1.4 christos 315: goto done;
316: }
317: if (setuid(pwd->pw_uid) == -1) {
1.21 christos 318: openpam_log(PAM_LOG_DEBUG, "%s: Cannot setuid %d (%s)",
319: __func__, (int)pwd->pw_uid, strerror(errno));
1.4 christos 320: goto done;
321: }
322: (void)close(STDIN_FILENO);
323: (void)open(_PATH_DEVNULL, O_RDONLY);
324: (void)dup2(agent_pipe[1], STDOUT_FILENO);
325: (void)dup2(agent_pipe[1], STDERR_FILENO);
1.2 christos 326: #ifdef F_CLOSEM
327: (void)fcntl(3, F_CLOSEM, 0);
328: #else
1.1 christos 329: for (fd = 3; fd < getdtablesize(); ++fd)
1.4 christos 330: (void)close(fd);
1.2 christos 331: #endif
1.4 christos 332: (void)execve(pam_ssh_agent,
333: (char **)__UNCONST(pam_ssh_agent_argv),
1.2 christos 334: (char **)__UNCONST(pam_ssh_agent_envp));
1.4 christos 335: done:
1.1 christos 336: _exit(127);
337: }
338:
339: /* parent */
340: close(agent_pipe[1]);
341: if ((f = fdopen(agent_pipe[0], "r")) == NULL)
342: return (PAM_SYSTEM_ERR);
343: pam_ssh_process_agent_output(pamh, f);
344: fclose(f);
345:
346: return (PAM_SUCCESS);
347: }
348:
349: /*
350: * Adds previously stored keys to a running agent.
351: */
352: static int
353: pam_ssh_add_keys_to_agent(pam_handle_t *pamh)
354: {
1.15 christos 355: const struct pam_ssh_key *psk;
1.1 christos 356: const char **kfn;
357: char **envlist, **env;
358: int pam_err;
1.23 christos 359: int agent_fd;
1.1 christos 360:
361: /* switch to PAM environment */
362: envlist = environ;
363: if ((environ = pam_getenvlist(pamh)) == NULL) {
1.4 christos 364: openpam_log(PAM_LOG_DEBUG, "%s: cannot get envlist",
1.14 ragge 365: __func__);
1.1 christos 366: environ = envlist;
367: return (PAM_SYSTEM_ERR);
368: }
369:
370: /* get a connection to the agent */
1.23 christos 371: if (ssh_get_authentication_socket(&agent_fd) != 0) {
1.4 christos 372: openpam_log(PAM_LOG_DEBUG,
373: "%s: cannot get authentication connection",
1.14 ragge 374: __func__);
1.1 christos 375: pam_err = PAM_SYSTEM_ERR;
1.23 christos 376: agent_fd = -1;
1.1 christos 377: goto end;
378: }
379:
380: /* look for keys to add to it */
381: for (kfn = pam_ssh_keyfiles; *kfn != NULL; ++kfn) {
1.15 christos 382: const void *vp;
383: pam_err = pam_get_data(pamh, *kfn, &vp);
384: psk = vp;
1.1 christos 385: if (pam_err == PAM_SUCCESS && psk != NULL) {
1.25 ! christos 386: if (ssh_add_identity(agent_fd, psk->key, psk->comment))
1.1 christos 387: openpam_log(PAM_LOG_DEBUG,
388: "added %s to ssh agent", psk->comment);
389: else
390: openpam_log(PAM_LOG_DEBUG, "failed "
391: "to add %s to ssh agent", psk->comment);
392: /* we won't need the key again, so wipe it */
393: pam_set_data(pamh, *kfn, NULL, NULL);
394: }
395: }
396: pam_err = PAM_SUCCESS;
397: end:
398: /* disconnect from agent */
1.23 christos 399: if (agent_fd != -1)
400: ssh_close_authentication_socket(agent_fd);
1.1 christos 401:
402: /* switch back to original environment */
403: for (env = environ; *env != NULL; ++env)
404: free(*env);
405: free(environ);
406: environ = envlist;
407:
408: return (pam_err);
409: }
410:
411: PAM_EXTERN int
412: pam_sm_open_session(pam_handle_t *pamh, int flags __unused,
413: int argc __unused, const char *argv[] __unused)
414: {
1.10 thorpej 415: struct passwd *pwd, pwres;
1.1 christos 416: const char *user;
1.15 christos 417: const void *data;
1.4 christos 418: int pam_err = PAM_SUCCESS;
1.10 thorpej 419: char pwbuf[1024];
1.1 christos 420:
421: /* no keys, no work */
422: if (pam_get_data(pamh, pam_ssh_have_keys, &data) != PAM_SUCCESS &&
423: openpam_get_option(pamh, "want_agent") == NULL)
424: return (PAM_SUCCESS);
425:
426: /* switch to user credentials */
427: pam_err = pam_get_user(pamh, &user, NULL);
428: if (pam_err != PAM_SUCCESS)
429: return (pam_err);
1.11 christos 430: if (getpwnam_r(user, &pwres, pwbuf, sizeof(pwbuf), &pwd) != 0 ||
431: pwd == NULL)
1.1 christos 432: return (PAM_USER_UNKNOWN);
1.4 christos 433:
434: /* start the agent */
435: pam_err = pam_ssh_start_agent(pamh, pwd);
436: if (pam_err != PAM_SUCCESS)
437: return pam_err;
438:
1.1 christos 439: pam_err = openpam_borrow_cred(pamh, pwd);
440: if (pam_err != PAM_SUCCESS)
1.4 christos 441: return pam_err;
1.1 christos 442:
443: /* we have an agent, see if we can add any keys to it */
444: pam_err = pam_ssh_add_keys_to_agent(pamh);
445: if (pam_err != PAM_SUCCESS) {
446: /* XXX ignore failures */
1.4 christos 447: openpam_log(PAM_LOG_DEBUG, "failed adding keys to ssh agent");
448: pam_err = PAM_SUCCESS;
1.1 christos 449: }
450:
451: openpam_restore_cred(pamh);
1.4 christos 452: return pam_err;
1.1 christos 453: }
454:
455: PAM_EXTERN int
456: pam_sm_close_session(pam_handle_t *pamh, int flags __unused,
457: int argc __unused, const char *argv[] __unused)
458: {
459: const char *ssh_agent_pid;
460: char *end;
461: int status;
462: pid_t pid;
463:
464: if ((ssh_agent_pid = pam_getenv(pamh, "SSH_AGENT_PID")) == NULL) {
465: openpam_log(PAM_LOG_DEBUG, "no ssh agent");
466: return (PAM_SUCCESS);
467: }
468: pid = (pid_t)strtol(ssh_agent_pid, &end, 10);
469: if (*ssh_agent_pid == '\0' || *end != '\0') {
470: openpam_log(PAM_LOG_DEBUG, "invalid ssh agent pid");
471: return (PAM_SESSION_ERR);
472: }
473: openpam_log(PAM_LOG_DEBUG, "killing ssh agent %d", (int)pid);
474: if (kill(pid, SIGTERM) == -1 ||
475: (waitpid(pid, &status, 0) == -1 && errno != ECHILD))
476: return (PAM_SYSTEM_ERR);
477: return (PAM_SUCCESS);
478: }
479:
480: PAM_MODULE_ENTRY("pam_ssh");
CVSweb <webmaster@jp.NetBSD.org>