pam_duo/pam_duo.c (287 lines of code) (raw):

/* * pam_duo.c * * Copyright (c) 2010 Duo Security * All rights reserved, all wrongs reversed. */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include <sys/param.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <ctype.h> #include <errno.h> #include <grp.h> #include <limits.h> #include <netdb.h> #include <pwd.h> #include <stdarg.h> #include <stdio.h> #include <syslog.h> #include <unistd.h> #include <openssl/crypto.h> #include <openssl/err.h> /* These #defines must be present according to PAM documentation. */ #define PAM_SM_AUTH #define PAM_SM_ACCOUNT #define PAM_SM_SESSION #define PAM_SM_PASSWORD /* NetBSD PAM b0rkage (gnat 39313) */ #ifdef __NetBSD__ #define NO_STATIC_MODULES #endif #ifdef HAVE_SECURITY_PAM_APPL_H #include <security/pam_appl.h> #endif #ifdef HAVE_SECURITY_PAM_MODULES_H #include <security/pam_modules.h> #endif #ifdef HAVE_SECURITY_PAM_EXT_H #include <security/pam_ext.h> /* Linux-PAM */ #endif /* OpenGroup RFC86.0 and XSSO specify no "const" on arguments */ #if defined(__LINUX_PAM__) || defined(OPENPAM) # define duopam_const const /* LinuxPAM, OpenPAM */ #else # define duopam_const /* Solaris, HP-UX, AIX */ #endif #include "util.h" #include "duo.h" #include "groupaccess.h" #include "pam_extra.h" #include "pam_duo_private.h" #ifndef PAM_EXTERN #define PAM_EXTERN #endif #ifndef DUO_PRIVSEP_USER # define DUO_PRIVSEP_USER "duo" #endif #define DUO_CONF DUO_CONF_DIR "/pam_duo.conf" static int __ini_handler(void *u, const char *section, const char *name, const char *val) { struct duo_config *cfg = (struct duo_config *)u; if (!duo_common_ini_handler(cfg, section, name, val)) { /* There are no options specific to pam_duo yet */ duo_syslog(LOG_ERR, "Invalid pam_duo option: '%s'", name); return (0); } return (1); } static void __duo_status(void *arg, const char *msg) { pam_info((pam_handle_t *)arg, "%s", msg); } static char * __duo_prompt(void *arg, const char *prompt, char *buf, size_t bufsz) { char *p = NULL; if (pam_prompt((pam_handle_t *)arg, PAM_PROMPT_ECHO_ON, &p, "%s", prompt) != PAM_SUCCESS || p == NULL) { return (NULL); } strlcpy(buf, p, bufsz); free(p); return (buf); } PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int pam_flags, int argc, const char *argv[]) { struct duo_config cfg; struct passwd *pw; struct in_addr addr; duo_t *duo; duo_code_t code; /* * Only variables that will be passed to a pam_* function * need to be marked as 'duopam_const char *', anything else * should be 'const char *'. This is because there are different * PAM implementations, some with the const qualifier, and some * without. */ duopam_const char *ip, *service, *user; const char *cmd, *p, *config, *host; // ASF hack: mix in hostname (whoami) with the command (cmd_extended) char cmd_extended[256], whoami[128]; int i, flags, pam_err, matched; duo_config_default(&cfg); /* Parse configuration */ config = DUO_CONF; if(parse_argv(&config, argc, argv) == 0) { return (PAM_SERVICE_ERR); } i = duo_parse_config(config, __ini_handler, &cfg); if (i == -2) { duo_syslog(LOG_ERR, "%s must be readable only by user 'root'", config); return (cfg.failmode == DUO_FAIL_SAFE ? PAM_SUCCESS : PAM_SERVICE_ERR); } else if (i == -1) { duo_syslog(LOG_ERR, "Couldn't open %s: %s", config, strerror(errno)); return (cfg.failmode == DUO_FAIL_SAFE ? PAM_SUCCESS : PAM_SERVICE_ERR); } else if (i > 0) { duo_syslog(LOG_ERR, "Parse error in %s, line %d", config, i); return (cfg.failmode == DUO_FAIL_SAFE ? PAM_SUCCESS : PAM_SERVICE_ERR); } else if (!cfg.apihost || !cfg.apihost[0] || !cfg.skey || !cfg.skey[0] || !cfg.ikey || !cfg.ikey[0]) { duo_syslog(LOG_ERR, "Missing host, ikey, or skey in %s", config); return (cfg.failmode == DUO_FAIL_SAFE ? PAM_SUCCESS : PAM_SERVICE_ERR); } #ifdef OPENSSL_FIPS /* * When fips_mode is configured, invoke OpenSSL's FIPS_mode_set() API. Note * that in some environments, FIPS may be enabled system-wide, causing FIPS * operation to be enabled automatically when OpenSSL is initialized. The * fips_mode option is an experimental feature allowing explicit entry to FIPS * operation in cases where it isn't enabled globally at the OS level (for * example, when integrating directly with the OpenSSL FIPS Object Module). */ if(!FIPS_mode_set(cfg.fips_mode)) { /* The smallest size buff can be according to the openssl docs */ char buff[256]; int error = ERR_get_error(); ERR_error_string_n(error, buff, sizeof(buff)); duo_syslog(LOG_ERR, "Unable to start fips_mode: %s", buff); return (EXIT_FAILURE); } #else if(cfg.fips_mode) { duo_syslog(LOG_ERR, "FIPS mode flag specified, but OpenSSL not built with FIPS support. Failing the auth."); return (EXIT_FAILURE); } #endif /* Check user */ if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS || (pw = getpwnam(user)) == NULL) { close_config(&cfg); return (PAM_USER_UNKNOWN); } /* XXX - Service-specific behavior */ flags = 0; cmd = NULL; if (pam_get_item(pamh, PAM_SERVICE, (duopam_const void **) (duopam_const void *)&service) != PAM_SUCCESS) { close_config(&cfg); return (PAM_SERVICE_ERR); } if (strcmp(service, "sshd") == 0) { /* * Disable incremental status reporting for sshd :-( * OpenSSH accumulates PAM_TEXT_INFO from modules to send in * an SSH_MSG_USERAUTH_BANNER post-auth, not real-time! */ flags |= DUO_FLAG_SYNC; } else if (strcmp(service, "sudo") == 0) { cmd = getenv("SUDO_COMMAND"); // ASF Hack: fetch local FQDN whoami[127] = '\0'; gethostname(whoami, 128); // ASF Hack: Add hostname to command run snprintf(cmd_extended, 256, "%s (%s)", whoami, cmd); cmd = (const char*) cmd_extended; } else if (strcmp(service, "su") == 0 || strcmp(service, "su-l") == 0) { /* Check calling user for Duo auth, just like sudo */ if ((pw = getpwuid(getuid())) == NULL) { close_config(&cfg); return (PAM_USER_UNKNOWN); } user = pw->pw_name; } /* Check group membership */ matched = duo_check_groups(pw, cfg.groups, cfg.groups_cnt); if (matched == -1) { close_config(&cfg); return (PAM_SERVICE_ERR); } else if (matched == 0) { if (cfg.group_access_fail) { /* suppress info notice for transparent fallthrough to another factor */ /* duo_syslog(LOG_INFO, "User %s not a member of an authorized UNIX group for Duo 2FA auth.", user); */ close_config(&cfg); return (PAM_SERVICE_ERR); } else { duo_syslog(LOG_INFO, "User %s bypassed Duo 2FA due to user's UNIX group", user); close_config(&cfg); return (PAM_SUCCESS); } } /* Use GECOS field if called for */ if (cfg.send_gecos || cfg.gecos_username_pos >= 0) { if (strlen(pw->pw_gecos) > 0) { if (cfg.gecos_username_pos >= 0) { user = duo_split_at(pw->pw_gecos, cfg.gecos_delim, cfg.gecos_username_pos); if (user == NULL || (strcmp(user, "") == 0)) { duo_log(LOG_DEBUG, "Could not parse GECOS field", pw->pw_name, NULL, NULL); user = pw->pw_name; } } else { user = pw->pw_gecos; } } else { duo_log(LOG_WARNING, "Empty GECOS field", pw->pw_name, NULL, NULL); } } /* Grab the remote host */ ip = NULL; pam_get_item(pamh, PAM_RHOST, (duopam_const void **)(duopam_const void *)&ip); host = ip; /* PAM is weird, check to see if PAM_RHOST is IP or hostname */ if (ip == NULL) { ip = ""; /* XXX inet_addr needs a non-null IP */ } if (!inet_aton(ip, &addr)) { /* We have a hostname, don't try to resolve, check fallback */ if (cfg.local_ip_fallback) { host = duo_local_ip(); } } /* Try Duo auth */ if ((duo = duo_open(cfg.apihost, cfg.ikey, cfg.skey, "pam_duo/" PACKAGE_VERSION, cfg.noverify ? "" : cfg.cafile, cfg.https_timeout, cfg.http_proxy)) == NULL) { duo_log(LOG_ERR, "Couldn't open Duo API handle", pw->pw_name, host, NULL); close_config(&cfg); return (PAM_SERVICE_ERR); } duo_set_conv_funcs(duo, __duo_prompt, __duo_status, pamh); if (cfg.autopush) { flags |= DUO_FLAG_AUTO; } pam_err = PAM_SERVICE_ERR; for (i = 0; i < cfg.prompts; i++) { code = duo_login(duo, user, host, flags, cfg.pushinfo ? cmd : NULL, cfg.failmode); if (code == DUO_FAIL) { duo_log(LOG_WARNING, "Failed Duo login", pw->pw_name, host, duo_geterr(duo)); if ((flags & DUO_FLAG_SYNC) == 0) { pam_info(pamh, "%s", ""); } /* Keep going */ continue; } /* Terminal conditions */ if (code == DUO_OK) { if ((p = duo_geterr(duo)) != NULL) { duo_log(LOG_WARNING, "Skipped Duo login", user, host, p); } else { duo_log(LOG_INFO, "Successful Duo login", user, host, NULL); } pam_err = PAM_SUCCESS; } else if (code == DUO_ABORT) { duo_log(LOG_WARNING, "Aborted Duo login", user, host, duo_geterr(duo)); pam_err = PAM_ABORT; } else if (code == DUO_FAIL_SAFE_ALLOW) { duo_log(LOG_WARNING, "Failsafe Duo login", user, host, duo_geterr(duo)); pam_err = PAM_SUCCESS; } else if (code == DUO_FAIL_SECURE_DENY) { duo_log(LOG_WARNING, "Failsecure Duo login", user, host, duo_geterr(duo)); pam_err = PAM_SERVICE_ERR; } else { duo_log(LOG_ERR, "Error in Duo login", user, host, duo_geterr(duo)); pam_err = PAM_SERVICE_ERR; } break; } if (i == cfg.prompts) { pam_err = PAM_MAXTRIES; } duo_close(duo); close_config(&cfg); return (pam_err); } PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char *argv[]) { return (PAM_SUCCESS); } PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char *argv[]) { return (PAM_SUCCESS); } PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char *argv[]) { return (PAM_SUCCESS); } PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char *argv[]) { return (PAM_SUCCESS); } PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char *argv[]) { return (PAM_SERVICE_ERR); } #ifdef PAM_MODULE_ENTRY PAM_MODULE_ENTRY("pam_duo"); #endif