689 lines
16 KiB
C
689 lines
16 KiB
C
/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
#include <ccan/json/json.h>
|
|
#include <ctype.h>
|
|
#include <libknot/rrtype/opt-cookie.h>
|
|
#include <libknot/db/db_lmdb.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "lib/cookies/alg_containers.h"
|
|
#include "modules/cookies/cookiectl.h"
|
|
|
|
#define NAME_CLIENT_ENABLED "client_enabled"
|
|
#define NAME_CLIENT_SECRET "client_secret"
|
|
#define NAME_CLIENT_COOKIE_ALG "client_cookie_alg"
|
|
#define NAME_AVAILABLE_CLIENT_COOKIE_ALGS "available_client_cookie_algs"
|
|
|
|
#define NAME_SERVER_ENABLED "server_enabled"
|
|
#define NAME_SERVER_SECRET "server_secret"
|
|
#define NAME_SERVER_COOKIE_ALG "server_cookie_alg"
|
|
#define NAME_AVAILABLE_SERVER_COOKIE_ALGS "available_server_cookie_algs"
|
|
|
|
/**
|
|
* @brief Initialises cookie control context.
|
|
* @param ctx cookie control context
|
|
*/
|
|
static void kr_cookie_ctx_init(struct kr_cookie_ctx *ctx)
|
|
{
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
memset(ctx, 0, sizeof(*ctx));
|
|
|
|
ctx->clnt.current.alg_id = ctx->clnt.recent.alg_id = -1;
|
|
ctx->srvr.current.alg_id = ctx->srvr.recent.alg_id = -1;
|
|
}
|
|
|
|
/**
|
|
* @brief Check whether node holds proper 'enabled' value.
|
|
* @param node JSON node holding the value
|
|
* @return true if value OK
|
|
*/
|
|
static bool enabled_ok(const JsonNode *node)
|
|
{
|
|
if (kr_fails_assert(node))
|
|
return false;
|
|
|
|
return node->tag == JSON_BOOL;
|
|
}
|
|
|
|
/**
|
|
* @brief Check whether node holds proper 'secret' value.
|
|
* @param node JSON node holding the value
|
|
* @return true if value OK
|
|
*/
|
|
static bool secret_ok(const JsonNode *node)
|
|
{
|
|
if (kr_fails_assert(node))
|
|
return false;
|
|
|
|
if (node->tag != JSON_STRING) {
|
|
return false;
|
|
}
|
|
|
|
const char *hexstr = node->string_;
|
|
|
|
size_t len = strlen(hexstr);
|
|
if ((len % 2) != 0) {
|
|
return false;
|
|
}
|
|
/* A check for minimal required length could also be performed. */
|
|
|
|
for (size_t i = 0; i < len; ++i) {
|
|
if (!isxdigit(tolower(hexstr[i]))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Find hash function with given name.
|
|
* @param node JSON node holding the value
|
|
* @param table lookup table with algorithm names
|
|
* @return pointer to table entry or NULL on error if does not exist
|
|
*/
|
|
static const knot_lookup_t *hash_func_lookup(const JsonNode *node,
|
|
const knot_lookup_t table[])
|
|
{
|
|
if (!node || node->tag != JSON_STRING) {
|
|
return NULL;
|
|
}
|
|
|
|
return knot_lookup_by_name(table, node->string_);
|
|
}
|
|
|
|
/**
|
|
* @brief Creates a cookie secret structure.
|
|
* @param size size of the actual secret
|
|
* @param zero set to true if value should be cleared
|
|
* @return pointer to new structure, NULL on failure or if @size is zero
|
|
*/
|
|
static struct kr_cookie_secret *new_cookie_secret(size_t size, bool zero)
|
|
{
|
|
if (size == 0) {
|
|
return NULL;
|
|
}
|
|
|
|
struct kr_cookie_secret *sq = malloc(sizeof(*sq) + size);
|
|
if (!sq) {
|
|
return NULL;
|
|
}
|
|
|
|
sq->size = size;
|
|
if (zero) {
|
|
memset(sq->data, 0, size);
|
|
}
|
|
return sq;
|
|
}
|
|
|
|
/**
|
|
* @brief Clone a cookie secret.
|
|
* @param sec secret to be cloned
|
|
* @return pointer to new structure, NULL on failure or if @size is zero
|
|
*/
|
|
static struct kr_cookie_secret *clone_cookie_secret(const struct kr_cookie_secret *sec)
|
|
{
|
|
if (!sec || sec->size == 0) {
|
|
return NULL;
|
|
}
|
|
|
|
struct kr_cookie_secret *sq = malloc(sizeof(*sq) + sec->size);
|
|
if (!sq) {
|
|
return NULL;
|
|
}
|
|
|
|
sq->size = sec->size;
|
|
memcpy(sq->data, sec->data, sq->size);
|
|
return sq;
|
|
}
|
|
|
|
static int hexchar2val(int d)
|
|
{
|
|
if (('0' <= d) && (d <= '9')) {
|
|
return d - '0';
|
|
} else if (('a' <= d) && (d <= 'f')) {
|
|
return d - 'a' + 0x0a;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
static int hexval2char(int i)
|
|
{
|
|
if ((0 <= i) && (i <= 9)) {
|
|
return i + '0';
|
|
} else if ((0x0a <= i) && (i <= 0x0f)) {
|
|
return i - 0x0a + 'A';
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Converts string containing two-digit hexadecimal number into int.
|
|
* @param hexstr hexadecimal string
|
|
* @return -1 on error, value from 0 to 255 else.
|
|
*/
|
|
static int hexbyte2int(const char *hexstr)
|
|
{
|
|
if (!hexstr) {
|
|
return -1;
|
|
}
|
|
|
|
int dhi = tolower(hexstr[0]);
|
|
if (!isxdigit(dhi)) {
|
|
/* Exit also on empty string. */
|
|
return -1;
|
|
}
|
|
int dlo = tolower(hexstr[1]);
|
|
if (!isxdigit(dlo)) {
|
|
return -1;
|
|
}
|
|
|
|
dhi = hexchar2val(dhi);
|
|
if (kr_fails_assert(dhi != -1))
|
|
return -1;
|
|
dlo = hexchar2val(dlo);
|
|
if (kr_fails_assert(dlo != -1))
|
|
return -1;
|
|
|
|
return (dhi << 4) | dlo;
|
|
}
|
|
|
|
/**
|
|
* @brief Writes two hexadecimal digits (two byes) into given memory location.
|
|
* @param tgt target location
|
|
* @param i number from 0 to 255
|
|
* @return 0 on success, -1 on failure
|
|
*/
|
|
static int int2hexbyte(char *tgt, int i)
|
|
{
|
|
if (!tgt || i < 0x00 || i > 0xff) {
|
|
return -1;
|
|
}
|
|
|
|
int ilo = hexval2char(i & 0x0f);
|
|
if (kr_fails_assert(ilo != -1))
|
|
return -1;
|
|
int ihi = hexval2char((i >> 4) & 0x0f);
|
|
if (kr_fails_assert(ihi != -1))
|
|
return -1;
|
|
|
|
tgt[0] = ihi;
|
|
tgt[1] = ilo;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Reads a string containing hexadecimal values.
|
|
* @note String must consist of hexadecimal digits only and must have even
|
|
* non-zero length.
|
|
*/
|
|
static struct kr_cookie_secret *new_sq_from_hexstr(const char *hexstr)
|
|
{
|
|
if (!hexstr) {
|
|
return NULL;
|
|
}
|
|
|
|
size_t len = strlen(hexstr);
|
|
if ((len % 2) != 0) {
|
|
return NULL;
|
|
}
|
|
|
|
struct kr_cookie_secret *sq = new_cookie_secret(len / 2, false);
|
|
if (!sq) {
|
|
return NULL;
|
|
}
|
|
|
|
uint8_t *data = sq->data;
|
|
for (size_t i = 0; i < len; i += 2) {
|
|
int num = hexbyte2int(hexstr + i);
|
|
if (num == -1) {
|
|
free(sq);
|
|
return NULL;
|
|
}
|
|
if (kr_fails_assert(0x00 <= num && num <= 0xff)) {
|
|
free(sq);
|
|
return NULL;
|
|
}
|
|
*data = num;
|
|
++data;
|
|
}
|
|
|
|
return sq;
|
|
}
|
|
|
|
/**
|
|
* @brief Creates new secret.
|
|
* @param node JSON node holding the secret value
|
|
* @return pointer to newly allocated secret, NULL on error
|
|
*/
|
|
static struct kr_cookie_secret *create_secret(const JsonNode *node)
|
|
{
|
|
if (!node) {
|
|
return NULL;
|
|
}
|
|
|
|
if (node->tag != JSON_STRING) {
|
|
return NULL;
|
|
}
|
|
|
|
return new_sq_from_hexstr(node->string_);
|
|
}
|
|
|
|
/**
|
|
* @brief Check whether configuration node contains valid values.
|
|
*/
|
|
static bool configuration_node_ok(const JsonNode *node)
|
|
{
|
|
if (kr_fails_assert(node))
|
|
return false;
|
|
|
|
if (!node->key) {
|
|
/* All top most nodes must have names. */
|
|
return false;
|
|
}
|
|
|
|
if (strcmp(node->key, NAME_CLIENT_ENABLED) == 0) {
|
|
return enabled_ok(node);
|
|
} else if (strcmp(node->key, NAME_CLIENT_SECRET) == 0) {
|
|
return secret_ok(node);
|
|
} else if (strcmp(node->key, NAME_CLIENT_COOKIE_ALG) == 0) {
|
|
return hash_func_lookup(node, kr_cc_alg_names) != NULL;
|
|
} else if (strcmp(node->key, NAME_SERVER_ENABLED) == 0) {
|
|
return enabled_ok(node);
|
|
} else if (strcmp(node->key, NAME_SERVER_SECRET) == 0) {
|
|
return secret_ok(node);
|
|
} else if (strcmp(node->key, NAME_SERVER_COOKIE_ALG) == 0) {
|
|
return hash_func_lookup(node, kr_sc_alg_names) != NULL;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @brief Creates a new string from secret quantity.
|
|
* @param sq secret quantity
|
|
* @return newly allocated string or NULL on error
|
|
*/
|
|
static char *new_hexstr_from_sq(const struct kr_cookie_secret *sq)
|
|
{
|
|
if (!sq) {
|
|
return NULL;
|
|
}
|
|
|
|
char *new_str = malloc((sq->size * 2) + 1);
|
|
if (!new_str) {
|
|
return NULL;
|
|
}
|
|
|
|
char *tgt = new_str;
|
|
for (size_t i = 0; i < sq->size; ++i) {
|
|
if (0 != int2hexbyte(tgt, sq->data[i])) {
|
|
free(new_str);
|
|
return NULL;
|
|
}
|
|
tgt += 2;
|
|
}
|
|
|
|
*tgt = '\0';
|
|
return new_str;
|
|
}
|
|
|
|
static bool read_secret(JsonNode *root, const char *node_name,
|
|
const struct kr_cookie_secret *secret)
|
|
{
|
|
if (kr_fails_assert(root && node_name && secret))
|
|
return false;
|
|
|
|
char *secret_str = new_hexstr_from_sq(secret);
|
|
if (!secret_str) {
|
|
return false;
|
|
}
|
|
|
|
JsonNode *str_node = json_mkstring(secret_str);
|
|
if (!str_node) {
|
|
free(secret_str);
|
|
return false;
|
|
}
|
|
|
|
json_append_member(root, node_name, str_node);
|
|
|
|
free(secret_str);
|
|
return true;
|
|
}
|
|
|
|
static bool read_available_hashes(JsonNode *root, const char *root_name,
|
|
const knot_lookup_t table[])
|
|
{
|
|
if (kr_fails_assert(root && root_name && table))
|
|
return false;
|
|
|
|
JsonNode *array = json_mkarray();
|
|
if (!array) {
|
|
return false;
|
|
}
|
|
|
|
const knot_lookup_t *aux_ptr = table;
|
|
while (aux_ptr && (aux_ptr->id >= 0) && aux_ptr->name) {
|
|
JsonNode *element = json_mkstring(aux_ptr->name);
|
|
if (!element) {
|
|
goto fail;
|
|
}
|
|
json_append_element(array, element);
|
|
++aux_ptr;
|
|
}
|
|
|
|
json_append_member(root, root_name, array);
|
|
|
|
return true;
|
|
|
|
fail:
|
|
if (array) {
|
|
json_delete(array);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @brief Check whether new settings are different from the old ones.
|
|
*/
|
|
static bool is_modified(const struct kr_cookie_comp *running,
|
|
struct kr_cookie_secret *secr,
|
|
const knot_lookup_t *alg_lookup)
|
|
{
|
|
if (kr_fails_assert(running))
|
|
return false;
|
|
|
|
if (alg_lookup && alg_lookup->id >= 0) {
|
|
if (running->alg_id != alg_lookup->id) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (secr) {
|
|
if (kr_fails_assert(secr->size > 0))
|
|
return false;
|
|
if (running->secr->size != secr->size ||
|
|
0 != memcmp(running->secr->data, secr->data,
|
|
running->secr->size)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @brief Returns newly allocated secret via pointer argument.
|
|
*/
|
|
static bool obtain_secret(JsonNode *root_node, struct kr_cookie_secret **secret,
|
|
const char *name)
|
|
{
|
|
if (kr_fails_assert(secret && name))
|
|
return false;
|
|
|
|
const JsonNode *node;
|
|
if ((node = json_find_member(root_node, name)) != NULL) {
|
|
*secret = create_secret(node);
|
|
if (!*secret) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Updates the current configuration and moves current to recent.
|
|
*/
|
|
static void update_running(struct kr_cookie_settings *running,
|
|
struct kr_cookie_secret **secret,
|
|
const knot_lookup_t *alg_lookup)
|
|
{
|
|
if (kr_fails_assert(running && secret) || kr_fails_assert(*secret || alg_lookup))
|
|
return;
|
|
|
|
running->recent.alg_id = -1;
|
|
free(running->recent.secr);
|
|
running->recent.secr = NULL;
|
|
|
|
running->recent.alg_id = running->current.alg_id;
|
|
if (alg_lookup) {
|
|
if (kr_fails_assert(alg_lookup->id >= 0))
|
|
return;
|
|
running->current.alg_id = alg_lookup->id;
|
|
}
|
|
|
|
if (*secret) {
|
|
running->recent.secr = running->current.secr;
|
|
running->current.secr = *secret;
|
|
*secret = NULL;
|
|
} else {
|
|
running->recent.secr = clone_cookie_secret(running->current.secr);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Applies modification onto client/server running configuration.
|
|
* @note The @a secret is going to be consumed.
|
|
* @param secret pointer to new secret
|
|
* @param alg_lookup new algorithm
|
|
* @param enabled JSON node holding boolean value
|
|
*/
|
|
static void apply_changes(struct kr_cookie_settings *running,
|
|
struct kr_cookie_secret **secret,
|
|
const knot_lookup_t *alg_lookup,
|
|
const JsonNode *enabled)
|
|
{
|
|
if (kr_fails_assert(running && secret))
|
|
return;
|
|
|
|
if (is_modified(&running->current, *secret, alg_lookup)) {
|
|
update_running(running, secret, alg_lookup);
|
|
}
|
|
|
|
if (enabled) {
|
|
kr_assert(enabled->tag == JSON_BOOL);
|
|
running->enabled = enabled->bool_;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Applies configuration.
|
|
*
|
|
* @note The function must be called after the input values have been checked
|
|
* for validity. Only first found values are applied.
|
|
*
|
|
* @param ctx cookie configuration context
|
|
* @param root_node JSON root node
|
|
* @return true if changes were applied
|
|
*/
|
|
static bool config_apply_json(struct kr_cookie_ctx *ctx, JsonNode *root_node)
|
|
{
|
|
if (kr_fails_assert(ctx && root_node))
|
|
return;
|
|
|
|
/*
|
|
* These must be allocated before actual change. Allocation failure
|
|
* should not leave configuration in inconsistent state.
|
|
*/
|
|
struct kr_cookie_secret *new_clnt_secret = NULL;
|
|
struct kr_cookie_secret *new_srvr_secret = NULL;
|
|
if (!obtain_secret(root_node, &new_clnt_secret, NAME_CLIENT_SECRET)) {
|
|
return false;
|
|
}
|
|
if (!obtain_secret(root_node, &new_srvr_secret, NAME_SERVER_SECRET)) {
|
|
free(new_clnt_secret);
|
|
return false;
|
|
}
|
|
|
|
/* Algorithm pointers. */
|
|
const knot_lookup_t *clnt_lookup = hash_func_lookup(json_find_member(root_node, NAME_CLIENT_COOKIE_ALG), kr_cc_alg_names);
|
|
const knot_lookup_t *srvr_lookup = hash_func_lookup(json_find_member(root_node, NAME_SERVER_COOKIE_ALG), kr_sc_alg_names);
|
|
|
|
const JsonNode *clnt_enabled_node = json_find_member(root_node, NAME_CLIENT_ENABLED);
|
|
const JsonNode *srvr_enabled_node = json_find_member(root_node, NAME_SERVER_ENABLED);
|
|
|
|
apply_changes(&ctx->clnt, &new_clnt_secret, clnt_lookup, clnt_enabled_node);
|
|
apply_changes(&ctx->srvr, &new_srvr_secret, srvr_lookup, srvr_enabled_node);
|
|
|
|
/*
|
|
* Allocated secrets should be already consumed. There is no need to
|
|
* free them.
|
|
*/
|
|
|
|
return true;
|
|
}
|
|
|
|
bool config_apply(struct kr_cookie_ctx *ctx, const char *args)
|
|
{
|
|
if (!ctx) {
|
|
return false;
|
|
}
|
|
|
|
if (!args || !strlen(args)) {
|
|
return true;
|
|
}
|
|
|
|
if (!args || !strlen(args)) {
|
|
return true;
|
|
}
|
|
|
|
bool success = false;
|
|
|
|
/* Check whether all supplied data are valid. */
|
|
JsonNode *root_node = json_decode(args);
|
|
if (!root_node) {
|
|
return false;
|
|
}
|
|
JsonNode *node;
|
|
json_foreach (node, root_node) {
|
|
success = configuration_node_ok(node);
|
|
if (!success) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Apply configuration if values seem to be OK. */
|
|
if (success) {
|
|
success = config_apply_json(ctx, root_node);
|
|
}
|
|
|
|
json_delete(root_node);
|
|
|
|
return success;
|
|
}
|
|
|
|
char *config_read(struct kr_cookie_ctx *ctx)
|
|
{
|
|
if (!ctx) {
|
|
return NULL;
|
|
}
|
|
|
|
const knot_lookup_t *lookup;
|
|
char *result;
|
|
JsonNode *root_node = json_mkobject();
|
|
if (!root_node) {
|
|
return NULL;
|
|
}
|
|
|
|
json_append_member(root_node, NAME_CLIENT_ENABLED,
|
|
json_mkbool(ctx->clnt.enabled));
|
|
|
|
read_secret(root_node, NAME_CLIENT_SECRET, ctx->clnt.current.secr);
|
|
|
|
lookup = knot_lookup_by_id(kr_cc_alg_names, ctx->clnt.current.alg_id);
|
|
if (lookup) {
|
|
json_append_member(root_node, NAME_CLIENT_COOKIE_ALG,
|
|
json_mkstring(lookup->name));
|
|
}
|
|
|
|
read_available_hashes(root_node, NAME_AVAILABLE_CLIENT_COOKIE_ALGS,
|
|
kr_cc_alg_names);
|
|
|
|
json_append_member(root_node, NAME_SERVER_ENABLED,
|
|
json_mkbool(ctx->srvr.enabled));
|
|
|
|
read_secret(root_node, NAME_SERVER_SECRET, ctx->srvr.current.secr);
|
|
|
|
lookup = knot_lookup_by_id(kr_sc_alg_names, ctx->srvr.current.alg_id);
|
|
if (lookup) {
|
|
json_append_member(root_node, NAME_SERVER_COOKIE_ALG,
|
|
json_mkstring(lookup->name));
|
|
}
|
|
|
|
read_available_hashes(root_node, NAME_AVAILABLE_SERVER_COOKIE_ALGS,
|
|
kr_sc_alg_names);
|
|
|
|
result = json_encode(root_node);
|
|
json_delete(root_node);
|
|
return result;
|
|
}
|
|
|
|
int config_init(struct kr_cookie_ctx *ctx)
|
|
{
|
|
if (!ctx) {
|
|
return kr_error(EINVAL);
|
|
}
|
|
|
|
kr_cookie_ctx_init(ctx);
|
|
|
|
struct kr_cookie_secret *cs = new_cookie_secret(KNOT_OPT_COOKIE_CLNT,
|
|
true);
|
|
struct kr_cookie_secret *ss = new_cookie_secret(KNOT_OPT_COOKIE_CLNT,
|
|
true);
|
|
if (!cs || !ss) {
|
|
free(cs);
|
|
free(ss);
|
|
return kr_error(ENOMEM);
|
|
}
|
|
|
|
const knot_lookup_t *clookup = knot_lookup_by_name(kr_cc_alg_names,
|
|
"FNV-64");
|
|
const knot_lookup_t *slookup = knot_lookup_by_name(kr_sc_alg_names,
|
|
"FNV-64");
|
|
if (!clookup || !slookup) {
|
|
free(cs);
|
|
free(ss);
|
|
return kr_error(ENOENT);
|
|
}
|
|
|
|
ctx->clnt.current.secr = cs;
|
|
ctx->clnt.current.alg_id = clookup->id;
|
|
|
|
ctx->srvr.current.secr = ss;
|
|
ctx->srvr.current.alg_id = slookup->id;
|
|
|
|
return kr_ok();
|
|
}
|
|
|
|
void config_deinit(struct kr_cookie_ctx *ctx)
|
|
{
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
ctx->clnt.enabled = false;
|
|
|
|
free(ctx->clnt.recent.secr);
|
|
ctx->clnt.recent.secr = NULL;
|
|
|
|
free(ctx->clnt.current.secr);
|
|
ctx->clnt.current.secr = NULL;
|
|
|
|
ctx->srvr.enabled = false;
|
|
|
|
free(ctx->srvr.recent.secr);
|
|
ctx->srvr.recent.secr = NULL;
|
|
|
|
free(ctx->srvr.current.secr);
|
|
ctx->srvr.current.secr = NULL;
|
|
}
|