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