From: Vladimír Čunát Date: Thu, 31 May 2018 11:51:03 +0000 (+0200) Subject: daemon/tls: work on server-side session tickets X-Git-Tag: v2.4.0~24^2~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c4dd4d359bcbec6557bb737f1b3b0f6cbfe3c9a3;p=thirdparty%2Fknot-resolver.git daemon/tls: work on server-side session tickets --- diff --git a/NEWS b/NEWS index 5f59c1735..cdbc4ab53 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ Security -------- - fix a rare case of zones incorrectly dowgraded to insecure status +New features +------------ +- TLS session resumption (RFC 5077), both server and client (!585, #105) + Bugfixes -------- - avoid turning off qname minimization in some cases, e.g. co.uk. (#339) diff --git a/daemon/README.rst b/daemon/README.rst index 5c790abf3..32289823d 100644 --- a/daemon/README.rst +++ b/daemon/README.rst @@ -459,8 +459,17 @@ For when listening on ``localhost`` just doesn't cut it. > net.tcp_pipeline(50) 50 +.. function:: net.outgoing_v4([string address]) + + Get/set the IPv4 address used to perform queries. There is also ``net.outgoing_v6`` for IPv6. + The default is ``nil``, which lets the OS choose any address. + + .. _tls-server-config: +TLS server configuration +^^^^^^^^^^^^^^^^^^^^^^^^ + .. function:: net.tls([cert_path], [key_path]) Get/set path to a server TLS certificate and private key for DNS/TLS. @@ -475,7 +484,7 @@ For when listening on ``localhost`` just doesn't cut it. > net.listen("::", 853) > net.listen("::", 443, {tls = true}) -.. function:: net.tls_padding([padding]) +.. function:: net.tls_padding([true | false]) Get/set EDNS(0) padding of answers to queries that arrive over TLS transport. If set to `true` (the default), it will use a sensible @@ -485,11 +494,30 @@ For when listening on ``localhost`` just doesn't cut it. answer will have size of a multiple of 64 (64, 128, 192, ...). If set to `false` (or a number < 2), it will disable padding entirely. -.. function:: net.outgoing_v4([string address]) +.. function:: net.tls_sticket_secret([string with pre-shared secret]) - Get/set the IPv4 address used to perform queries. There is also ``net.outgoing_v6`` for IPv6. - The default is ``nil``, which lets the OS choose any address. + Set secret for TLS session resumption via tickets, by :rfc:`5077`. + + The server-side key is rotated roughly once per hour. + By default or if called without secret, the key is random. + That is good for long-term forward secrecy, but multiple kresd instances + won't be able to resume each other's sessions. + + If you provide the same secret to multiple instances, they will be able to resume + each other's sessions *without* any further communication between them. + For good security, the secret must have enough entropy to be hard to guess, + and it should still be occasionally rotated manually (and securely forgotten), + to reduce the scope of privacy leak in case the + `secret leaks eventually `_. + + .. warning:: setting the secret is probably too risky with TLS <= 1.2. + At this moment no gnutls stable release even supports TLS 1.3. + Therefore setting the secrets should be considered experimental for now. + +.. function:: net.tls_sticket_secret_file([string with path to a file containing pre-shared secret]) + The same as :func:`net.tls_sticket_secret`, + except the secret is read from a (binary) file. .. _dnssec-config: diff --git a/daemon/bindings.c b/daemon/bindings.c index e1ca117e7..fe489331e 100644 --- a/daemon/bindings.c +++ b/daemon/bindings.c @@ -642,113 +642,96 @@ static int net_tls_padding(lua_State *L) return 1; } -/* Configure client-side TLS session ticket key generation. - * - * note Don't call from CLI when there are forked kresd instances as it - * will break synchronous ticket key regeneration. - * - * Expected parameters from lua - * salt salt string used for session ticket key generation. - * It's guaranteed that all forked kresd instances - * with same salt string will always use the same session ticket key - * without additional synchronization. - * If salt string is empty, kresd won't use session tickets at server side - * and therefore won't support session resumption. - */ +/** Shorter salt can't contain much entropy. */ +#define net_tls_sticket_MIN_SECRET_LEN 32 -static int net_tls_sticket_key_salt_string(lua_State *L) +static int net_tls_sticket_secret_string(lua_State *L) { + struct network *net = &engine_luaget(L)->net; - if (lua_gettop(L) != 1 || !lua_isstring(L, 1)) { - lua_pushstring(L, "net.tls_sticket_salt_string takes one parameter: (\"salt string\")"); - lua_error(L); - } + size_t secret_len; + const char *secret; - struct engine *engine = engine_luaget(L); - struct network *net = &engine->net; - const char *salt = lua_tostring(L, 1); - size_t salt_len = strlen(salt); - if (net->tls_session_ticket_ctx != NULL) { - tls_session_ticket_ctx_destroy(net->tls_session_ticket_ctx); - net->tls_session_ticket_ctx = NULL; - } - if (salt_len) { - net->tls_session_ticket_ctx = tls_session_ticket_ctx_create(net->loop, salt, salt_len); - if (net->tls_session_ticket_ctx == NULL) { - lua_pushstring(L, "net.tls_sticket_salt_string - can't create session ticket context"); + if (lua_gettop(L) == 0) { + /* Zero-length secret, implying random key. */ + secret_len = 0; + secret = NULL; + } else { + if (lua_gettop(L) != 1 || !lua_isstring(L, 1)) { + lua_pushstring(L, + "net.tls_sticket_secret takes one parameter: (\"secret string\")"); + lua_error(L); + } + secret = lua_tolstring(L, 1, &secret_len); + if (secret_len < net_tls_sticket_MIN_SECRET_LEN || !secret) { + lua_pushstring(L, "net.tls_sticket_secret - the secret is shorter than " + xstr(net_tls_sticket_MIN_SECRET_LEN) " bytes"); lua_error(L); } } + tls_session_ticket_ctx_destroy(net->tls_session_ticket_ctx); + net->tls_session_ticket_ctx = + tls_session_ticket_ctx_create(net->loop, secret, secret_len); + if (net->tls_session_ticket_ctx == NULL) { + lua_pushstring(L, + "net.tls_sticket_secret_string - can't create session ticket context"); + lua_error(L); + } + lua_pushboolean(L, true); return 1; } -/* Configure client-side TLS session ticket key generation. - * - * note Don't call from CLI when there are forked kresd instances as it - * will break synchronous ticket key regeneration. - * - * Expected parameters from lua - * file text file containing salt string. - * If file is empty, resolver won't use session tickets at server side - * and therefore won't support session resumption. - */ - -static int net_tls_sticket_key_salt_file(lua_State *L) +static int net_tls_sticket_secret_file(lua_State *L) { - if (lua_gettop(L) != 1 || !lua_isstring(L, 1)) { - lua_pushstring(L, "net.tls_sticket_salt_file takes one parameter: (\"file name\")"); + lua_pushstring(L, + "net.tls_sticket_secret_file takes one parameter: (\"file name\")"); lua_error(L); } - struct engine *engine = engine_luaget(L); - struct network *net = &engine->net; - const char *file_name = lua_tostring(L, 1); if (strlen(file_name) == 0) { - lua_pushstring(L, "net.tls_sticket_salt_file - empty file name"); + lua_pushstring(L, "net.tls_sticket_secret_file - empty file name"); lua_error(L); } FILE *fp = fopen(file_name, "r"); if (fp == NULL) { - lua_pushstring(L, "net.tls_sticket_salt_file - can't open file '"); - lua_pushstring(L, file_name); - lua_pushstring(L, "' ("); - lua_pushstring(L, strerror(errno)); - lua_pushstring(L, ")"); - lua_concat(L, 5); + lua_pushfstring(L, "net.tls_sticket_secret_file - can't open file '%s': %s", + file_name, strerror(errno)); lua_error(L); } - char *salt = NULL; - size_t salt_buf_len = 0; - /* getline() also reads newline characters, if any. */ - ssize_t salt_len = getline(&salt, &salt_buf_len, fp); - if (salt_len < 0) { - lua_pushstring(L, "net.tls_sticket_salt_file - error reading from '"); - lua_pushstring(L, file_name); - lua_pushstring(L, "' ("); - lua_pushstring(L, strerror(errno)); - lua_pushstring(L, ")"); - lua_concat(L, 5); + char secret_buf[TLS_SESSION_TICKET_SECRET_MAX_LEN]; + const size_t secret_len = fread(secret_buf, 1, sizeof(secret_buf), fp); + int err = ferror(fp); + if (err) { + lua_pushfstring(L, + "net.tls_sticket_secret_file - error reading from file '%s': %s", + file_name, strerror(err)); lua_error(L); } - if (net->tls_session_ticket_ctx != NULL) { - tls_session_ticket_ctx_destroy(net->tls_session_ticket_ctx); - net->tls_session_ticket_ctx = NULL; - } - if (salt_len > 0) { - net->tls_session_ticket_ctx = tls_session_ticket_ctx_create(net->loop, salt, salt_len); - if (net->tls_session_ticket_ctx == NULL) { - lua_pushstring(L, "net.tls_sticket_salt_file - can't create session ticket context"); - lua_error(L); - } + if (secret_len < net_tls_sticket_MIN_SECRET_LEN) { + lua_pushfstring(L, + "net.tls_sticket_secret_file - file '%s' is shorter than " + xstr(net_tls_sticket_MIN_SECRET_LEN) " bytes", + file_name); + lua_error(L); } - free(salt); fclose(fp); + + struct network *net = &engine_luaget(L)->net; + + tls_session_ticket_ctx_destroy(net->tls_session_ticket_ctx); + net->tls_session_ticket_ctx = + tls_session_ticket_ctx_create(net->loop, secret_buf, secret_len); + if (net->tls_session_ticket_ctx == NULL) { + lua_pushstring(L, + "net.tls_sticket_secret_file - can't create session ticket context"); + lua_error(L); + } lua_pushboolean(L, true); return 1; } @@ -823,8 +806,8 @@ int lib_net(lua_State *L) { "tls_server", net_tls }, { "tls_client", net_tls_client }, { "tls_padding", net_tls_padding }, - { "tls_sticket_salt_string", net_tls_sticket_key_salt_string }, - { "tls_sticket_salt_file", net_tls_sticket_key_salt_file }, + { "tls_sticket_secret", net_tls_sticket_secret_string }, + { "tls_sticket_secret_file", net_tls_sticket_secret_file }, { "outgoing_v4", net_outgoing_v4 }, { "outgoing_v6", net_outgoing_v6 }, { NULL, NULL } diff --git a/daemon/daemon.mk b/daemon/daemon.mk index 39421af58..c3c15075e 100644 --- a/daemon/daemon.mk +++ b/daemon/daemon.mk @@ -7,6 +7,7 @@ kresd_SOURCES := \ daemon/ffimodule.c \ daemon/tls.c \ daemon/tls_ephemeral_credentials.c \ + daemon/tls_session_ticket-srv.c \ daemon/zimport.c \ daemon/main.c diff --git a/daemon/network.c b/daemon/network.c index 70c478122..77186d64c 100644 --- a/daemon/network.c +++ b/daemon/network.c @@ -52,7 +52,8 @@ void network_init(struct network *net, uv_loop_t *loop) net->loop = loop; net->endpoints = map_make(NULL); net->tls_client_params = map_make(NULL); - net->tls_session_ticket_ctx = NULL; + net->tls_session_ticket_ctx = /* unsync. random, by default */ + tls_session_ticket_ctx_create(loop, NULL, 0); } } @@ -110,9 +111,7 @@ void network_deinit(struct network *net) tls_credentials_free(net->tls_credentials); tls_client_params_free(&net->tls_client_params); net->tls_credentials = NULL; - if (net->tls_session_ticket_ctx != NULL) { - tls_session_ticket_ctx_destroy(net->tls_session_ticket_ctx); - } + tls_session_ticket_ctx_destroy(net->tls_session_ticket_ctx); } } diff --git a/daemon/network.h b/daemon/network.h index ca50c204f..241e595d7 100644 --- a/daemon/network.h +++ b/daemon/network.h @@ -42,6 +42,7 @@ struct endpoint { typedef array_t(struct endpoint*) endpoint_array_t; /* @endcond */ +struct tls_session_ticket_ctx; struct network { uv_loop_t *loop; map_t endpoints; diff --git a/daemon/tls.c b/daemon/tls.c index debd0aa6b..3036a7f94 100644 --- a/daemon/tls.c +++ b/daemon/tls.c @@ -19,21 +19,21 @@ * this program. If not, see . */ -#include -#include #include #include -#include -#include -#include #include +#include #include -#include +#include +#include +#include + +#include "contrib/ucw/lib.h" #include "contrib/base64.h" -#include "daemon/worker.h" -#include "daemon/tls.h" #include "daemon/io.h" +#include "daemon/tls.h" +#include "daemon/worker.h" #define EPHEMERAL_CERT_EXPIRATION_SECONDS_RENEW_BEFORE 60*60*24*7 #define GNUTLS_PIN_MIN_VERSION 0x030400 @@ -45,115 +45,11 @@ #define DEBUG_MSG(fmt...) #endif -#if GNUTLS_VERSION_NUMBER >= 0x030400 -#define tls_memset gnutls_memset -#else -#define tls_memset memset -#endif - -#if GNUTLS_VERSION_NUMBER >= 0x030407 -#define SESSION_TICKET_KEYGEN_HASH GNUTLS_DIG_SHA3_512 -#else -#define SESSION_TICKET_KEYGEN_HASH GNUTLS_DIG_SHA512 -#endif - -#define TLS_SESSION_TICKET_KEY_REGENERATION_INTERVAL 3600000 - static char const server_logstring[] = "tls"; static char const client_logstring[] = "tls_client"; static int client_verify_certificate(gnutls_session_t tls_session); -/* FIXME: review session_ticket_key* again before merge! */ -/** Value from gnutls:lib/ext/session_ticket.c - * Beware: changing this needs to change the hashing implementation. */ -#define SESSION_KEY_SIZE 64 - -/** Fields are internal to session_ticket_key_* functions. */ -struct session_ticket_key { - char key[SESSION_KEY_SIZE]; - uint16_t hash_len; - char hash_data[]; -}; - -struct tls_session_ticket_ctx { - size_t epoch; - uv_timer_t key_timer; - struct session_ticket_key *key; -}; - -/** Check invariants, based on gnutls version. */ -static bool session_ticket_key_invariants(void) -{ - static int result = 0; - if (result) return result > 0; - bool ok = true; - /* SHA3-512 output size may never change, but let's check it anyway :-) */ - ok = ok && gnutls_hash_get_len(SESSION_TICKET_KEYGEN_HASH) == SESSION_KEY_SIZE; - /* 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 salt. Beware: salt must be kept secure. */ -static struct session_ticket_key * session_ticket_key_create(const char *salt, size_t salt_len) -{ - const size_t hash_len = sizeof(size_t) + salt_len; - if (!salt || !salt_len || hash_len > UINT16_MAX || hash_len < salt_len) { - assert(!EINVAL); - return NULL; - /* reasonable salt_len is best enforced in config API */ - } - if (!session_ticket_key_invariants()) { - assert(!EFAULT); - return NULL; - } - struct session_ticket_key *key = - malloc(offsetof(struct session_ticket_key, hash_data) + hash_len); - if (!key) return NULL; - key->hash_len = hash_len; - memcpy(key->hash_data + sizeof(size_t), salt, salt_len); - return key; -} - -/** Recompute the session ticket key, deterministically from epoch and salt. */ -static int session_ticket_key_recompute(struct session_ticket_key *key, size_t epoch) -{ - if (!key || key->hash_len <= sizeof(size_t)) { - assert(!EINVAL); - return kr_error(EINVAL); - } - memcpy(key->hash_data, &epoch, sizeof(size_t)); - /* TODO: ^^ support mixing endians? */ - int ret = gnutls_hash_fast(SESSION_TICKET_KEYGEN_HASH, key->hash_data, - key->hash_len, key->key); - return ret == 0 ? kr_ok() : kr_error(ret); -} - -/** Return reference to a key in the format suitable for gnutls. */ -static inline gnutls_datum_t session_ticket_key_get(struct session_ticket_key *key) -{ - assert(key); - return (gnutls_datum_t){ - .size = SESSION_KEY_SIZE, - .data = (unsigned char *)key, - }; -} - -/** Free all resources of the key (securely). */ -static void session_ticket_key_destroy(struct session_ticket_key *key) -{ - assert(key); - tls_memset(key, 0, offsetof(struct session_ticket_key, hash_data) - + key->hash_len); - free(key); -} - - /** * Set mandatory security settings from * https://tools.ietf.org/html/draft-ietf-dprive-dtls-and-tls-profiles-11#section-9 @@ -265,10 +161,9 @@ struct tls_ctx_t *tls_new(struct worker_ctx *worker) gnutls_transport_set_push_function(tls->c.tls_session, worker_gnutls_push); gnutls_transport_set_ptr(tls->c.tls_session, tls); - if (net->tls_session_ticket_ctx != NULL) { - assert(net->tls_session_ticket_ctx->key); - gnutls_datum_t session_ticket_key = session_ticket_key_get(net->tls_session_ticket_ctx->key); - gnutls_session_ticket_enable_server(tls->c.tls_session, &session_ticket_key); + if (net->tls_session_ticket_ctx) { + tls_session_ticket_enable(net->tls_session_ticket_ctx, + tls->c.tls_session); } return tls; @@ -1084,75 +979,4 @@ int tls_client_ctx_set_params(struct tls_client_ctx_t *ctx, return kr_ok(); } -static void session_ticket_timer_callback(uv_timer_t *timer) -{ - struct tls_session_ticket_ctx *ctx = (struct tls_session_ticket_ctx *)timer->data; - struct session_ticket_key *key = ctx->key; - assert(key); - ctx->epoch += 1; - session_ticket_key_recompute(key, ctx->epoch); - kr_log_verbose("[tls] TLS session ticket key regeneration\n"); - uv_timer_again(&ctx->key_timer); -} - -struct tls_session_ticket_ctx* tls_session_ticket_ctx_create(uv_loop_t *loop, - const char *salt, - size_t salt_len) -{ - assert(loop && salt && salt_len); - - struct tls_session_ticket_ctx *ctx = malloc(sizeof(struct tls_session_ticket_ctx)); - if (ctx == NULL) { - return NULL; - } - - struct session_ticket_key *key = session_ticket_key_create(salt, salt_len); - if (key == NULL) { - free(ctx); - return NULL; - } - - if (uv_timer_init(loop, &ctx->key_timer) != 0) { - session_ticket_key_destroy(key); - free(ctx); - return NULL; - } - - ctx->key_timer.data = ctx; - int res = uv_timer_start(&ctx->key_timer, session_ticket_timer_callback, - TLS_SESSION_TICKET_KEY_REGENERATION_INTERVAL, - TLS_SESSION_TICKET_KEY_REGENERATION_INTERVAL); - - if (res != 0) { - session_ticket_key_destroy(key); - free(ctx); - return NULL; - } - - ctx->key = key; - ctx->epoch = 0; - - session_ticket_timer_callback(&ctx->key_timer); - - return ctx; -} - -void tls_session_ticket_ctx_destroy(struct tls_session_ticket_ctx *ctx) -{ - if (ctx == NULL) { - return; - } - - if (ctx->key != NULL) { - session_ticket_key_destroy(ctx->key); - ctx->key = NULL; - } - - ctx->epoch = 0; - ctx->key_timer.data = NULL; - uv_timer_stop(&ctx->key_timer); - - free(ctx); -} - #undef DEBUG_MSG diff --git a/daemon/tls.h b/daemon/tls.h index eb1454ee9..724463ecc 100644 --- a/daemon/tls.h +++ b/daemon/tls.h @@ -100,8 +100,6 @@ struct tls_client_ctx_t { struct tls_client_paramlist_entry *params; }; -struct tls_session_ticket_ctx; - /*! Create an empty TLS context in query context */ struct tls_ctx_t* tls_new(struct worker_ctx *worker); @@ -170,9 +168,30 @@ int tls_client_ctx_set_params(struct tls_client_ctx_t *ctx, struct tls_client_paramlist_entry *entry, struct session *session); -/** Create the session ticket context and copy the salt. */ -struct tls_session_ticket_ctx* tls_session_ticket_ctx_create(uv_loop_t *loop, - const char *salt, - size_t salt_len); -/** Free all resources of the session ticket context. */ + +/* Session tickets, server side. Implementation in ./tls_session_ticket-srv.c */ + +/*! Opaque struct used by tls_session_ticket_* functions. */ +struct tls_session_ticket_ctx; + +/*! Suggested maximum reasonable secret length. */ +#define TLS_SESSION_TICKET_SECRET_MAX_LEN 1024 + +/*! Create a session ticket context and initialize it (secret gets copied inside). + * + * Passing zero-length secret implies using a random key, i.e. not synchronized + * between multiple instances. + * + * Beware that knowledge of the secret (if nonempty) breaks forward secrecy, + * so you should rotate the secret regularly and securely erase all past secrets. + * With TLS < 1.3 it's probably too risky to set nonempty secret. + */ +struct tls_session_ticket_ctx * tls_session_ticket_ctx_create( + uv_loop_t *loop, const char *secret, size_t secret_len); + +/*! Try to enable session tickets for a server session. */ +void tls_session_ticket_enable(struct tls_session_ticket_ctx *ctx, gnutls_session_t session); + +/*! Free all resources of the session ticket context. NULL is accepted as well. */ void tls_session_ticket_ctx_destroy(struct tls_session_ticket_ctx *ctx); + diff --git a/daemon/tls_session_ticket-srv.c b/daemon/tls_session_ticket-srv.c new file mode 100644 index 000000000..9456f0c68 --- /dev/null +++ b/daemon/tls_session_ticket-srv.c @@ -0,0 +1,254 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + 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, see . + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#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. */ +#ifndef TLS_SESSION_RESUMPTION_SYNC + /* Probably not much sense having it with gnutls < 3.6. */ + #define TLS_SESSION_RESUMPTION_SYNC (GNUTLS_VERSION_NUMBER >= 0x030600) +#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 + +#if GNUTLS_VERSION_NUMBER >= 0x030407 + #define TST_HASH GNUTLS_DIG_SHA3_512 +#else + #define TST_HASH abort() +#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 (secret_len && + (!secret || hash_len > UINT16_MAX || hash_len < secret_len)) { + assert(!EINVAL); + return NULL; + /* reasonable secret_len is best enforced in config API */ + } + if (!tst_key_invariants()) { + assert(!EFAULT); + return NULL; + } + #if !TLS_SESSION_RESUMPTION_SYNC + if (secret_len) { + kr_log_error("[tls] session ticket: secrets not enabled (compile-time)\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 (key_tmp.size != SESSION_KEY_SIZE) { + assert(!EFAULT); + 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 (!ctx || ctx->hash_len < sizeof(epoch)) { + assert(!EINVAL); + return kr_error(EINVAL); + } + if (!force_update && memcmp(ctx->hash_data, &epoch, sizeof(epoch)) == 0) { + return kr_ok(); /* we are up to date */ + /* TODO: support mixing endians? */ + } + 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 + assert(false); + 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) +{ + assert(timer); + tst_ctx_t *ctx = timer->data; + assert(ctx); + 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) { + assert(err != kr_error(EINVAL)); + kr_log_error("[tls] session ticket: failed rotation, err = %d\n", err); + } + /* 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; + assert(remain_ms < (TST_KEY_LIFETIME + 1 /*rounding tolerance*/) * 1000); + kr_log_verbose("[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 (err) { + assert(false); + kr_log_error("[tls] session ticket: failed to schedule, err = %d\n", err); + } +} + +/* Implementation for prototypes from ./tls.h */ + +void tls_session_ticket_enable(struct tls_session_ticket_ctx *ctx, gnutls_session_t session) +{ + assert(ctx && session); + 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) +{ + assert(loop && (!secret_len || secret)); + 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); +} +