Fetching contributors…
Cannot retrieve contributors at this time
1554 lines (1321 sloc) 35.5 KB
/* ykclient.c --- Implementation of Yubikey OTP validation client library.
*
* Written by Simon Josefsson <simon@josefsson.org>.
* Copyright (c) 2006-2013 Yubico AB
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
#include "ykclient.h"
#include "ykclient_server_response.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <curl/curl.h>
#include "sha.h"
#include "cencode.h"
#include "cdecode.h"
#define NONCE_LEN 32
#define MAX_TEMPLATES 255
#define ADD_NONCE "&nonce="
#define ADD_OTP "&otp="
#define ADD_HASH "&h="
#define ADD_ID "?id="
#define ADD_TS "&timestamp=1"
#define TEMPLATE_FORMAT_OLD 1
#define TEMPLATE_FORMAT_NEW 2
struct ykclient_st
{
const char *ca_path;
const char *ca_info;
const char *proxy;
size_t num_templates;
char **url_templates;
int template_format;
char last_url[256];
unsigned int client_id;
size_t keylen;
const char *key;
char *key_buf;
char *nonce;
char nonce_supplied;
int verify_signature;
ykclient_server_response_t *srv_response;
};
struct curl_data
{
char *curl_chunk;
size_t curl_chunk_size;
};
struct ykclient_handle_st
{
CURL **easy;
CURLM *multi;
size_t num_easy;
struct curl_data *data;
char **url_exp;
};
static const char *default_url_templates[] = {
"https://api.yubico.com/wsapi/2.0/verify",
"https://api2.yubico.com/wsapi/2.0/verify",
"https://api3.yubico.com/wsapi/2.0/verify",
"https://api4.yubico.com/wsapi/2.0/verify",
"https://api5.yubico.com/wsapi/2.0/verify",
};
static const char *user_agent = PACKAGE "/" PACKAGE_VERSION;
/** Initialise the global context for the library
*
* This function is not thread safe. It must be invoked successfully
* before using any other function.
*
* @return one of the YKCLIENT_* values or YKCLIENT_OK on success.
*/
ykclient_rc
ykclient_global_init (void)
{
if (curl_global_init (CURL_GLOBAL_ALL) != 0)
return YKCLIENT_CURL_INIT_ERROR;
return YKCLIENT_OK;
}
/** Deinitialise the global context for the library
*
* This function is not thread safe. After this function has been
* called, no other library functions may be used reliably.
*
* @return one of the YKCLIENT_* values or YKCLIENT_OK on success.
*/
void
ykclient_global_done (void)
{
curl_global_cleanup ();
}
/** Initialise a new configuration structure
*
* Additional options can be set with ykclient_set_* functions
* after memory for the configuration has been allocated with
* this function.
*
* @param ykc Where to write a pointer to the new configuration.
* @return one of the YKCLIENT_* values or YKCLIENT_OK on success.
*/
ykclient_rc
ykclient_init (ykclient_t ** ykc)
{
ykclient_t *p;
p = malloc (sizeof (*p));
if (!p)
{
return YKCLIENT_OUT_OF_MEMORY;
}
memset (p, 0, (sizeof (*p)));
p->ca_path = NULL;
p->ca_info = NULL;
p->proxy = NULL;
p->key = NULL;
p->keylen = 0;
p->key_buf = NULL;
memset (p->last_url, 0, sizeof (p->last_url));
p->nonce = NULL;
p->nonce_supplied = 0;
p->srv_response = NULL;
/*
* Verification of server signature can only be done if there is
* an API key provided
*/
p->verify_signature = 0;
/*
* Set the default URLs (these can be overridden later)
*/
ykclient_set_url_bases (p,
sizeof (default_url_templates) /
sizeof (char *), default_url_templates);
*ykc = p;
return YKCLIENT_OK;
}
/** Frees a configuration structure allocated by ykclient_init
*
* Any handles created with ykclient_handle_init must be freed
* separately with ykclient_handle_done.
*
* @param ykc configuration to free.
*/
void
ykclient_done (ykclient_t ** ykc)
{
if (ykc && *ykc)
{
if ((*ykc)->url_templates)
{
size_t i;
for (i = 0; i < (*ykc)->num_templates; i++)
{
free ((*ykc)->url_templates[i]);
}
free ((*ykc)->url_templates);
}
if ((*ykc)->srv_response)
{
ykclient_server_response_free((*ykc)->srv_response);
}
free ((*ykc)->key_buf);
free (*ykc);
}
if (ykc)
{
*ykc = NULL;
}
}
/** Callback for processing CURL data received from the validation server
*
*/
static size_t
curl_callback (void *ptr, size_t size, size_t nmemb, void *data)
{
struct curl_data *curl_data = (struct curl_data *) data;
size_t realsize = size * nmemb;
char *p;
if (curl_data->curl_chunk)
{
p = realloc (curl_data->curl_chunk,
curl_data->curl_chunk_size + realsize + 1);
}
else
{
p = malloc (curl_data->curl_chunk_size + realsize + 1);
}
if (!p)
{
return 0;
}
curl_data->curl_chunk = p;
memcpy (&(curl_data->curl_chunk[curl_data->curl_chunk_size]), ptr,
realsize);
curl_data->curl_chunk_size += realsize;
curl_data->curl_chunk[curl_data->curl_chunk_size] = 0;
return realsize;
}
/** Create a new handle
*
* These handles contain curl easy and multi handles required to
* process a request.
*
* This must be called after configuring template URLs, and handles
* MUST NOT be reused if new template URLs have been set.
*
* If new template URLs have been set all handles must be destroyed
* with ykclient_handle_close and recreated with this function.
*
* Handles must be cleaned with ykclient_handle_cleanup between
* requests, and closed with ykclient_handle_close when they are no
* longer needed.
*
* @param ykc Yubikey client configuration.
* @param[out] ykh where to write pointer to new handle.
* @return one of the YKCLIENT_* values or YKCLIENT_OK on success.
*/
ykclient_rc
ykclient_handle_init (ykclient_t * ykc, ykclient_handle_t ** ykh)
{
ykclient_handle_t *p;
*ykh = NULL;
p = malloc (sizeof (*p));
if (!p)
{
return YKCLIENT_OUT_OF_MEMORY;
}
memset (p, 0, (sizeof (*p)));
p->multi = curl_multi_init ();
if (!p->multi)
{
free (p);
return YKCLIENT_CURL_INIT_ERROR;
}
p->easy = malloc (sizeof (CURL *) * ykc->num_templates);
for (p->num_easy = 0; p->num_easy < ykc->num_templates; p->num_easy++)
{
CURL *easy;
struct curl_data *data;
data = malloc (sizeof (*data));
if (!data)
{
ykclient_handle_done (&p);
return YKCLIENT_OUT_OF_MEMORY;
}
data->curl_chunk = NULL;
data->curl_chunk_size = 0;
easy = curl_easy_init ();
if (!easy)
{
free (data);
ykclient_handle_done (&p);
return YKCLIENT_CURL_INIT_ERROR;
}
if (ykc->ca_path)
{
curl_easy_setopt (easy, CURLOPT_CAPATH, ykc->ca_path);
}
if (ykc->ca_info)
{
curl_easy_setopt (easy, CURLOPT_CAINFO, ykc->ca_info);
}
if (ykc->proxy)
{
/*
* The proxy string may be prefixed with [scheme]://ip:port to specify which kind of proxy is used.
* Valid choices are: socks4://, socks4a://, socks5:// or socks5h://
* Use socks5h to ask the proxy to do the dns resolving.
* If no scheme or port is specified HTTP proxy port 1080 will be used.
*/
curl_easy_setopt (easy, CURLOPT_PROXY, ykc->proxy);
}
curl_easy_setopt (easy, CURLOPT_WRITEDATA, (void *) data);
curl_easy_setopt (easy, CURLOPT_PRIVATE, (void *) data);
curl_easy_setopt (easy, CURLOPT_WRITEFUNCTION, curl_callback);
curl_easy_setopt (easy, CURLOPT_USERAGENT, user_agent);
curl_multi_add_handle (p->multi, easy);
p->easy[p->num_easy] = easy;
}
if(p->num_easy == 0) {
ykclient_handle_done (&p);
return YKCLIENT_BAD_INPUT;
}
/* Take this opportunity to allocate the array for expanded URLs */
p->url_exp = malloc (sizeof (char *) * p->num_easy);
if (!p->url_exp)
{
ykclient_handle_done (&p);
return YKCLIENT_OUT_OF_MEMORY;
}
memset (p->url_exp, 0, (sizeof (char *) * p->num_easy));
*ykh = p;
return YKCLIENT_OK;
}
/** Cleanup memory allocated for requests
*
* Cleans up any memory allocated and held by the handle for a
* request. Must be called after each request.
*
* @param ykh to close.
*/
void
ykclient_handle_cleanup (ykclient_handle_t * ykh)
{
size_t i;
struct curl_data *data;
int requests = 0;
/*
* Curl will not allow a connection to be re-used unless the
* request finished, call curl_multi_perform one last time
* to give libcurl an opportunity to mark the request as
* complete.
*
* If the delay between yk_request_send and
* ykclient_handle_cleanup is sufficient to allow the request
* to complete, the connection can be re-used, else it will
* be re-established on next yk_request_send.
*/
(void) curl_multi_perform (ykh->multi, &requests);
for (i = 0; i < ykh->num_easy; i++)
{
free (ykh->url_exp[i]);
ykh->url_exp[i] = NULL;
curl_easy_getinfo (ykh->easy[i], CURLINFO_PRIVATE, (char **) &data);
free (data->curl_chunk);
data->curl_chunk = NULL;
data->curl_chunk_size = 0;
curl_multi_remove_handle (ykh->multi, ykh->easy[i]);
curl_multi_add_handle (ykh->multi, ykh->easy[i]);
}
}
/** Close a handle freeing memory and destroying connections
*
* Frees any memory allocated for the handle, and calls various CURL
* functions to destroy all curl easy and multi handles created for
* this handle.
*
* @param ykh to close.
*/
void
ykclient_handle_done (ykclient_handle_t ** ykh)
{
struct curl_data *data;
size_t i;
if (ykh && *ykh)
{
ykclient_handle_cleanup (*ykh);
for (i = 0; i < (*ykh)->num_easy; i++)
{
curl_easy_getinfo ((*ykh)->easy[i], CURLINFO_PRIVATE,
(char **) &data);
free (data);
curl_multi_remove_handle ((*ykh)->multi, (*ykh)->easy[i]);
curl_easy_cleanup ((*ykh)->easy[i]);
}
if ((*ykh)->multi)
{
curl_multi_cleanup ((*ykh)->multi);
}
free ((*ykh)->url_exp);
free ((*ykh)->easy);
free (*ykh);
}
if (ykh)
{
*ykh = NULL;
}
}
void
ykclient_set_verify_signature (ykclient_t * ykc, int value)
{
if (ykc == NULL)
{
return;
}
ykc->verify_signature = value;
}
void
ykclient_set_client (ykclient_t * ykc,
unsigned int client_id, size_t keylen, const char *key)
{
ykc->client_id = client_id;
ykc->keylen = keylen;
ykc->key = key;
}
ykclient_rc
ykclient_set_client_hex (ykclient_t * ykc,
unsigned int client_id, const char *key)
{
size_t i;
size_t key_len;
size_t bin_len;
ykc->client_id = client_id;
if (key == NULL)
{
return YKCLIENT_OK;
}
key_len = strlen (key);
if (key_len % 2 != 0)
{
return YKCLIENT_HEX_DECODE_ERROR;
}
bin_len = key_len / 2;
free (ykc->key_buf);
ykc->key_buf = malloc (bin_len);
if (!ykc->key_buf)
{
return YKCLIENT_OUT_OF_MEMORY;
}
for (i = 0; i < bin_len; i++)
{
int tmp;
if (sscanf (&key[2 * i], "%02x", &tmp) != 1)
{
free (ykc->key_buf);
ykc->key_buf = NULL;
return YKCLIENT_HEX_DECODE_ERROR;
}
ykc->key_buf[i] = tmp;
}
ykc->keylen = bin_len;
ykc->key = ykc->key_buf;
return YKCLIENT_OK;
}
ykclient_rc
ykclient_set_client_b64 (ykclient_t * ykc,
unsigned int client_id, const char *key)
{
size_t key_len;
ssize_t dec_len;
base64_decodestate b64;
ykc->client_id = client_id;
if (key == NULL)
{
return YKCLIENT_OK;
}
key_len = strlen (key);
free (ykc->key_buf);
ykc->key_buf = malloc (key_len + 1);
if (!ykc->key_buf)
{
return YKCLIENT_OUT_OF_MEMORY;
}
base64_init_decodestate (&b64);
dec_len = (ssize_t) base64_decode_block (key, key_len, ykc->key_buf, &b64);
if (dec_len < 0)
{
return YKCLIENT_BASE64_DECODE_ERROR;
}
ykc->keylen = (size_t) dec_len;
ykc->key = ykc->key_buf;
/* Now that we have a key the sensible default is to verify signatures */
ykc->verify_signature = 1;
return YKCLIENT_OK;
}
/** Set the CA path
*
* Must be called before creating handles.
*/
void
ykclient_set_ca_path (ykclient_t * ykc, const char *ca_path)
{
ykc->ca_path = ca_path;
}
/** Set the CA info, needed for linking with GnuTLS
*
* Must be called before creating handles.
*/
void
ykclient_set_ca_info (ykclient_t * ykc, const char *ca_info)
{
ykc->ca_info = ca_info;
}
/** Set the proxy
*
* Must be called before creating handles.
*/
void
ykclient_set_proxy (ykclient_t * ykc, const char *proxy)
{
ykc->proxy = proxy;
}
/** Set a single URL template
*
* @param ykc Yubikey client configuration.
* @param url_template to set.
* @return one of the YKCLIENT_* values or YKCLIENT_OK on success.
*/
ykclient_rc
ykclient_set_url_template (ykclient_t * ykc, const char *url_template)
{
return ykclient_set_url_templates (ykc, 1, (const char **) &url_template);
}
/** Set the URLs of the YK validation servers
*
* The URL strings will be copied to the new buffers, so the
* caller may free the original URL strings if they are no
* longer needed.
*
* @note This function MUST be called before calling ykclient_handle_init
*
* @param ykc Yubikey client configuration.
* @param num_templates Number of template URLs passed in url_templates.
* @param url_templates Array of template URL strings.
* @return one of the YKCLIENT_* values or YKCLIENT_OK on success.
*/
ykclient_rc
ykclient_set_url_templates (ykclient_t * ykc, size_t num_templates,
const char **url_templates)
{
ykclient_rc ret =
ykclient_set_url_bases (ykc, num_templates, url_templates);
if (ret == YKCLIENT_OK)
{
ykc->template_format = TEMPLATE_FORMAT_OLD;
}
return ret;
}
ykclient_rc
ykclient_set_url_bases (ykclient_t * ykc, size_t num_templates,
const char **url_templates)
{
size_t i;
if (num_templates > MAX_TEMPLATES)
{
return YKCLIENT_BAD_INPUT;
}
/* Clean out any previously allocated templates */
if (ykc->url_templates)
{
for (i = 0; i < ykc->num_templates; i++)
{
free (ykc->url_templates[i]);
}
free (ykc->url_templates);
}
/* Reallocate the template array */
ykc->url_templates = malloc (sizeof (char *) * num_templates);
if (!ykc->url_templates)
{
return YKCLIENT_OUT_OF_MEMORY;
}
memset (ykc->url_templates, 0, (sizeof (char *) * num_templates));
for (ykc->num_templates = 0; ykc->num_templates < num_templates;
ykc->num_templates++)
{
ykc->url_templates[ykc->num_templates] =
strdup (url_templates[ykc->num_templates]);
if (!ykc->url_templates[ykc->num_templates])
{
return YKCLIENT_OUT_OF_MEMORY;
}
}
ykc->template_format = TEMPLATE_FORMAT_NEW;
return YKCLIENT_OK;
}
/*
* Set the nonce. A default nonce is generated in ykclient_init (), but
* if you either want to specify your own nonce, or want to remove the
* nonce (needed to send signed requests to v1 validation servers),
* you must call this function. Set nonce to NULL to disable it.
*/
void
ykclient_set_nonce (ykclient_t * ykc, char *nonce)
{
ykc->nonce_supplied = 1;
ykc->nonce = nonce;
}
/** Convert a ykclient_rc value to a string
*
* Returns a more verbose error message relating to the ykclient_rc
* value passed as ret.
*
* @param ret the error code to convert.
* @return verbose error string.
*/
const char *
ykclient_strerror (ykclient_rc ret)
{
const char *p;
switch (ret)
{
case YKCLIENT_OK:
p = "Success";
break;
case YKCLIENT_CURL_PERFORM_ERROR:
p = "Error performing curl";
break;
case YKCLIENT_BAD_OTP:
p = "Yubikey OTP was bad (BAD_OTP)";
break;
case YKCLIENT_REPLAYED_OTP:
p = "Yubikey OTP was replayed (REPLAYED_OTP)";
break;
case YKCLIENT_REPLAYED_REQUEST:
p = "Yubikey request was replayed (REPLAYED_REQUEST)";
break;
case YKCLIENT_BAD_SIGNATURE:
p = "Request signature was invalid (BAD_SIGNATURE)";
break;
case YKCLIENT_BAD_SERVER_SIGNATURE:
p = "Server response signature was invalid (BAD_SERVER_SIGNATURE)";
break;
case YKCLIENT_MISSING_PARAMETER:
p = "Request was missing a parameter (MISSING_PARAMETER)";
break;
case YKCLIENT_NO_SUCH_CLIENT:
p = "Client identity does not exist (NO_SUCH_CLIENT)";
break;
case YKCLIENT_OPERATION_NOT_ALLOWED:
p = "Authorization denied (OPERATION_NOT_ALLOWED)";
break;
case YKCLIENT_BACKEND_ERROR:
p = "Internal server error (BACKEND_ERROR)";
break;
case YKCLIENT_NOT_ENOUGH_ANSWERS:
p = "Too few validation servers available (NOT_ENOUGH_ANSWERS)";
break;
case YKCLIENT_OUT_OF_MEMORY:
p = "Out of memory";
break;
case YKCLIENT_PARSE_ERROR:
p = "Could not parse server response";
break;
case YKCLIENT_FORMAT_ERROR:
p = "Internal printf format error";
break;
case YKCLIENT_CURL_INIT_ERROR:
p = "Error initializing curl";
break;
case YKCLIENT_HMAC_ERROR:
p = "HMAC signature validation/generation error";
break;
case YKCLIENT_HEX_DECODE_ERROR:
p = "Error decoding hex string";
break;
case YKCLIENT_BASE64_DECODE_ERROR:
p = "Error decoding base64 string";
break;
case YKCLIENT_NOT_IMPLEMENTED:
p = "Not implemented";
break;
case YKCLIENT_HANDLE_NOT_REINIT:
p = "Request template URLs modified without reinitialising handles";
break;
case YKCLIENT_BAD_INPUT:
p = "Passed invalid or incorrect number of parameters";
break;
default:
p = "Unknown error";
}
return p;
}
/** Generates or duplicates an existing nonce value
*
* If a nonce value was set with ykclient_set_nonce, it will be duplicated
* and a pointer to the memory returned in nonce.
*
* If a nonce value has not been set a new buffer will be allocated and a
* random string of NONCE_LEN will be written to it.
*
* Memory pointed to by nonce must be freed by the called when it is no
* longer requiest.
*
* @param ykc Yubikey client configuration.
* @param[out] nonce where to write the pointer to the nonce value.
* @return one of the YKCLIENT_* values or YKCLIENT_OK on success.
*/
static ykclient_rc
ykclient_generate_nonce (ykclient_t * ykc, char **nonce)
{
*nonce = NULL;
/*
* If we were supplied with a static value just strdup,
* makes memory management easier.
*/
if (ykc->nonce_supplied)
{
if (ykc->nonce)
{
*nonce = strdup (ykc->nonce);
if (*nonce == NULL)
return YKCLIENT_OUT_OF_MEMORY;
}
}
/*
* Generate a random 'nonce' value
*/
else
{
struct timeval tv;
size_t i;
char *p;
p = malloc (NONCE_LEN + 1);
if (!p)
{
return YKCLIENT_OUT_OF_MEMORY;
}
gettimeofday (&tv, 0);
srandom (tv.tv_sec * tv.tv_usec);
for (i = 0; i < NONCE_LEN; ++i)
{
p[i] = (random () % 26) + 'a';
}
p[NONCE_LEN] = 0;
*nonce = p;
}
return YKCLIENT_OK;
}
static ykclient_rc
ykclient_expand_new_url (const char *template,
const char *encoded_otp, const char *nonce,
int client_id, char **url_exp)
{
size_t len =
strlen (template) + strlen (encoded_otp) + strlen (ADD_OTP) +
strlen (ADD_ID) + strlen(ADD_TS) + 1;
len += snprintf (NULL, 0, "%d", client_id);
if (nonce)
{
len += strlen (nonce) + strlen (ADD_NONCE);
}
*url_exp = malloc (len);
if (!*url_exp)
{
return YKCLIENT_OUT_OF_MEMORY;
}
if (nonce)
{
snprintf (*url_exp, len, "%s" ADD_ID "%d" ADD_NONCE "%s" ADD_OTP "%s" ADD_TS,
template, client_id, nonce, encoded_otp);
}
else
{
snprintf (*url_exp, len, "%s" ADD_ID "%d" ADD_OTP "%s" ADD_TS, template,
client_id, encoded_otp);
}
return YKCLIENT_OK;
}
static ykclient_rc
ykclient_expand_old_url (const char *template,
const char *encoded_otp, const char *nonce,
int client_id, char **url_exp)
{
{
size_t len;
ssize_t wrote;
len = strlen (template) + strlen (encoded_otp) + 20;
*url_exp = malloc (len);
if (!*url_exp)
{
return YKCLIENT_OUT_OF_MEMORY;
}
wrote = snprintf (*url_exp, len, template, client_id, encoded_otp);
if (wrote < 0 || (size_t) wrote > len)
{
return YKCLIENT_FORMAT_ERROR;
}
}
if (nonce)
{
/* Create new URL with nonce in it. */
char *nonce_url, *otp_offset;
size_t len;
ssize_t wrote;
len = strlen (*url_exp) + strlen (ADD_NONCE) + strlen (nonce) + 1;
nonce_url = malloc (len + 4); /* avoid valgrind complaint */
if (!nonce_url)
{
return YKCLIENT_OUT_OF_MEMORY;
}
/* Find the &otp= in url and insert ?nonce= before otp. Must get
* sorted headers since we calculate HMAC on the result.
*
* XXX this will break if the validation protocol gets a parameter that
* sorts in between "nonce" and "otp", because the headers we sign won't
* be alphabetically sorted if we insert the nonce between "nz" and "otp".
* Also, we assume that everyone will have at least one parameter ("id=")
* before "otp" so there is no need to search for "?otp=".
*/
otp_offset = strstr (*url_exp, ADD_OTP);
if (otp_offset == NULL)
{
/* point at \0 at end of url in case there is no otp */
otp_offset = *url_exp + len;
}
/* break up ykc->url where we want to insert nonce */
*otp_offset = 0;
wrote = snprintf (nonce_url, len, "%s" ADD_NONCE "%s&%s", *url_exp,
nonce, otp_offset + 1);
if (wrote < 0 || (size_t) wrote + 1 != len)
{
free (nonce_url);
return YKCLIENT_FORMAT_ERROR;
}
free (*url_exp);
*url_exp = nonce_url;
}
return YKCLIENT_OK;
}
/** Expand URL templates specified with set_url_templates
*
* Expands placeholderss or inserts additional parameters for nonce,
* OTP, and signing values into duplicates of URL templates.
*
* The memory allocated for these duplicates must be freed
* by calling either ykclient_handle_done or ykclient_handle_cleanup
* after they are no longer needed.
*
* @param ykc Yubikey client configuration.
* @param ykh Yubikey client handle.
* @param yubikey OTP string passed to the client.
* @param nonce Random value included in the request and validated in the response.
* @return one of the YKCLIENT_* values or YKCLIENT_OK on success.
*/
static ykclient_rc
ykclient_expand_urls (ykclient_t * ykc, ykclient_handle_t * ykh,
const char *yubikey, const char *nonce)
{
ykclient_rc out = YKCLIENT_OK;
size_t i, j;
char *signature = NULL;
char *encoded_otp = NULL;
/* The handle must have the same number of easy handles as we have templates */
if (ykc->num_templates != ykh->num_easy)
{
return YKCLIENT_HANDLE_NOT_REINIT;
}
for (i = 0; i < ykc->num_templates; i++)
{
ykclient_rc ret;
if (!encoded_otp)
{
/* URL-encode the OTP */
encoded_otp = curl_easy_escape (ykh->easy[i], yubikey, 0);
}
if (ykc->template_format == TEMPLATE_FORMAT_OLD)
{
ret = ykclient_expand_old_url (ykc->url_templates[i],
encoded_otp, nonce, ykc->client_id,
&ykh->url_exp[i]);
}
else
{
ret = ykclient_expand_new_url (ykc->url_templates[i],
encoded_otp, nonce, ykc->client_id,
&ykh->url_exp[i]);
}
if (ret != YKCLIENT_OK)
{
out = ret;
goto finish;
}
if (ykc->key && ykc->keylen)
{
if (!signature)
{
char b64dig[3 * 4 * SHA1HashSize + 1];
uint8_t digest[USHAMaxHashSize];
base64_encodestate b64;
char *text;
int res, res2;
/* Find parameters to sign. */
text = strchr (ykh->url_exp[i], '?');
if (!text)
{
out = YKCLIENT_PARSE_ERROR;
goto finish;
}
text++;
/* HMAC data. */
res = hmac (SHA1, (unsigned char *) text, strlen (text),
(const unsigned char *) ykc->key, ykc->keylen,
digest);
if (res != shaSuccess)
{
out = YKCLIENT_HMAC_ERROR;
goto finish;
}
/* Base64 signature. */
base64_init_encodestate (&b64);
res =
base64_encode_block ((char *) digest, SHA1HashSize, b64dig,
&b64);
res2 = base64_encode_blockend (&b64dig[res], &b64);
b64dig[res + res2 - 1] = '\0';
signature = curl_easy_escape (ykh->easy[i], b64dig, 0);
}
/* Create new URL with signature ( h= ) appended to it . */
{
char *sign_url;
size_t len;
ssize_t wrote;
len =
strlen (ykh->url_exp[i]) + strlen (ADD_HASH) +
strlen (signature) + 1;
sign_url = malloc (len);
if (!sign_url)
{
free (sign_url);
out = YKCLIENT_OUT_OF_MEMORY;
goto finish;
}
wrote =
snprintf (sign_url, len, "%s" ADD_HASH "%s", ykh->url_exp[i],
signature);
if (wrote < 0 || (size_t) wrote + 1 != len)
{
free (sign_url);
out = YKCLIENT_FORMAT_ERROR;
goto finish;
}
free (ykh->url_exp[i]);
ykh->url_exp[i] = sign_url;
}
}
curl_easy_setopt (ykh->easy[i], CURLOPT_URL, ykh->url_exp[i]);
}
finish:
if (encoded_otp)
{
curl_free (encoded_otp);
}
if (signature)
{
curl_free (signature);
}
/* On error free any URLs we previously built */
if (out != YKCLIENT_OK)
{
for (j = 0; j < i; j++)
{
free (ykh->url_exp[j]);
ykh->url_exp[j] = NULL;
}
}
return out;
}
/** Convert the response from the validation server into a ykclient_rc
*
* @param status message from the validation server.
* @return one of the YKCLIENT_* values.
*/
static ykclient_rc
ykclient_parse_srv_error (const char *status)
{
if (strcmp (status, "OK") == 0)
{
return YKCLIENT_OK;
}
else if (strcmp (status, "BAD_OTP") == 0)
{
return YKCLIENT_BAD_OTP;
}
else if (strcmp (status, "REPLAYED_OTP") == 0)
{
return YKCLIENT_REPLAYED_OTP;
}
else if (strcmp (status, "REPLAYED_REQUEST") == 0)
{
return YKCLIENT_REPLAYED_REQUEST;
}
else if (strcmp (status, "BAD_SIGNATURE") == 0)
{
return YKCLIENT_BAD_SIGNATURE;
}
else if (strcmp (status, "MISSING_PARAMETER") == 0)
{
return YKCLIENT_MISSING_PARAMETER;
}
else if (strcmp (status, "NO_SUCH_CLIENT") == 0)
{
return YKCLIENT_NO_SUCH_CLIENT;
}
else if (strcmp (status, "OPERATION_NOT_ALLOWED") == 0)
{
return YKCLIENT_OPERATION_NOT_ALLOWED;
}
else if (strcmp (status, "BACKEND_ERROR") == 0)
{
return YKCLIENT_BACKEND_ERROR;
}
else if (strcmp (status, "NOT_ENOUGH_ANSWERS") == 0)
{
return YKCLIENT_NOT_ENOUGH_ANSWERS;
}
return YKCLIENT_PARSE_ERROR;
}
/** Send requests to one or more validation servers and processes the response
*
* @param ykc Yubikey client configuration.
* @param ykh Yubikey client handle.
* @param yubikey OTP string passed to the client.
* @param nonce Random value included in the request and validated in the response.
* @return one of the YKCLIENT_* values or YKCLIENT_OK on success.
*/
static ykclient_rc
ykclient_request_send (ykclient_t * ykc, ykclient_handle_t * ykh,
const char *yubikey, char *nonce)
{
ykclient_rc out = YKCLIENT_OK;
int requests;
if (!ykc->num_templates)
{
return YKCLIENT_MISSING_PARAMETER;
}
/* The handle must have the same number of easy handles as we have templates */
if (ykc->num_templates != ykh->num_easy)
{
return YKCLIENT_HANDLE_NOT_REINIT;
}
memset (ykc->last_url, 0, sizeof (ykc->last_url));
/* Perform the request */
do
{
int msgs = 1;
CURLMcode curl_ret = curl_multi_perform (ykh->multi, &requests);
struct timeval timeout;
fd_set fdread;
fd_set fdwrite;
fd_set fdexcep;
int maxfd = -1;
long curl_timeo = -1;
/* curl before 7.20.0 can return CURLM_CALL_MULTI_PERFORM, continue so we
* call curl_multi_perform again. */
if (curl_ret == CURLM_CALL_MULTI_PERFORM)
{
continue;
}
if (curl_ret != CURLM_OK)
{
fprintf (stderr, "Error with curl: %s\n",
curl_multi_strerror (curl_ret));
out = YKCLIENT_CURL_PERFORM_ERROR;
goto finish;
}
FD_ZERO (&fdread);
FD_ZERO (&fdwrite);
FD_ZERO (&fdexcep);
memset (&timeout, 0, sizeof (timeout));
timeout.tv_sec = 0;
timeout.tv_usec = 250000;
curl_multi_timeout (ykh->multi, &curl_timeo);
if (curl_timeo >= 0)
{
timeout.tv_sec = curl_timeo / 1000;
if (timeout.tv_sec > 1)
{
timeout.tv_sec = 0;
timeout.tv_usec = 250000;
}
else
{
timeout.tv_usec = (curl_timeo % 1000) * 1000;
}
}
curl_multi_fdset (ykh->multi, &fdread, &fdwrite, &fdexcep, &maxfd);
select (maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout);
while (msgs)
{
CURL *curl_easy;
struct curl_data *data;
char *url_used;
char *status;
CURLMsg *msg;
msg = curl_multi_info_read (ykh->multi, &msgs);
if (!msg || msg->msg != CURLMSG_DONE)
{
continue;
}
/* if we get anything other than CURLE_OK we throw away this result */
if (msg->data.result != CURLE_OK)
{
out = YKCLIENT_CURL_PERFORM_ERROR;
continue;
}
curl_easy = msg->easy_handle;
curl_easy_getinfo (curl_easy, CURLINFO_PRIVATE, (char **) &data);
if (data == 0 || data->curl_chunk_size == 0 ||
data->curl_chunk == NULL)
{
out = YKCLIENT_PARSE_ERROR;
goto finish;
}
curl_easy_getinfo (curl_easy, CURLINFO_EFFECTIVE_URL, &url_used);
strncpy (ykc->last_url, url_used, sizeof (ykc->last_url));
if(ykc->srv_response)
{
ykclient_server_response_free (ykc->srv_response);
}
ykc->srv_response = ykclient_server_response_init ();
if (ykc->srv_response == NULL)
{
out = YKCLIENT_PARSE_ERROR;
goto finish;
}
out = ykclient_server_response_parse (data->curl_chunk,
ykc->srv_response);
if (out != YKCLIENT_OK)
{
goto finish;
}
if (ykc->verify_signature != 0 &&
ykclient_server_response_verify_signature (ykc->srv_response,
ykc->key,
ykc->keylen))
{
out = YKCLIENT_BAD_SERVER_SIGNATURE;
goto finish;
}
status = ykclient_server_response_get (ykc->srv_response, "status");
if (!status)
{
out = YKCLIENT_PARSE_ERROR;
goto finish;
}
out = ykclient_parse_srv_error (status);
if (out == YKCLIENT_OK)
{
char *server_otp;
/* Verify that the OTP and nonce we put in our request is echoed
* in the response.
*
* This is to protect us from a man in the middle sending us a
* previously seen genuine response again (such as an status=OK
* response even though the real server will respond
* status=REPLAYED_OTP in a few milliseconds.
*/
if (nonce)
{
char *server_nonce =
ykclient_server_response_get (ykc->srv_response,
"nonce");
if (server_nonce == NULL || strcmp (nonce, server_nonce))
{
out = YKCLIENT_HMAC_ERROR;
goto finish;
}
}
server_otp = ykclient_server_response_get (ykc->srv_response, "otp");
if (server_otp == NULL || strcmp (yubikey, server_otp))
{
out = YKCLIENT_HMAC_ERROR;
}
goto finish;
}
else if ((out != YKCLIENT_PARSE_ERROR) &&
(out != YKCLIENT_REPLAYED_REQUEST))
{
goto finish;
}
ykclient_server_response_free (ykc->srv_response);
ykc->srv_response = NULL;
}
}
while (requests);
finish:
return out;
}
/** Returns the actual URL the request was sent to
*
* @param ykc Yubikey client configuration.
* @return the last URL a request was send to.
*/
const char *
ykclient_get_last_url (ykclient_t * ykc)
{
return ykc->last_url;
}
/** Generates and send requests to one or more validation servers
*
* Sends a request to each of the servers specified by set_url_templates and
* validates the response.
*
* @param ykc Yubikey client configuration.
* @param yubikey OTP string passed to the client.
* @return one of the YKCLIENT_* values or YKCLIENT_OK on success.
*/
ykclient_rc
ykclient_request_process (ykclient_t * ykc, ykclient_handle_t * ykh,
const char *yubikey)
{
ykclient_rc out;
char *nonce = NULL;
/* Generate nonce value */
out = ykclient_generate_nonce (ykc, &nonce);
if (out != YKCLIENT_OK)
{
goto finish;
}
/* Build request/template specific URLs */
out = ykclient_expand_urls (ykc, ykh, yubikey, nonce);
if (out != YKCLIENT_OK)
{
goto finish;
}
/* Send the request to the validation server */
out = ykclient_request_send (ykc, ykh, yubikey, nonce);
finish:
free (nonce);
return out;
}
/** Generates and send requests to one or more validation servers
*
* Constructs a throwaway Curl handle, and sends a request to each of the
* servers specified by set_url_templates.
*
* @note ykclient_request_process should be used for repeat requests as it
* @note supports connection caching.
*
* @param ykc Yubikey client configuration.
* @param yubikey OTP string passed to the client.
* @return one of the YKCLIENT_* values or YKCLIENT_OK on success.
*/
ykclient_rc
ykclient_request (ykclient_t * ykc, const char *yubikey)
{
ykclient_rc out;
ykclient_handle_t *ykh;
/* Initialise a throw away handle */
out = ykclient_handle_init (ykc, &ykh);
if (out != YKCLIENT_OK)
{
return out;
}
out = ykclient_request_process (ykc, ykh, yubikey);
ykclient_handle_done (&ykh);
return out;
}
/** Extended API to validate an OTP (hexkey)
*
* Will default to YubiCloud validation service, but may be used
* with any service, if non-NULL ykc_in pointer is passed, and
* ykclient_set_url_templates is used to configure template URLs.
*
*/
ykclient_rc
ykclient_verify_otp_v2 (ykclient_t * ykc_in,
const char *yubikey_otp,
unsigned int client_id,
const char *hexkey,
size_t urlcount,
const char **urls, const char *api_key)
{
ykclient_rc out;
ykclient_t *ykc;
if (ykc_in == NULL)
{
out = ykclient_init (&ykc);
if (out != YKCLIENT_OK)
{
return out;
}
}
else
{
ykc = ykc_in;
}
ykclient_set_client_hex (ykc, client_id, hexkey);
if (urlcount != 0 && *urls != 0)
{
if (strstr (urls[0], ADD_OTP "%s"))
{
ykclient_set_url_templates (ykc, urlcount, urls);
}
else
{
ykclient_set_url_bases (ykc, urlcount, urls);
}
}
if (api_key)
{
ykclient_set_verify_signature (ykc, 1);
ykclient_set_client_b64 (ykc, client_id, api_key);
}
out = ykclient_request (ykc, yubikey_otp);
if (ykc_in == NULL)
{
ykclient_done (&ykc);
}
return out;
}
/** Simple API to validate an OTP (hexkey) using YubiCloud
*/
ykclient_rc
ykclient_verify_otp (const char *yubikey_otp,
unsigned int client_id, const char *hexkey)
{
return ykclient_verify_otp_v2 (NULL,
yubikey_otp,
client_id, hexkey, 0, NULL, NULL);
}
/**
* Fetch out server response of last query
*/
const ykclient_server_response_t *
ykclient_get_server_response(ykclient_t *ykc) {
return ykc->srv_response;
}