--------
- 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)
> 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.
> 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
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 <https://en.wikipedia.org/wiki/Forward_secrecy>`_.
+
+ .. 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:
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;
}
{ "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 }
daemon/ffimodule.c \
daemon/tls.c \
daemon/tls_ephemeral_credentials.c \
+ daemon/tls_session_ticket-srv.c \
daemon/zimport.c \
daemon/main.c
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);
}
}
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);
}
}
typedef array_t(struct endpoint*) endpoint_array_t;
/* @endcond */
+struct tls_session_ticket_ctx;
struct network {
uv_loop_t *loop;
map_t endpoints;
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#include <gnutls/gnutls.h>
-#include <gnutls/x509.h>
#include <gnutls/abstract.h>
#include <gnutls/crypto.h>
-#include <stdlib.h>
-#include <errno.h>
-#include <assert.h>
#include <gnutls/gnutls.h>
+#include <gnutls/x509.h>
#include <uv.h>
-#include <contrib/ucw/lib.h>
+#include <assert.h>
+#include <errno.h>
+#include <stdlib.h>
+
+#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
#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
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;
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
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);
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);
+
--- /dev/null
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ 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 <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#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. */
+#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);
+}
+