]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
daemon/tls: work on server-side session tickets
authorVladimír Čunát <vladimir.cunat@nic.cz>
Thu, 31 May 2018 11:51:03 +0000 (13:51 +0200)
committerPetr Špaček <petr.spacek@nic.cz>
Wed, 13 Jun 2018 13:47:04 +0000 (15:47 +0200)
NEWS
daemon/README.rst
daemon/bindings.c
daemon/daemon.mk
daemon/network.c
daemon/network.h
daemon/tls.c
daemon/tls.h
daemon/tls_session_ticket-srv.c [new file with mode: 0644]

diff --git a/NEWS b/NEWS
index 5f59c1735c706a924b00cf4b63ac73556435ee8e..cdbc4ab53c9f3bdb00b41f3a22d8204e992181a1 100644 (file)
--- 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)
index 5c790abf337d1da5a97e4d7f1b61eb7e03c51dbd..32289823d544106a086518728c2cd2f1bc13da4e 100644 (file)
@@ -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 <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:
 
index e1ca117e7fed6c30d53aa879bb27b3a345bd9fb2..fe489331e3f2cf899df4098a77e29dce84d8ed88 100644 (file)
@@ -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 }
index 39421af58518a4ba6427964880ae0c11c85031bd..c3c15075ef8dfc552a129d844671367a302969c1 100644 (file)
@@ -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
 
index 70c4781221edf2882984f6e03e7aead75d2e6768..77186d64c335ddee8eead7a070ffbdbcf0c94e4d 100644 (file)
@@ -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);
        }
 }
 
index ca50c204f97d0c027a25e01c59723bb7886ea4ee..241e595d762a153ec05084c252990592724fea0e 100644 (file)
@@ -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;
index debd0aa6b5237ac863447c6e3dfaf5486b0b9209..3036a7f94921edb2558e7da5e0271c93a9c4d090 100644 (file)
  * 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
@@ -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
index eb1454ee901f01e6fc3493a3ee4e4857536544fc..724463ecc4f6b8bf84545342eeeae33309202f31 100644 (file)
@@ -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 (file)
index 0000000..9456f0c
--- /dev/null
@@ -0,0 +1,254 @@
+/*  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);
+}
+