diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:26:00 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:26:00 +0000 |
commit | 830407e88f9d40d954356c3754f2647f91d5c06a (patch) | |
tree | d6a0ece6feea91f3c656166dbaa884ef8a29740e /daemon/tls_session_ticket-srv.c | |
parent | Initial commit. (diff) | |
download | knot-resolver-upstream/5.6.0.tar.xz knot-resolver-upstream/5.6.0.zip |
Adding upstream version 5.6.0.upstream/5.6.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | daemon/tls_session_ticket-srv.c | 245 |
1 files changed, 245 insertions, 0 deletions
diff --git a/daemon/tls_session_ticket-srv.c b/daemon/tls_session_ticket-srv.c new file mode 100644 index 0000000..b198903 --- /dev/null +++ b/daemon/tls_session_ticket-srv.c @@ -0,0 +1,245 @@ +/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <inttypes.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <sys/time.h> + +#include <gnutls/gnutls.h> +#include <gnutls/crypto.h> +#include <uv.h> + +#include "lib/utils.h" + +/* Style: "local/static" identifiers are usually named tst_* */ + +/** The number of seconds between synchronized rotation of TLS session ticket key. */ +#define TST_KEY_LIFETIME 4096 + +/** Value from gnutls:lib/ext/session_ticket.c + * Beware: changing this needs to change the hashing implementation. */ +#define SESSION_KEY_SIZE 64 + +/** Compile-time support for setting the secret. */ +/* This is not secure with TLS <= 1.2 but TLS 1.3 and secure configuration + * is not available in GnuTLS yet. See https://gitlab.com/gnutls/gnutls/issues/477 */ +#define TLS_SESSION_RESUMPTION_SYNC (GNUTLS_VERSION_NUMBER >= 0x030603) +#if TLS_SESSION_RESUMPTION_SYNC + #define TST_HASH GNUTLS_DIG_SHA3_512 +#else + #define TST_HASH abort() +#endif + +#if GNUTLS_VERSION_NUMBER < 0x030400 + /* It's of little use anyway. We may get the secret through lua, + * which creates a copy outside of our control. */ + #define gnutls_memset memset +#endif + +/** Fields are internal to tst_key_* functions. */ +typedef struct tls_session_ticket_ctx { + uv_timer_t timer; /**< timer for rotation of the key */ + unsigned char key[SESSION_KEY_SIZE]; /**< the key itself */ + bool has_secret; /**< false -> key is random for each epoch */ + uint16_t hash_len; /**< length of `hash_data` */ + char hash_data[]; /**< data to hash to obtain `key`; + * it's `time_t epoch` and then the secret string */ +} tst_ctx_t; + +/** Check invariants, based on gnutls version. */ +static bool tst_key_invariants(void) +{ + static int result = 0; /*< cache for multiple invocations */ + if (result) return result > 0; + bool ok = true; + #if TLS_SESSION_RESUMPTION_SYNC + /* SHA3-512 output size may never change, but let's check it anyway :-) */ + ok = ok && gnutls_hash_get_len(TST_HASH) == SESSION_KEY_SIZE; + #endif + /* The ticket key size might change in a different gnutls version. */ + gnutls_datum_t key = { 0, 0 }; + ok = ok && gnutls_session_ticket_key_generate(&key) == 0 + && key.size == SESSION_KEY_SIZE; + free(key.data); + result = ok ? 1 : -1; + return ok; +} + +/** Create the internal structures and copy the secret. Beware: secret must be kept secure. */ +static tst_ctx_t * tst_key_create(const char *secret, size_t secret_len, uv_loop_t *loop) +{ + const size_t hash_len = sizeof(time_t) + secret_len; + if (kr_fails_assert(!secret_len || (secret && hash_len >= secret_len && hash_len <= UINT16_MAX))) { + return NULL; + /* reasonable secret_len is best enforced in config API */ + } + if (kr_fails_assert(tst_key_invariants())) + return NULL; + #if !TLS_SESSION_RESUMPTION_SYNC + if (secret_len) { + kr_log_error(TLS, "session ticket: secrets were not enabled at compile-time (your GnuTLS version is not supported)\n"); + return NULL; /* ENOTSUP */ + } + #endif + + tst_ctx_t *ctx = malloc(sizeof(*ctx) + hash_len); /* can be slightly longer */ + if (!ctx) return NULL; + ctx->has_secret = secret_len > 0; + ctx->hash_len = hash_len; + if (secret_len) { + memcpy(ctx->hash_data + sizeof(time_t), secret, secret_len); + } + + if (uv_timer_init(loop, &ctx->timer) != 0) { + free(ctx); + return NULL; + } + ctx->timer.data = ctx; + return ctx; +} + +/** Random variant of secret rotation: generate into key_tmp and copy. */ +static int tst_key_get_random(tst_ctx_t *ctx) +{ + gnutls_datum_t key_tmp = { NULL, 0 }; + int err = gnutls_session_ticket_key_generate(&key_tmp); + if (err) return kr_error(err); + if (kr_fails_assert(key_tmp.size == SESSION_KEY_SIZE)) + return kr_error(EFAULT); + memcpy(ctx->key, key_tmp.data, SESSION_KEY_SIZE); + gnutls_memset(key_tmp.data, 0, SESSION_KEY_SIZE); + free(key_tmp.data); + return kr_ok(); +} + +/** Recompute the session ticket key, if epoch has changed or forced. */ +static int tst_key_update(tst_ctx_t *ctx, time_t epoch, bool force_update) +{ + if (kr_fails_assert(ctx && ctx->hash_len >= sizeof(epoch))) + return kr_error(EINVAL); + /* documented limitation: time_t and endianness must match + * on instances sharing a secret */ + if (!force_update && memcmp(ctx->hash_data, &epoch, sizeof(epoch)) == 0) { + return kr_ok(); /* we are up to date */ + } + memcpy(ctx->hash_data, &epoch, sizeof(epoch)); + + if (!ctx->has_secret) { + return tst_key_get_random(ctx); + } + /* Otherwise, deterministic variant of secret rotation, if supported. */ + #if !TLS_SESSION_RESUMPTION_SYNC + kr_assert(!ENOTSUP); + return kr_error(ENOTSUP); + #else + int err = gnutls_hash_fast(TST_HASH, ctx->hash_data, + ctx->hash_len, ctx->key); + return err == 0 ? kr_ok() : kr_error(err); + #endif +} + +/** Free all resources of the key (securely). */ +static void tst_key_destroy(uv_handle_t *timer) +{ + if (kr_fails_assert(timer)) + return; + tst_ctx_t *ctx = timer->data; + if (kr_fails_assert(ctx)) + return; + gnutls_memset(ctx, 0, offsetof(tst_ctx_t, hash_data) + ctx->hash_len); + free(ctx); +} + +static void tst_key_check(uv_timer_t *timer, bool force_update); +static void tst_timer_callback(uv_timer_t *timer) +{ + tst_key_check(timer, false); +} + +/** Update the ST key if needed and reschedule itself via the timer. */ +static void tst_key_check(uv_timer_t *timer, bool force_update) +{ + tst_ctx_t *stst = (tst_ctx_t *)timer->data; + /* Compute the current epoch. */ + struct timeval now; + if (gettimeofday(&now, NULL)) { + kr_log_error(TLS, "session ticket: gettimeofday failed, %s\n", + strerror(errno)); + return; + } + uv_update_time(timer->loop); /* to have sync. between real and mono time */ + const time_t epoch = now.tv_sec / TST_KEY_LIFETIME; + /* Update the key; new sessions will fetch it from the location. + * Old ones hopefully can't get broken by that; documentation + * for gnutls_session_ticket_enable_server() doesn't say. */ + int err = tst_key_update(stst, epoch, force_update); + if (err) { + kr_log_error(TLS, "session ticket: failed rotation, %s\n", + kr_strerror(err)); + if (kr_fails_assert(err != kr_error(EINVAL))) + return; + } + /* Reschedule. */ + const time_t tv_sec_next = (epoch + 1) * TST_KEY_LIFETIME; + const uint64_t ms_until_second = 1000 - (now.tv_usec + 501) / 1000; + const uint64_t remain_ms = (tv_sec_next - now.tv_sec - 1) * (uint64_t)1000 + + ms_until_second + 1; + /* ^ +1 because we don't want to wake up half a millisecond before the epoch! */ + if (kr_fails_assert(remain_ms < (TST_KEY_LIFETIME + 1 /*rounding tolerance*/) * 1000)) + return; + kr_log_debug(TLS, "session ticket: epoch %"PRIu64 + ", scheduling rotation check in %"PRIu64" ms\n", + (uint64_t)epoch, remain_ms); + err = uv_timer_start(timer, &tst_timer_callback, remain_ms, 0); + if (kr_fails_assert(err == 0)) { + kr_log_error(TLS, "session ticket: failed to schedule, %s\n", + uv_strerror(err)); + return; + } +} + +/* Implementation for prototypes from ./tls.h */ + +void tls_session_ticket_enable(struct tls_session_ticket_ctx *ctx, gnutls_session_t session) +{ + if (kr_fails_assert(ctx && session)) + return; + const gnutls_datum_t gd = { + .size = SESSION_KEY_SIZE, + .data = ctx->key, + }; + int err = gnutls_session_ticket_enable_server(session, &gd); + if (err) { + kr_log_error(TLS, "failed to enable session tickets: %s (%d)\n", + gnutls_strerror_name(err), err); + /* but continue without tickets */ + } +} + +tst_ctx_t * tls_session_ticket_ctx_create(uv_loop_t *loop, const char *secret, + size_t secret_len) +{ + if (kr_fails_assert(loop && (!secret_len || secret))) + return NULL; + #if GNUTLS_VERSION_NUMBER < 0x030500 + /* We would need different SESSION_KEY_SIZE; avoid an error. */ + return NULL; + #endif + tst_ctx_t *ctx = tst_key_create(secret, secret_len, loop); + if (ctx) { + tst_key_check(&ctx->timer, true); + } + return ctx; +} + +void tls_session_ticket_ctx_destroy(tst_ctx_t *ctx) +{ + if (ctx == NULL) { + return; + } + uv_close((uv_handle_t *)&ctx->timer, &tst_key_destroy); +} + |