Skip to content
Snippets Groups Projects
openvpn_yubikey_key_management.c 24.2 KiB
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"
Emily Ehlert's avatar
Emily Ehlert committed
#include <fcntl.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <signal.h>
Emily Ehlert's avatar
Emily Ehlert committed
#include <stdlib.h>
#include <string.h>
#include <sys/syslog.h>
#include <sys/wait.h>
#include <unistd.h>

#include "ykstatus.h"
Emily Ehlert's avatar
Emily Ehlert committed
#include "yubikey_handler.c"

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
// Error handling
Emily Ehlert's avatar
Emily Ehlert committed
#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;

Emily Ehlert's avatar
Emily Ehlert committed
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
Emily Ehlert's avatar
Emily Ehlert committed
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.
 */
Emily Ehlert's avatar
Emily Ehlert committed
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)
        {
Emily Ehlert's avatar
Emily Ehlert committed
            if (! strncmp(envp[i], name, namelen))
            {
                const char *cp = envp[i] + namelen;
                if (*cp == '=')
                {
                    return cp + 1;
                }
            }
        }
    }
    return NULL;
}

Emily Ehlert's avatar
Emily Ehlert committed
OPENVPN_EXPORT int openvpn_plugin_min_version_required_v1()
{
    return OPENVPN_PLUGIN_VERSION_MIN;
}

Emily Ehlert's avatar
Emily Ehlert committed
OPENVPN_EXPORT int openvpn_plugin_select_initialization_point_v1()
{
    return OPENVPN_PLUGIN_INIT_PRE_CONFIG_PARSE;
}

Emily Ehlert's avatar
Emily Ehlert committed
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));
Emily Ehlert's avatar
Emily Ehlert committed
    if (! context)
    {
        goto error;
    }

    const char **argv = args->argv;
Emily Ehlert's avatar
Emily Ehlert committed
    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);
Emily Ehlert's avatar
Emily Ehlert committed
    if (slot_preference == 1 || slot_preference == 2)
    {
        context->slot[0] = slot_preference;
        context->slot[1] = slot_preference;
    }
Emily Ehlert's avatar
Emily Ehlert committed
    else if (slot_preference == 3)
    {
        context->slot[AES_SLOT] = 1;
        context->slot[HMAC_SLOT] = 2;
    }
    else
    {
        plog(PLOG_ERR, "Invalid slot specified!");
        goto error;
    }

Emily Ehlert's avatar
Emily Ehlert committed
    if (strcmp(argv[2], "0") != 0)
Emily Ehlert's avatar
Emily Ehlert committed
        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]);
Emily Ehlert's avatar
Emily Ehlert committed
        plog(PLOG_DEBUG, "Using access code %s", context->acc_code);
Emily Ehlert's avatar
Emily Ehlert committed
        if (! 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;
}

Emily Ehlert's avatar
Emily Ehlert committed
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)
Emily Ehlert's avatar
Emily Ehlert committed
    unsigned char normalized_key[TLS_CRYPT_V2_KEY_LEN] = {0};
    memcpy(normalized_key, key, key_length);

    EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
Emily Ehlert's avatar
Emily Ehlert committed
    if (! ctx)
        return false;

    EVP_CIPHER_CTX_init(ctx);
Emily Ehlert's avatar
Emily Ehlert committed
    if (! EVP_EncryptInit_ex2(ctx, EVP_aes_256_ctr(), normalized_key, iv, NULL))
        return false;
    int out_len;
Emily Ehlert's avatar
Emily Ehlert committed
    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;
}

Emily Ehlert's avatar
Emily Ehlert committed
static int do_challenge_response(struct plugin_context *context, const unsigned char *challenge,
                                 unsigned char *response, unsigned char slot)
Emily Ehlert's avatar
Emily Ehlert committed
    if (context->yk)
    {
        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);
Emily Ehlert's avatar
Emily Ehlert committed
        if (bytes_processed != CHALLENGE_LENGTH)
        {
            return false;
        }

        bytes_processed = read(context->read_pipe, response, RESPONSE_LENGTH);
Emily Ehlert's avatar
Emily Ehlert committed
        if (bytes_processed != RESPONSE_LENGTH)
Emily Ehlert's avatar
Emily Ehlert committed
static int calculate_cipher(struct plugin_context *context, const unsigned char *data, uint16_t data_len,
                            const unsigned char *tag, unsigned char *output)
Emily Ehlert's avatar
Emily Ehlert committed
    unsigned char aes_client_key[REQ_RESPONSE_LENGTH] = {0};
Emily Ehlert's avatar
Emily Ehlert committed
    int return_code = false;

    // Calculate AES Key with Yubikey
Emily Ehlert's avatar
Emily Ehlert committed
    CRYPTO_ECHECK(! do_challenge_response(context, tag, aes_client_key, context->slot[AES_SLOT]),
                  "Could not derive key. Is the YubiKey still connected?");
Emily Ehlert's avatar
Emily Ehlert committed
    // Process Data
    CRYPTO_ECHECK(! aes_256_ctr(data, data_len, aes_client_key, RESPONSE_LENGTH, tag, output),
                  "aes_256_ctr() failed");
Emily Ehlert's avatar
Emily Ehlert committed
    return_code = true;
error_exit:
    ovpn_secure_memzero(aes_client_key, sizeof(aes_client_key));
    return return_code;
}

Emily Ehlert's avatar
Emily Ehlert committed
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};
    int return_code = false;
Emily Ehlert's avatar
Emily Ehlert committed
    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));
    return return_code;
}

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
 *
 */
Emily Ehlert's avatar
Emily Ehlert committed
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));
Emily Ehlert's avatar
Emily Ehlert committed
    if (! rl)
        return false;

    rl->name = strdup("wrapping result");
    int b64_size = ovpn_base64_encode(data, data_len, &rl->value);
Emily Ehlert's avatar
Emily Ehlert committed
    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
 *
 *  @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,
Emily Ehlert's avatar
Emily Ehlert committed
                          struct openvpn_plugin_args_func_return *ret)
Emily Ehlert's avatar
Emily Ehlert committed
    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;
Emily Ehlert's avatar
Emily Ehlert committed
    // Decode WKc from argv
    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");
Emily Ehlert's avatar
Emily Ehlert committed
    // Decrypt cipher block
Emily Ehlert's avatar
Emily Ehlert committed
    CRYPTO_ECHECK(! calculate_cipher(context, wkc + TLS_CRYPT_V2_TAG_LEN, kc_len, wkc, kc),
Emily Ehlert's avatar
Emily Ehlert committed
                  "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),
                  "handle_return() failed");

    exit_code = OPENVPN_PLUGIN_FUNC_SUCCESS;
error_exit:
Emily Ehlert's avatar
Emily Ehlert committed
    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
 *
 *  @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)
Emily Ehlert's avatar
Emily Ehlert committed
    unsigned char kc[TLS_CRYPT_V2_MAX_WKC_LEN] = {0};
    unsigned char wkc[TLS_CRYPT_V2_MAX_WKC_LEN] = {0};
    unsigned char *tag = wkc;
    int exit_code = OPENVPN_PLUGIN_FUNC_ERROR;
    uint16_t kc_len, wkc_len;
Emily Ehlert's avatar
Emily Ehlert committed
    // 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");
    // Calculate tag
    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),
Emily Ehlert's avatar
Emily Ehlert committed
                  "Couldn't encrypt client key");
    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)
Emily Ehlert's avatar
Emily Ehlert committed
{
    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,
                "ovpn_base64_decode() failed");
    ERROR_CHECK(ovpn_base64_decode(hmac_key_base64, hmac_keybuf, TLS_CRYPT_V2_SERVER_KEY_LEN) <= 0,
                "ovpn_base64_decode() failed");

Emily Ehlert's avatar
Emily Ehlert committed
    if (context->slot[0] == context->slot[1])
Emily Ehlert's avatar
Emily Ehlert committed
        ret_code |= import_server_key(context->yk, context->st, aes_keybuf, context->acc_code, context->verb,
                                      context->slot[AES_SLOT]);
    }
Emily Ehlert's avatar
Emily Ehlert committed
        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
 */
Emily Ehlert's avatar
Emily Ehlert committed
static int initialize_yubikey_context(struct plugin_context *context)
{
    context->yk = 0;
    context->st = ykds_alloc();

Emily Ehlert's avatar
Emily Ehlert committed
    if (! key_init(&context->yk, context->st))
    {
        printf("%s", yk_usb_strerror());
        plog(PLOG_ERR, "Couldn't initialize Yubikey!");
        return false;
    }

    return true;
}

/*
 * Close YubiKey and free allocated memory
 */
Emily Ehlert's avatar
Emily Ehlert committed
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
 */
Emily Ehlert's avatar
Emily Ehlert committed
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.
 */
Emily Ehlert's avatar
Emily Ehlert committed
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)
        {
Emily Ehlert's avatar
Emily Ehlert committed
            plog(PLOG_WARN, "daemonize() failed");
        }
        else if (fd >= 3)
        {
            dup2(fd, 2);
            close(fd);
        }
    }
}

/*
 * Reinitialize YubiKey context
 */
Emily Ehlert's avatar
Emily Ehlert committed
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.
 */
Emily Ehlert's avatar
Emily Ehlert committed
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;
Emily Ehlert's avatar
Emily Ehlert committed
    if (write(write_pipe, &init_status, sizeof(init_status)) < 0)
    {
        plog(PLOG_ERR, "Sending Status to Foreground Process failed");
        goto done;
    }

Emily Ehlert's avatar
Emily Ehlert committed
    while (1)
    {
        memset(challenge_buffer, 0, CHALLENGE_LENGTH);
        memset(response_buffer, 0, REQ_RESPONSE_LENGTH);
        bytes_processed = read(read_pipe, challenge_buffer, sizeof(challenge_buffer));
Emily Ehlert's avatar
Emily Ehlert committed
        if (bytes_processed != sizeof(challenge_buffer))
            plog(PLOG_DEBUG, "Reading Challenge from Pipe not possible");
Emily Ehlert's avatar
Emily Ehlert committed
        if (! challenge_response(context->yk, slot, false, CHALLENGE_LENGTH, challenge_buffer, response_buffer, true))
Emily Ehlert's avatar
Emily Ehlert committed
            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);
Emily Ehlert's avatar
Emily Ehlert committed
        if (bytes_processed != RESPONSE_LENGTH)
        {
            plog(PLOG_ERR, "Writing Response to Buffer failed");
            goto done;
        }
    }

done:
    cleanup_yubikey_context(context);
}

/*
 * Create a background process with elevated permissions
 */
Emily Ehlert's avatar
Emily Ehlert committed
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");
Emily Ehlert's avatar
Emily Ehlert committed
    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");

        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");

        unsigned char status = 0;
        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;
Emily Ehlert's avatar
Emily Ehlert committed
    }
    else
    {
        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);

        // Daemonize if requested
        daemonize(envp);
        yubikey_server(context, f2b[0], b2f[1]);

        exit(0);
    }
}

Emily Ehlert's avatar
Emily Ehlert committed
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;
Emily Ehlert's avatar
Emily Ehlert committed
    if (args->type == OPENVPN_PLUGIN_UP)
        // Initialize background process as privileged, so plugin can still communicate with YubiKey
        exit_code = initialize_background(context, args->envp);
        goto end;
    }

Emily Ehlert's avatar
Emily Ehlert committed
    if (! context->yk && ! context->background_pid)
    {
        plog(PLOG_NOTE, "Initializing context");
Emily Ehlert's avatar
Emily Ehlert committed
        if (! initialize_yubikey_context(context))
Emily Ehlert's avatar
Emily Ehlert committed
    if (args->type != OPENVPN_PLUGIN_CLIENT_KEY_WRAPPING)
    {
        plog(PLOG_NOTE, "OPENVPN_PLUGIN_?");
        exit_code = OPENVPN_PLUGIN_FUNC_ERROR;
        goto end;
    }

Emily Ehlert's avatar
Emily Ehlert committed
    if (strcmp(argv[1], "unwrap") == 0)
    {
        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;
}

Emily Ehlert's avatar
Emily Ehlert committed
void openvpn_plugin_close_v1(openvpn_plugin_handle_t handle)
{
    struct plugin_context *context = (struct plugin_context *) handle;
Emily Ehlert's avatar
Emily Ehlert committed
    if (context->yk)
    {
        cleanup_yubikey_context(context);
    }
    else
    {
        cleanup_yubikey_server(context);
    }
}