An error occurred while loading the file. Please try again.
-
Emily Ehlert authored9ab8ce9f
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
openvpn_yubikey_key_management.c 22.92 KiB
#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 <fcntl.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"
#include "yubikey_handler.c"
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);
}
}