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