Newer
Older
#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 "openvpn-plugin.h"
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syslog.h>
#include <sys/wait.h>
#include <unistd.h>
#include "ykstatus.h"
static char *MODULE = "OPENVPN_PLUGIN_CLIENT_KEY_WRAPPING";
#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
#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
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;
YK_KEY *yk;
YK_STATUS *st;
pid_t background_pid;
int write_pipe;
int read_pipe;
void *acc_code;
int verb;
};
// Local wrapping of the log function, to add more details
{
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)
{
{
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;
}
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));
{
goto error;
}
const char **argv = args->argv;
{
plog(PLOG_ERR, "Not all args specified!");
goto error;
}
unsigned char slot_preference = (unsigned char) strtol(argv[1], NULL, 10);
{
context->slot[0] = slot_preference;
context->slot[1] = slot_preference;
}
{
context->slot[AES_SLOT] = 1;
context->slot[HMAC_SLOT] = 2;
}
else
{
plog(PLOG_ERR, "Invalid slot specified!");
goto error;
}
unsigned long access_code_length = strlen(argv[2]) + 1;
context->acc_code = calloc(access_code_length, sizeof(char));
snprintf(context->acc_code, access_code_length, "%s", argv[2]);
plog(PLOG_DEBUG, "Using access code %s", context->acc_code);
{
plog(PLOG_ERR, "calloc(acc_code) failed");
goto error;
}
}
// Verbosity
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)
memcpy(normalized_key, key, key_length);
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
return false;
EVP_CIPHER_CTX_init(ctx);
if (! EVP_EncryptInit_ex2(ctx, EVP_aes_256_ctr(), normalized_key, iv, NULL))
if (! EVP_EncryptUpdate(ctx, output, &out_len, data, data_len))
return false;
EVP_CIPHER_CTX_free(ctx);
return true;
}
static int hmac_sha256(unsigned char *hmac_client_key, const unsigned char *data, uint16_t data_len, unsigned char *tag)
{
unsigned char data_buffer[TLS_CRYPT_V2_MAX_WKC_LEN] = {0};
data_buffer[0] = (data_len >> 8) & 0xFF;
data_buffer[1] = data_len & 0xFF;
memcpy(data_buffer + 2, data, data_len);
return HMAC(EVP_sha256(), hmac_client_key, RESPONSE_LENGTH, data_buffer, data_len + TLS_CRYPT_V2_LEN_LEN, tag, NULL) != NULL;
}
static int do_challenge_response(struct plugin_context *context, const unsigned char *challenge,
unsigned char *response, unsigned char slot)
{
plog(PLOG_DEBUG, "Performing local challenge");
return challenge_response(context->yk, slot, false, CHALLENGE_LENGTH, challenge, response, context->verb);
}
else
{
plog(PLOG_DEBUG, "Sending challenge to background process");
ssize_t bytes_processed;
bytes_processed = write(context->write_pipe, challenge, CHALLENGE_LENGTH);
{
return false;
}
bytes_processed = read(context->read_pipe, response, 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)
int return_code = false;
// Calculate AES Key with Yubikey
CRYPTO_ECHECK(! do_challenge_response(context, tag, aes_client_key, context->slot[AES_SLOT]),
"Could not derive key. Is the YubiKey still connected?");
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};
CRYPTO_ECHECK(! do_challenge_response(context, data, hmac_client_key, context->slot[HMAC_SLOT]),
"Could not derive key. Is the YubiKey still connected?");
CRYPTO_ECHECK(! hmac_sha256(hmac_client_key, data, data_len, tag),
"hmac_sha256() failed");
return_code = true;
error_exit:
ovpn_secure_memzero(hmac_client_key, sizeof(hmac_client_key));
}
static uint16_t bytesToShort(const unsigned char *bytes)
{
return *(bytes) << 8 | *(bytes + 1);
}
/**
* Parse data into return structure for OpenVPN
*
* @param ret Pointer to return structure
* @param data Data to be parsed
* @param len Length of data
* @return Returns true on success otherwise false
*
*/
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));
return false;
rl->name = strdup("wrapping result");
int b64_size = ovpn_base64_encode(data, data_len, &rl->value);
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
*
* @param context Pointer to a global plug-in context
* @param wkc_base64 Pointer to wrapped client key in base64 encoding
* @param ret Pointer to return structure, for returning data back to OpenVPN
* @return Return OPENVPN_PLUGIN_FUNC_ERROR on error and OPENVPN_PLUGIN_FUNC_SUCCESS on success
static int yubikey_unwrap(struct plugin_context *context, const char *wkc_base64,
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;
uint16_t wkc_len, kc_len, net_len;
plog(PLOG_DEBUG, "Received WKc: %s", wkc_base64);
wkc_len = ovpn_base64_decode(wkc_base64, wkc, TLS_CRYPT_V2_MAX_WKC_LEN);
CRYPTO_ECHECK(wkc_len < 0,
"ovpn_base64_decode() failed");
kc_len = wkc_len - TLS_CRYPT_V2_TAG_LEN - TLS_CRYPT_V2_LEN_LEN;
CRYPTO_ECHECK(kc_len < 0,
"Invalid Length of WKc");
net_len = bytesToShort(wkc + wkc_len - 2);
CRYPTO_ECHECK(net_len != wkc_len,
"Invalid Declaration of Length for WKc");
CRYPTO_ECHECK(! calculate_cipher(context, wkc + TLS_CRYPT_V2_TAG_LEN, kc_len, wkc, kc),
// 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),
"handle_return() failed");
exit_code = OPENVPN_PLUGIN_FUNC_SUCCESS;
error_exit:
return exit_code;
}
/**
* Wrap a client key by using keys derived from a HMAC-SHA1 key stored inside a YubiKey
*
* @param context Pointer to a global plug-in context
* @param kc_base64 Pointer to client key in base64 encoding
* @param ret Pointer to return structure, for returning data back to OpenVPN
* @return Return OPENVPN_PLUGIN_FUNC_ERROR on error and OPENVPN_PLUGIN_FUNC_SUCCESS on success
static int yubikey_wrap(struct plugin_context *context, const char *kc_base64, 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};
int exit_code = OPENVPN_PLUGIN_FUNC_ERROR;
// Decode Kc from argv
plog(PLOG_DEBUG, "Received Kc: %s", kc_base64);
kc_len = ovpn_base64_decode(kc_base64, kc, TLS_CRYPT_V2_MAX_WKC_LEN);
CRYPTO_ECHECK(kc_len < 0,
"ovpn_base64_decode() failed");
CRYPTO_ECHECK(! calculate_tag(context, kc, kc_len, tag),
"Couldn't calculate tag");
CRYPTO_ECHECK(! calculate_cipher(context, kc, kc_len, tag, wkc + TLS_CRYPT_V2_TAG_LEN),
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),
"handle_return() 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
*
* @param context Pointer to a global plug-in context
* @param aes_key_base64 Pointer to AES server key in base64 encoding
* @param hmac_key_base64 Pointer to HMAC server key in base64 encoding
* @return Return 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 *aes_key_base64, const char *hmac_key_base64)
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(aes_key_base64, aes_keybuf, TLS_CRYPT_V2_SERVER_KEY_LEN) <= 0,
ERROR_CHECK(ovpn_base64_decode(hmac_key_base64, hmac_keybuf, TLS_CRYPT_V2_SERVER_KEY_LEN) <= 0,
"ovpn_base64_decode() failed");
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, 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));
/*
* Allocate YubiKey context and connect to first token
*/
static int initialize_yubikey_context(struct plugin_context *context)
{
context->yk = 0;
context->st = ykds_alloc();
{
printf("%s", yk_usb_strerror());
plog(PLOG_ERR, "Couldn't initialize Yubikey!");
return false;
}
return true;
}
/*
* Close YubiKey and free allocated memory
*/
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();
}
/*
* Close pipes and wait for background process to exit
*/
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.
*/
{
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)
{
}
else if (fd >= 3)
{
dup2(fd, 2);
close(fd);
}
}
}
/*
* Reinitialize YubiKey context
*/
static int try_resurrect(struct plugin_context *context)
{
cleanup_yubikey_context(context);
return initialize_yubikey_context(context);
}
/*
* Main function of background process. Waits for any challenges from plugin, performs them and returns result.
*/
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 slot;
unsigned char challenge_buffer[CHALLENGE_LENGTH];
unsigned char response_buffer[REQ_RESPONSE_LENGTH];
ssize_t bytes_processed;
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;
}
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;
}
{
memset(challenge_buffer, 0, CHALLENGE_LENGTH);
memset(response_buffer, 0, REQ_RESPONSE_LENGTH);
bytes_processed = read(read_pipe, challenge_buffer, sizeof(challenge_buffer));
plog(PLOG_DEBUG, "Reading Challenge from Pipe not possible");
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);
{
plog(PLOG_ERR, "Writing Response to Buffer failed");
goto done;
}
}
done:
cleanup_yubikey_context(context);
}
/*
* Create a background process with elevated permissions
*/
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");
{
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");
int bytes_processed = write(context->write_pipe, &context->slot, sizeof(unsigned char));
ERROR_CHECK(bytes_processed != sizeof(unsigned char),
"Failed sending slot information to background process");
bytes_processed = read(context->read_pipe, &status, sizeof(status));
ERROR_CHECK(bytes_processed != sizeof(status) || status != RESPONSE_INIT_SUCCEEDED,
"Failed initializing background process");
return OPENVPN_PLUGIN_FUNC_SUCCESS;
close(f2b[1]);
close(b2f[0]);
// 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);
yubikey_server(context, f2b[0], b2f[1]);
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 = OPENVPN_PLUGIN_FUNC_ERROR;
// Initialize background process as privileged, so plugin can still communicate with YubiKey
exit_code = initialize_background(context, args->envp);
goto end;
}
{
plog(PLOG_NOTE, "Initializing context");
{
goto end;
}
}
{
plog(PLOG_NOTE, "OPENVPN_PLUGIN_?");
exit_code = OPENVPN_PLUGIN_FUNC_ERROR;
goto end;
}
{
plog(PLOG_NOTE, "Unwrapping Client Key with YubiKey");
exit_code = yubikey_unwrap(context, argv[2], ret);
}
else if (strcmp(argv[1], "wrap") == 0)
{
plog(PLOG_NOTE, "Wrapping Client Key with YubiKey");
exit_code = yubikey_wrap(context, argv[2], ret);
}
else if (strcmp(argv[1], "import") == 0)
{
plog(PLOG_NOTE, "Importing Server Key to YubiKey");
exit_code = yubikey_import_server_key(context, argv[2], argv[3]);
}
else
{
exit_code = OPENVPN_PLUGIN_FUNC_ERROR;
}
end:
return exit_code;
}
void openvpn_plugin_close_v1(openvpn_plugin_handle_t handle)
{
struct plugin_context *context = (struct plugin_context *) handle;
{
cleanup_yubikey_context(context);
}
else
{
cleanup_yubikey_server(context);
}
}