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