/* * OpenVPN -- An application to securely tunnel IP networks * over a single TCP/UDP port, with support for SSL/TLS-based * session authentication and key exchange, * packet encryption, packet authentication, and * packet compression. * * Copyright (C) 2002-2018 OpenVPN Inc * Copyright (C) 2016-2018 Selva Nair * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ /* * OpenVPN plugin module to do PAM authentication using a split * privilege model. */ #ifdef HAVE_CONFIG_H #include #endif #include #ifdef USE_PAM_DLOPEN #include "pamdl.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include "utils.h" #include #define DEBUG(verb) ((verb) >= 4) /* Command codes for foreground -> background communication */ #define COMMAND_VERIFY 0 #define COMMAND_EXIT 1 /* Response codes for background -> foreground communication */ #define RESPONSE_INIT_SUCCEEDED 10 #define RESPONSE_INIT_FAILED 11 #define RESPONSE_VERIFY_SUCCEEDED 12 #define RESPONSE_VERIFY_FAILED 13 /* Pointers to functions exported from openvpn */ static plugin_secure_memzero_t plugin_secure_memzero = NULL; static plugin_base64_decode_t plugin_base64_decode = NULL; /* * Plugin state, used by foreground */ struct auth_pam_context { /* Foreground's socket to background process */ int foreground_fd; /* Process ID of background process */ pid_t background_pid; /* Verbosity level of OpenVPN */ int verb; }; /* * Name/Value pairs for conversation function. * Special Values: * * "USERNAME" -- substitute client-supplied username * "PASSWORD" -- substitute client-specified password * "COMMONNAME" -- substitute client certificate common name * "OTP" -- substitute static challenge response if available */ #define N_NAME_VALUE 16 struct name_value { const char *name; const char *value; }; struct name_value_list { int len; struct name_value data[N_NAME_VALUE]; }; /* * Used to pass the username/password * to the PAM conversation function. */ struct user_pass { int verb; char username[128]; char password[128]; char common_name[128]; char response[128]; const struct name_value_list *name_value_list; }; /* Background process function */ static void pam_server(int fd, const char *service, int verb, const struct name_value_list *name_value_list); /* * Socket read/write functions. */ static int recv_control(int fd) { unsigned char c; const ssize_t size = read(fd, &c, sizeof(c)); if (size == sizeof(c)) { return c; } else { /*fprintf (stderr, "AUTH-PAM: DEBUG recv_control.read=%d\n", (int)size);*/ return -1; } } static int send_control(int fd, int code) { unsigned char c = (unsigned char) code; const ssize_t size = write(fd, &c, sizeof(c)); if (size == sizeof(c)) { return (int) size; } else { return -1; } } static int recv_string(int fd, char *buffer, int len) { if (len > 0) { ssize_t size; memset(buffer, 0, len); size = read(fd, buffer, len); buffer[len-1] = 0; if (size >= 1) { return (int)size; } } return -1; } static int send_string(int fd, const char *string) { const int len = strlen(string) + 1; const ssize_t size = write(fd, string, len); if (size == len) { return (int) size; } else { return -1; } } #ifdef DO_DAEMONIZE /* * Daemonize if "daemon" env var is true. * Preserve stderr across daemonization if * "daemon_log_redirect" env var is true. */ static void daemonize(const char *envp[]) { const char *daemon_string = get_env("daemon", envp); if (daemon_string && daemon_string[0] == '1') { const char *log_redirect = get_env("daemon_log_redirect", envp); int fd = -1; if (log_redirect && log_redirect[0] == '1') { fd = dup(2); } if (daemon(0, 0) < 0) { fprintf(stderr, "AUTH-PAM: daemonization failed\n"); } else if (fd >= 3) { dup2(fd, 2); close(fd); } } } #endif /* ifdef DO_DAEMONIZE */ /* * Close most of parent's fds. * Keep stdin/stdout/stderr, plus one * other fd which is presumed to be * our pipe back to parent. * Admittedly, a bit of a kludge, * but posix doesn't give us a kind * of FD_CLOEXEC which will stop * fds from crossing a fork(). */ static void close_fds_except(int keep) { int i; closelog(); for (i = 3; i <= 100; ++i) { if (i != keep) { close(i); } } } /* * Usually we ignore signals, because our parent will * deal with them. */ static void set_signals(void) { signal(SIGTERM, SIG_DFL); signal(SIGINT, SIG_IGN); signal(SIGHUP, SIG_IGN); signal(SIGUSR1, SIG_IGN); signal(SIGUSR2, SIG_IGN); signal(SIGPIPE, SIG_IGN); } /* * Return 1 if query matches match. */ static int name_value_match(const char *query, const char *match) { while (!isalnum(*query)) { if (*query == '\0') { return 0; } ++query; } return strncasecmp(match, query, strlen(match)) == 0; } /* * Split and decode up->password in the form SCRV1:base64_pass:base64_response * into pass and response and save in up->password and up->response. * If the password is not in the expected format, input is not changed. */ static void split_scrv1_password(struct user_pass *up) { const int skip = strlen("SCRV1:"); if (strncmp(up->password, "SCRV1:", skip) != 0) { return; } char *tmp = strdup(up->password); if (!tmp) { fprintf(stderr, "AUTH-PAM: out of memory parsing static challenge password\n"); goto out; } char *pass = tmp + skip; char *resp = strchr(pass, ':'); if (!resp) /* string not in SCRV1:xx:yy format */ { goto out; } *resp++ = '\0'; int n = plugin_base64_decode(pass, up->password, sizeof(up->password)-1); if (n > 0) { up->password[n] = '\0'; n = plugin_base64_decode(resp, up->response, sizeof(up->response)-1); if (n > 0) { up->response[n] = '\0'; if (DEBUG(up->verb)) { fprintf(stderr, "AUTH-PAM: BACKGROUND: parsed static challenge password\n"); } goto out; } } /* decode error: reinstate original value of up->password and return */ plugin_secure_memzero(up->password, sizeof(up->password)); plugin_secure_memzero(up->response, sizeof(up->response)); strcpy(up->password, tmp); /* tmp is guaranteed to fit in up->password */ fprintf(stderr, "AUTH-PAM: base64 decode error while parsing static challenge password\n"); out: if (tmp) { plugin_secure_memzero(tmp, strlen(tmp)); free(tmp); } } OPENVPN_EXPORT int openvpn_plugin_open_v3(const int v3structver, struct openvpn_plugin_args_open_in const *args, struct openvpn_plugin_args_open_return *ret) { pid_t pid; int fd[2]; struct auth_pam_context *context; struct name_value_list name_value_list; const int base_parms = 2; const char **argv = args->argv; const char **envp = args->envp; /* Check API compatibility -- struct version 5 or higher needed */ if (v3structver < 5) { fprintf(stderr, "AUTH-PAM: This plugin is incompatible with the running version of OpenVPN\n"); return OPENVPN_PLUGIN_FUNC_ERROR; } /* * Allocate our context */ context = (struct auth_pam_context *) calloc(1, sizeof(struct auth_pam_context)); if (!context) { goto error; } context->foreground_fd = -1; /* * Intercept the --auth-user-pass-verify callback. */ ret->type_mask = OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY); /* Save global pointers to functions exported from openvpn */ plugin_secure_memzero = args->callbacks->plugin_secure_memzero; plugin_base64_decode = args->callbacks->plugin_base64_decode; /* * Make sure we have two string arguments: the first is the .so name, * the second is the PAM service type. */ if (string_array_len(argv) < base_parms) { fprintf(stderr, "AUTH-PAM: need PAM service parameter\n"); goto error; } /* * See if we have optional name/value pairs to match against * PAM module queried fields in the conversation function. */ name_value_list.len = 0; if (string_array_len(argv) > base_parms) { const int nv_len = string_array_len(argv) - base_parms; int i; if ((nv_len & 1) == 1 || (nv_len / 2) > N_NAME_VALUE) { fprintf(stderr, "AUTH-PAM: bad name/value list length\n"); goto error; } name_value_list.len = nv_len / 2; for (i = 0; i < name_value_list.len; ++i) { const int base = base_parms + i * 2; name_value_list.data[i].name = argv[base]; name_value_list.data[i].value = argv[base+1]; } } /* * Get verbosity level from environment */ { const char *verb_string = get_env("verb", envp); if (verb_string) { context->verb = atoi(verb_string); } } /* * Make a socket for foreground and background processes * to communicate. */ if (socketpair(PF_UNIX, SOCK_DGRAM, 0, fd) == -1) { fprintf(stderr, "AUTH-PAM: socketpair call failed\n"); goto error; } /* * Fork off the privileged process. It will remain privileged * even after the foreground process drops its privileges. */ pid = fork(); if (pid) { int status; /* * Foreground Process */ context->background_pid = pid; /* close our copy of child's socket */ close(fd[1]); /* don't let future subprocesses inherit child socket */ if (fcntl(fd[0], F_SETFD, FD_CLOEXEC) < 0) { fprintf(stderr, "AUTH-PAM: Set FD_CLOEXEC flag on socket file descriptor failed\n"); } /* wait for background child process to initialize */ status = recv_control(fd[0]); if (status == RESPONSE_INIT_SUCCEEDED) { context->foreground_fd = fd[0]; ret->handle = (openvpn_plugin_handle_t *) context; return OPENVPN_PLUGIN_FUNC_SUCCESS; } } else { /* * Background Process */ /* close all parent fds except our socket back to parent */ close_fds_except(fd[1]); /* Ignore most signals (the parent will receive them) */ set_signals(); #ifdef DO_DAEMONIZE /* Daemonize if --daemon option is set. */ daemonize(envp); #endif /* execute the event loop */ pam_server(fd[1], argv[1], context->verb, &name_value_list); close(fd[1]); exit(0); return 0; /* NOTREACHED */ } error: if (context) { free(context); } return OPENVPN_PLUGIN_FUNC_ERROR; } OPENVPN_EXPORT int openvpn_plugin_func_v1(openvpn_plugin_handle_t handle, const int type, const char *argv[], const char *envp[]) { struct auth_pam_context *context = (struct auth_pam_context *) handle; if (type == OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY && context->foreground_fd >= 0) { /* get username/password from envp string array */ const char *username = get_env("username", envp); const char *password = get_env("password", envp); const char *common_name = get_env("common_name", envp) ? get_env("common_name", envp) : ""; if (username && strlen(username) > 0 && password) { if (send_control(context->foreground_fd, COMMAND_VERIFY) == -1 || send_string(context->foreground_fd, username) == -1 || send_string(context->foreground_fd, password) == -1 || send_string(context->foreground_fd, common_name) == -1) { fprintf(stderr, "AUTH-PAM: Error sending auth info to background process\n"); } else { const int status = recv_control(context->foreground_fd); if (status == RESPONSE_VERIFY_SUCCEEDED) { return OPENVPN_PLUGIN_FUNC_SUCCESS; } if (status == -1) { fprintf(stderr, "AUTH-PAM: Error receiving auth confirmation from background process\n"); } } } } return OPENVPN_PLUGIN_FUNC_ERROR; } OPENVPN_EXPORT void openvpn_plugin_close_v1(openvpn_plugin_handle_t handle) { struct auth_pam_context *context = (struct auth_pam_context *) handle; if (DEBUG(context->verb)) { fprintf(stderr, "AUTH-PAM: close\n"); } if (context->foreground_fd >= 0) { /* tell background process to exit */ if (send_control(context->foreground_fd, COMMAND_EXIT) == -1) { fprintf(stderr, "AUTH-PAM: Error signaling background process to exit\n"); } /* wait for background process to exit */ if (context->background_pid > 0) { waitpid(context->background_pid, NULL, 0); } close(context->foreground_fd); context->foreground_fd = -1; } free(context); } OPENVPN_EXPORT void openvpn_plugin_abort_v1(openvpn_plugin_handle_t handle) { struct auth_pam_context *context = (struct auth_pam_context *) handle; /* tell background process to exit */ if (context && context->foreground_fd >= 0) { send_control(context->foreground_fd, COMMAND_EXIT); close(context->foreground_fd); context->foreground_fd = -1; } } /* * PAM conversation function */ static int my_conv(int n, const struct pam_message **msg_array, struct pam_response **response_array, void *appdata_ptr) { const struct user_pass *up = ( const struct user_pass *) appdata_ptr; struct pam_response *aresp; int i; int ret = PAM_SUCCESS; *response_array = NULL; if (n <= 0 || n > PAM_MAX_NUM_MSG) { return (PAM_CONV_ERR); } if ((aresp = calloc(n, sizeof *aresp)) == NULL) { return (PAM_BUF_ERR); } /* loop through each PAM-module query */ for (i = 0; i < n; ++i) { const struct pam_message *msg = msg_array[i]; aresp[i].resp_retcode = 0; aresp[i].resp = NULL; if (DEBUG(up->verb)) { fprintf(stderr, "AUTH-PAM: BACKGROUND: my_conv[%d] query='%s' style=%d\n", i, msg->msg ? msg->msg : "NULL", msg->msg_style); } if (up->name_value_list && up->name_value_list->len > 0) { /* use name/value list match method */ const struct name_value_list *list = up->name_value_list; int j; /* loop through name/value pairs */ for (j = 0; j < list->len; ++j) { const char *match_name = list->data[j].name; const char *match_value = list->data[j].value; if (name_value_match(msg->msg, match_name)) { /* found name/value match */ aresp[i].resp = NULL; if (DEBUG(up->verb)) { fprintf(stderr, "AUTH-PAM: BACKGROUND: name match found, query/match-string ['%s', '%s'] = '%s'\n", msg->msg, match_name, match_value); } if (strstr(match_value, "USERNAME")) { aresp[i].resp = searchandreplace(match_value, "USERNAME", up->username); } else if (strstr(match_value, "PASSWORD")) { aresp[i].resp = searchandreplace(match_value, "PASSWORD", up->password); } else if (strstr(match_value, "COMMONNAME")) { aresp[i].resp = searchandreplace(match_value, "COMMONNAME", up->common_name); } else if (strstr(match_value, "OTP")) { aresp[i].resp = searchandreplace(match_value, "OTP", up->response); } else { aresp[i].resp = strdup(match_value); } if (aresp[i].resp == NULL) { ret = PAM_CONV_ERR; } break; } } if (j == list->len) { ret = PAM_CONV_ERR; } } else { /* use PAM_PROMPT_ECHO_x hints */ switch (msg->msg_style) { case PAM_PROMPT_ECHO_OFF: aresp[i].resp = strdup(up->password); if (aresp[i].resp == NULL) { ret = PAM_CONV_ERR; } break; case PAM_PROMPT_ECHO_ON: aresp[i].resp = strdup(up->username); if (aresp[i].resp == NULL) { ret = PAM_CONV_ERR; } break; case PAM_ERROR_MSG: case PAM_TEXT_INFO: break; default: ret = PAM_CONV_ERR; break; } } } if (ret == PAM_SUCCESS) { *response_array = aresp; } else { free(aresp); } return ret; } /* * Return 1 if authenticated and 0 if failed. * Called once for every username/password * to be authenticated. */ static int pam_auth(const char *service, const struct user_pass *up) { struct pam_conv conv; pam_handle_t *pamh = NULL; int status = PAM_SUCCESS; int ret = 0; const int name_value_list_provided = (up->name_value_list && up->name_value_list->len > 0); /* Initialize PAM */ conv.conv = my_conv; conv.appdata_ptr = (void *)up; status = pam_start(service, name_value_list_provided ? NULL : up->username, &conv, &pamh); if (status == PAM_SUCCESS) { /* Call PAM to verify username/password */ status = pam_authenticate(pamh, 0); if (status == PAM_SUCCESS) { status = pam_acct_mgmt(pamh, 0); } if (status == PAM_SUCCESS) { ret = 1; } /* Output error message if failed */ if (!ret) { fprintf(stderr, "AUTH-PAM: BACKGROUND: user '%s' failed to authenticate: %s\n", up->username, pam_strerror(pamh, status)); } /* Close PAM */ pam_end(pamh, status); } return ret; } /* * Background process -- runs with privilege. */ static void pam_server(int fd, const char *service, int verb, const struct name_value_list *name_value_list) { struct user_pass up; int command; #ifdef USE_PAM_DLOPEN static const char pam_so[] = "libpam.so"; #endif /* * Do initialization */ if (DEBUG(verb)) { fprintf(stderr, "AUTH-PAM: BACKGROUND: INIT service='%s'\n", service); } #ifdef USE_PAM_DLOPEN /* * Load PAM shared object */ if (!dlopen_pam(pam_so)) { fprintf(stderr, "AUTH-PAM: BACKGROUND: could not load PAM lib %s: %s\n", pam_so, dlerror()); send_control(fd, RESPONSE_INIT_FAILED); goto done; } #endif /* * Tell foreground that we initialized successfully */ if (send_control(fd, RESPONSE_INIT_SUCCEEDED) == -1) { fprintf(stderr, "AUTH-PAM: BACKGROUND: write error on response socket [1]\n"); goto done; } /* * Event loop */ while (1) { memset(&up, 0, sizeof(up)); up.verb = verb; up.name_value_list = name_value_list; /* get a command from foreground process */ command = recv_control(fd); if (DEBUG(verb)) { fprintf(stderr, "AUTH-PAM: BACKGROUND: received command code: %d\n", command); } switch (command) { case COMMAND_VERIFY: if (recv_string(fd, up.username, sizeof(up.username)) == -1 || recv_string(fd, up.password, sizeof(up.password)) == -1 || recv_string(fd, up.common_name, sizeof(up.common_name)) == -1) { fprintf(stderr, "AUTH-PAM: BACKGROUND: read error on command channel: code=%d, exiting\n", command); goto done; } if (DEBUG(verb)) { #if 0 fprintf(stderr, "AUTH-PAM: BACKGROUND: USER/PASS: %s/%s\n", up.username, up.password); #else fprintf(stderr, "AUTH-PAM: BACKGROUND: USER: %s\n", up.username); #endif } /* If password is of the form SCRV1:base64:base64 split it up */ split_scrv1_password(&up); if (pam_auth(service, &up)) /* Succeeded */ { if (send_control(fd, RESPONSE_VERIFY_SUCCEEDED) == -1) { fprintf(stderr, "AUTH-PAM: BACKGROUND: write error on response socket [2]\n"); goto done; } } else /* Failed */ { if (send_control(fd, RESPONSE_VERIFY_FAILED) == -1) { fprintf(stderr, "AUTH-PAM: BACKGROUND: write error on response socket [3]\n"); goto done; } } plugin_secure_memzero(up.password, sizeof(up.password)); break; case COMMAND_EXIT: goto done; case -1: fprintf(stderr, "AUTH-PAM: BACKGROUND: read error on command channel\n"); goto done; default: fprintf(stderr, "AUTH-PAM: BACKGROUND: unknown command code: code=%d, exiting\n", command); goto done; } plugin_secure_memzero(up.response, sizeof(up.response)); } done: plugin_secure_memzero(up.password, sizeof(up.password)); plugin_secure_memzero(up.response, sizeof(up.response)); #ifdef USE_PAM_DLOPEN dlclose_pam(); #endif if (DEBUG(verb)) { fprintf(stderr, "AUTH-PAM: BACKGROUND: EXIT\n"); } return; }