#include <stdio.h> /* * 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-2021 OpenVPN Inc <sales@openvpn.net> * * 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. */ /* * This plugin utilizes a YubiKey with an OTP Challenge Response support to * store a 20 Byte long HMAC-SHA1 server key. This server key is then used to * generate client specific server keys, which, if compromised, will only affect * this client. Because YubiKey doesn't support AES Key storage / derivation, we * use a challenge response mechanisms to derive keys which can't be * reverse engineered to the master server key. Using this plugin reduces the * security of the tls-crypt-v2 from 2 256-Bit keys to a single 160-Bit one. */ #include <string.h> #include <stdlib.h> #include "openvpn-plugin.h" #include <openssl/evp.h> #include <openssl/hmac.h> #include <fcntl.h> #include <sys/syslog.h> #include <signal.h> #include <sys/wait.h> #include <unistd.h> #include "yubikey_handler.c" #include "ykstatus.h" static char *MODULE = "OPENVPN_PLUGIN_CLIENT_KEY_WRAPPING"; /* * Constants indicating minimum API and struct versions by the functions * in this plugin. Consult openvpn-plugin.h, look for: * OPENVPN_PLUGIN_VERSION and OPENVPN_PLUGINv3_STRUCTVER */ #define OPENVPN_PLUGIN_VERSION_MIN 3 #define OPENVPN_PLUGIN_STRUCTVER_MIN 5 #define TLS_CRYPT_V2_MAX_WKC_LEN 1024 #define TLS_CRYPT_V2_KEY_LEN 32 #define TLS_CRYPT_V2_TAG_LEN 32 #define TLS_CRYPT_V2_LEN_LEN 2 #define TLS_CRYPT_V2_SERVER_KEY_LEN 64 #define AES_SLOT 0 #define HMAC_SLOT 1 /* Error handling */ #define ERROR_CHECK(cond, text) do { if (cond) { plog(PLOG_ERR, text); return OPENVPN_PLUGIN_FUNC_ERROR; } } while(0) #define CRYPTO_ECHECK(cond, text) do { if (cond) { plog(PLOG_ERR, text); goto error_exit; } } while(0) #define RESPONSE_LENGTH 20 #define CHALLENGE_LENGTH TLS_CRYPT_V2_TAG_LEN #define RESPONSE_INIT_SUCCEEDED 10 /* Exported plug-in v3 API functions */ plugin_vlog_t plugin_vlog_func = NULL; plugin_base64_decode_t ovpn_base64_decode = NULL; plugin_base64_encode_t ovpn_base64_encode = NULL; plugin_secure_memzero_t ovpn_secure_memzero = NULL; /* * Our context, where we keep our state. */ struct plugin_context { YK_KEY *yk; YK_STATUS *st; pid_t background_pid; int write_pipe; int read_pipe; unsigned char slot[2]; void *acc_code; int verb; }; /* local wrapping of the log function, to add more details */ static void plog(int flags, char *fmt, ...) { char logid[129]; snprintf(logid, 128, "%s", MODULE); va_list arglist; va_start(arglist, fmt); plugin_vlog_func(flags, logid, fmt, arglist); va_end(arglist); } /* * Given an environmental variable name, search * the envp array for its value, returning it * if found or NULL otherwise. */ static const char * get_env(const char *name, const char *envp[]) { if (envp) { int i; const unsigned int namelen = strlen(name); for (i = 0; envp[i]; ++i) { if (!strncmp(envp[i], name, namelen)) { const char *cp = envp[i] + namelen; if (*cp == '=') { return cp + 1; } } } } return NULL; } OPENVPN_EXPORT int openvpn_plugin_min_version_required_v1() { return OPENVPN_PLUGIN_VERSION_MIN; } OPENVPN_EXPORT int openvpn_plugin_select_initialization_point_v1() { return OPENVPN_PLUGIN_INIT_PRE_CONFIG_PARSE; } 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) { if (v3structver < OPENVPN_PLUGIN_STRUCTVER_MIN) { plog(PLOG_ERR, "this plugin is incompatible with the running version of OpenVPN"); return OPENVPN_PLUGIN_FUNC_ERROR; } if (args->ssl_api != SSLAPI_OPENSSL) { plog(PLOG_ERR, "plug-in can only be used against OpenVPN with OpenSSL\n"); return OPENVPN_PLUGIN_FUNC_ERROR; } /* Save global pointers to functions exported from openvpn */ plugin_vlog_func = args->callbacks->plugin_vlog; ovpn_base64_decode = args->callbacks->plugin_base64_decode; ovpn_base64_encode = args->callbacks->plugin_base64_encode; ovpn_secure_memzero = args->callbacks->plugin_secure_memzero; struct plugin_context *context = NULL; context = (struct plugin_context *) calloc(1, sizeof(struct plugin_context)); if (!context) { goto error; } const char **argv = args->argv; if(!argv[1] || !argv[2]) { plog(PLOG_ERR, "Not all args specified!"); goto error; } unsigned char slot_preference = (unsigned char) strtol(argv[1], NULL, 10); if(slot_preference == 1 || slot_preference == 2) { context->slot[0] = slot_preference; context->slot[1] = slot_preference; } else if(slot_preference == 3) { context->slot[AES_SLOT] = 1; context->slot[HMAC_SLOT] = 2; } else { plog(PLOG_ERR, "Invalid slot specified!"); goto error; } if(strcmp(argv[2], "0") != 0) { context->acc_code = calloc(strlen(argv[2]) + 1, sizeof(char)); strcpy(context->acc_code, argv[2]); plog(PLOG_DEBUG, "Using access code %s", context->acc_code); if(!context->acc_code) { plog(PLOG_ERR, "calloc(acc_code) failed"); goto error; } } context->background_pid = 0; /* * Get verbosity level from environment */ { const char *verb_string = get_env("verb", args->envp); if (verb_string) { context->verb = (int) strtol(verb_string, NULL, 10); } } /* * Which callbacks to intercept. */ ret->type_mask = OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_CLIENT_KEY_WRAPPING) | OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_UP); ret->handle = (openvpn_plugin_handle_t *) context; return OPENVPN_PLUGIN_FUNC_SUCCESS; error: plog(PLOG_NOTE, "initialization failed"); return OPENVPN_PLUGIN_FUNC_ERROR; } static int aes_256_ctr(unsigned const char *data, uint16_t data_len, const unsigned char *key, uint16_t key_length, const unsigned char *iv, unsigned char *output) { unsigned char normalized_key[TLS_CRYPT_V2_KEY_LEN] = { 0 }; memcpy(normalized_key, key, key_length); EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); if(!ctx) return false; EVP_CIPHER_CTX_init(ctx); if(!EVP_EncryptInit_ex2(ctx, EVP_aes_256_ctr(), normalized_key, iv, NULL)) return false; int out_len; if(!EVP_EncryptUpdate(ctx, output, &out_len, data, data_len)) return false; EVP_CIPHER_CTX_free(ctx); return true; } static int do_challenge_response(struct plugin_context *context, const unsigned char *challenge, unsigned char *response, unsigned char slot) { if(context->yk) { plog(PLOG_DEBUG, "Performing local challenge"); return challenge_response(context->yk, slot, false, CHALLENGE_LENGTH, challenge, response, true); } else { plog(PLOG_DEBUG, "Sending challenge to background process"); ssize_t bytes_processed; bytes_processed = write(context->write_pipe, &slot, sizeof(unsigned char)); if(bytes_processed != sizeof(unsigned char)) { return false; } bytes_processed = write(context->write_pipe, challenge, CHALLENGE_LENGTH); if(bytes_processed != CHALLENGE_LENGTH) { return false; } bytes_processed = read(context->read_pipe, response, RESPONSE_LENGTH); if(bytes_processed != RESPONSE_LENGTH) { return false; } } return true; } static int calculate_cipher(struct plugin_context *context, const unsigned char *data, uint16_t data_len, const unsigned char *tag, unsigned char* output) { unsigned char aes_client_key[REQ_RESPONSE_LENGTH] = { 0 }; int return_code = false; // Calculate AES Key with Yubikey CRYPTO_ECHECK(!do_challenge_response(context, tag, aes_client_key, context->slot[AES_SLOT]), "Couldn't derive client-server AES key"); // Process Data CRYPTO_ECHECK(!aes_256_ctr(data, data_len, aes_client_key, RESPONSE_LENGTH, tag, output), "aes_256_ctr() failed"); return_code = true; error_exit: ovpn_secure_memzero(aes_client_key, sizeof(aes_client_key)); return return_code; } static int calculate_tag(struct plugin_context *context, const unsigned char *data, uint16_t data_len, unsigned char *tag) { unsigned char hmac_client_key[REQ_RESPONSE_LENGTH] = { 0 }; unsigned char data_buffer[TLS_CRYPT_V2_MAX_WKC_LEN] = { 0 }; int exit_code = false; CRYPTO_ECHECK(!do_challenge_response(context, data, hmac_client_key, context->slot[HMAC_SLOT]), "Couldn't derive client-server HMAC key"); data_buffer[0] = (data_len >> 8) & 0xFF; data_buffer[1] = data_len & 0xFF; memcpy(data_buffer + 2, data, data_len); CRYPTO_ECHECK(!HMAC(EVP_sha256(), hmac_client_key, RESPONSE_LENGTH, data_buffer, data_len + TLS_CRYPT_V2_LEN_LEN, tag, NULL), "HMAC() failed"); exit_code = true; error_exit: ovpn_secure_memzero(hmac_client_key, sizeof(hmac_client_key)); return exit_code; } static uint16_t bytesToShort(const unsigned char *bytes) { return *(bytes) << 8 | *(bytes + 1); } static int handle_return(struct openvpn_plugin_args_func_return *ret, const void *data, int data_len) { struct openvpn_plugin_string_list *rl = calloc(1, sizeof(struct openvpn_plugin_string_list)); if(!rl) return false; rl->name = strdup("wrapping result"); int b64_size = ovpn_base64_encode(data, data_len, &rl->value); if(b64_size < 0) return false; struct openvpn_plugin_string_list **ret_list = ret->return_list; *ret_list = rl; return true; } /** * Unwrap a client key by using keys derived from a HMAC-SHA1 key stored inside a YubiKey * * @return int Returns OPENVPN_PLUGIN_FUNC_ERROR on error and OPENVPN_PLUGIN_FUNC_SUCCESS on success * */ static int yubikey_unwrap(struct plugin_context *context, const char **argv, struct openvpn_plugin_args_func_return *ret) { unsigned char kc[TLS_CRYPT_V2_MAX_WKC_LEN] = { 0 }; unsigned char wkc[TLS_CRYPT_V2_MAX_WKC_LEN] = { 0 }; unsigned char tag[TLS_CRYPT_V2_TAG_LEN] = { 0 }; int exit_code = OPENVPN_PLUGIN_FUNC_ERROR; // Decode WKc from argv const char *wkc_base64 = argv[2]; plog(PLOG_DEBUG, "Received WKc: %s", wkc_base64); int wkc_len = ovpn_base64_decode(wkc_base64, wkc, TLS_CRYPT_V2_MAX_WKC_LEN); CRYPTO_ECHECK(wkc_len < 0, "ovpn_base64_decode failed"); // Length checks int kc_len = wkc_len - TLS_CRYPT_V2_TAG_LEN - TLS_CRYPT_V2_LEN_LEN; CRYPTO_ECHECK(kc_len < 0, "Invalid Length of WKc"); uint16_t net_len = bytesToShort(wkc + wkc_len - 2); CRYPTO_ECHECK(net_len != wkc_len, "Invalid Declaration of Length for WKc"); // Decrypt cipher block CRYPTO_ECHECK(!calculate_cipher(context, wkc + TLS_CRYPT_V2_TAG_LEN, kc_len, wkc, kc), "Couldn't decrypt client key"); // Calculate tag and compare CRYPTO_ECHECK(!calculate_tag(context, kc, kc_len, tag), "Couldn't calculate tag"); CRYPTO_ECHECK(memcmp(tag, wkc, TLS_CRYPT_V2_TAG_LEN) != 0, "Tags don't match"); // Prepare return for openvpn CRYPTO_ECHECK(!handle_return(ret, kc, kc_len), "Returning results failed"); exit_code = OPENVPN_PLUGIN_FUNC_SUCCESS; error_exit: ovpn_secure_memzero(kc, sizeof(kc)); return exit_code; } /** * Wrap a client key by using keys derived from a HMAC-SHA1 key stored inside a YubiKey * * @return int Returns OPENVPN_PLUGIN_FUNC_ERROR on error and OPENVPN_PLUGIN_FUNC_SUCCESS on success * */ static int yubikey_wrap(struct plugin_context *context, const char **argv, struct openvpn_plugin_args_func_return *ret) { unsigned char kc[TLS_CRYPT_V2_MAX_WKC_LEN] = { 0 }; unsigned char wkc[TLS_CRYPT_V2_MAX_WKC_LEN] = { 0 }; unsigned char tag[TLS_CRYPT_V2_TAG_LEN] = { 0 }; int exit_code = OPENVPN_PLUGIN_FUNC_ERROR; // Decode Kc from argv const char *kc_base64 = argv[2]; plog(PLOG_DEBUG, "Received Kc: %s", kc_base64); int kc_len = ovpn_base64_decode(kc_base64, kc, TLS_CRYPT_V2_MAX_WKC_LEN); CRYPTO_ECHECK(kc_len < 0, "ovpn_base64_decode() failed"); // Calculate tag CRYPTO_ECHECK(!calculate_tag(context, kc, kc_len, wkc), "Couldn't calculate tag"); CRYPTO_ECHECK(!calculate_cipher(context, kc, kc_len, wkc, wkc + TLS_CRYPT_V2_TAG_LEN), "Couldn't encrypt client key"); uint16_t wkc_len = kc_len + TLS_CRYPT_V2_TAG_LEN + TLS_CRYPT_V2_LEN_LEN; wkc[wkc_len - 2] = (wkc_len >> 8) & 0xFF; wkc[wkc_len - 1] = wkc_len & 0xFF; // Prepare return for openvpn CRYPTO_ECHECK(!handle_return(ret, wkc, wkc_len), "Returning results failed"); exit_code = OPENVPN_PLUGIN_FUNC_SUCCESS; error_exit: return exit_code; } /** * Import first 20 Bytes of generated AES Server Key as HMAC-SHA1 Key into Yubikey * * @return int Returns OPENVPN_PLUGIN_FUNC_ERROR on error and OPENVPN_PLUGIN_FUNC_SUCCESS on success * */ static int yubikey_import_server_key(struct plugin_context *context, const char **argv) { char aes_keybuf[TLS_CRYPT_V2_SERVER_KEY_LEN]; char hmac_keybuf[TLS_CRYPT_V2_SERVER_KEY_LEN]; int ret_code = 0; ERROR_CHECK(ovpn_base64_decode(argv[2], aes_keybuf, TLS_CRYPT_V2_SERVER_KEY_LEN) <= 0, "ovpn_base64_decode() failed"); ERROR_CHECK(ovpn_base64_decode(argv[3], hmac_keybuf, TLS_CRYPT_V2_SERVER_KEY_LEN) <= 0, "ovpn_base64_decode() failed"); if(context->slot[0] == context->slot[1]) { ret_code |= import_server_key(context->yk, context->st, aes_keybuf, context->acc_code, context->verb, context->slot[AES_SLOT]);} else { ret_code |= import_server_key(context->yk, context->st, aes_keybuf, context->acc_code, context->verb, context->slot[AES_SLOT]); ret_code |= import_server_key(context->yk, context->st, hmac_keybuf, context->acc_code, context->verb, context->slot[HMAC_SLOT]); } ovpn_secure_memzero(aes_keybuf, sizeof(aes_keybuf)); ovpn_secure_memzero(hmac_keybuf, sizeof(hmac_keybuf)); return ret_code; } static int initialize_yubikey_context(struct plugin_context *context) { context->yk = 0; context->st = ykds_alloc(); if(!key_init(&context->yk, context->st)) { printf("%s", yk_usb_strerror()); plog(PLOG_ERR, "Couldn't initialize Yubikey!"); return false; } return true; } static void cleanup_yubikey_context(struct plugin_context *context) { if (context->st) { ykds_free(context->st); context->st = NULL; } if (context->yk) { yk_close_key(context->yk); context->yk = NULL; } yk_release(); } static void cleanup_yubikey_server(const struct plugin_context *context) { close(context->write_pipe); close(context->read_pipe); waitpid(context->background_pid, NULL, 0); } /* * 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) { plog(PLOG_WARN, "daemonize() failed"); } else if (fd >= 3) { dup2(fd, 2); close(fd); } } } static int try_resurrect(struct plugin_context *context) { cleanup_yubikey_context(context); return initialize_yubikey_context(context); } static void yubikey_server(struct plugin_context *context, int read_pipe, int write_pipe) { if(!initialize_yubikey_context(context)) { plog(PLOG_ERR, "Background Yubikey Initialization failed"); return; } unsigned char init_status = RESPONSE_INIT_SUCCEEDED; if(write(write_pipe, &init_status, sizeof (init_status)) < 0) { plog(PLOG_ERR, "Sending Status to Foreground Process failed"); goto done; } unsigned char slot; unsigned char challenge_buffer[CHALLENGE_LENGTH]; unsigned char response_buffer[REQ_RESPONSE_LENGTH]; ssize_t bytes_processed; while(1) { memset(challenge_buffer, 0, CHALLENGE_LENGTH); memset(response_buffer, 0, REQ_RESPONSE_LENGTH); bytes_processed = read(read_pipe, &slot, sizeof(unsigned char)); if(bytes_processed != sizeof(unsigned char)) { plog(PLOG_NOTE, "Reading Slot from Pipe not possible"); goto done; } bytes_processed = read(read_pipe, challenge_buffer, sizeof(challenge_buffer)); if(bytes_processed != sizeof(challenge_buffer)) { plog(PLOG_NOTE, "Reading Challenge from Pipe not possible"); goto done; } if(!challenge_response(context->yk, slot, false, CHALLENGE_LENGTH, challenge_buffer, response_buffer, true)) { if(!try_resurrect(context) || !challenge_response(context->yk, slot, false, CHALLENGE_LENGTH, challenge_buffer, response_buffer, true)) { plog(PLOG_ERR, "Yubikey Challenge failed"); goto done; } } bytes_processed = write(write_pipe, response_buffer, RESPONSE_LENGTH); if(bytes_processed != RESPONSE_LENGTH) { plog(PLOG_ERR, "Writing Response to Buffer failed"); goto done; } } done: cleanup_yubikey_context(context); } static int initialize_background(struct plugin_context *context, const char *envp[]) { int f2b[2]; int b2f[2]; pid_t pid; ERROR_CHECK(pipe(f2b), "F2B Pipe Failed"); ERROR_CHECK(pipe(b2f), "F2B Pipe Failed"); pid = fork(); if(pid) { close(f2b[0]); close(b2f[1]); context->background_pid = pid; context->write_pipe = f2b[1]; context->read_pipe = b2f[0]; /* don't let future subprocesses inherit child socket */ ERROR_CHECK(fcntl(context->write_pipe, F_SETFD, FD_CLOEXEC) < 0, "Failed setting FD attribute"); ERROR_CHECK(fcntl(context->read_pipe, F_SETFD, FD_CLOEXEC) < 0, "Failed setting FD attribute"); unsigned char status = 0; read(context->read_pipe, &status, sizeof(status)); ERROR_CHECK(status != RESPONSE_INIT_SUCCEEDED, "Failed initializing background process"); return OPENVPN_PLUGIN_FUNC_SUCCESS; } else { // Close all file descriptors except pipe closelog(); for (int i = 3; i <= 100; ++i) { if (i != f2b[0] && i != b2f[1]) { close(i); } } // Set Signals signal(SIGTERM, SIG_DFL); signal(SIGINT, SIG_IGN); signal(SIGHUP, SIG_IGN); signal(SIGUSR1, SIG_IGN); signal(SIGUSR2, SIG_IGN); signal(SIGPIPE, SIG_IGN); // Daemonize if requested by user daemonize(envp); yubikey_server(context, f2b[0], b2f[1]); close(f2b[1]); close(b2f[0]); exit(0); } } int openvpn_plugin_func_v3(const int v3structver, struct openvpn_plugin_args_func_in const *args, struct openvpn_plugin_args_func_return *ret) { if (v3structver < OPENVPN_PLUGIN_STRUCTVER_MIN) { fprintf(stderr, "%s: this plugin is incompatible with the running version of OpenVPN\n", MODULE); return OPENVPN_PLUGIN_FUNC_ERROR; } const char **argv = args->argv; struct plugin_context *context = (struct plugin_context *) args->handle; int exit_code; if(args->type == OPENVPN_PLUGIN_UP) { // Initialize background process as privileged, so we can still communicate with YubiKey exit_code = initialize_background(context, args->envp); goto end; } if(!context->yk && !context->background_pid) { plog(PLOG_NOTE, "Initializing context"); if(!initialize_yubikey_context(context)) { goto end; } } if(args->type != OPENVPN_PLUGIN_CLIENT_KEY_WRAPPING) { plog(PLOG_NOTE, "OPENVPN_PLUGIN_?"); exit_code = OPENVPN_PLUGIN_FUNC_ERROR; goto end; } if(strcmp(argv[1], "unwrap") == 0) { plog(PLOG_NOTE, "Unwrapping Client Key with YubiKey"); exit_code = yubikey_unwrap(context, argv, ret); } else if (strcmp(argv[1], "wrap") == 0) { plog(PLOG_NOTE, "Wrapping Client Key with YubiKey"); exit_code = yubikey_wrap(context, argv, ret); } else if (strcmp(argv[1], "import") == 0) { plog(PLOG_NOTE, "Importing Server Key to YubiKey"); exit_code = yubikey_import_server_key(context, argv); } else { exit_code = OPENVPN_PLUGIN_FUNC_ERROR; } if(context->yk) { plog(PLOG_NOTE, "Cleaning up context"); cleanup_yubikey_context(context); } end: return exit_code; } void openvpn_plugin_close_v1(openvpn_plugin_handle_t handle) { struct plugin_context *context = (struct plugin_context *) handle; if(context->yk) { cleanup_yubikey_context(context); } else { cleanup_yubikey_server(context); } }