MEDIUM: Add support for updating TLS ticket keys via socket

Until now, HAproxy needed to be restarted to change the TLS ticket
keys. With this patch, the TLS keys can be updated on a per-file
basis using the admin socket. Two new socket commands have been
introduced: "show tls-keys" and "set ssl tls-keys".

Signed-off-by: Nenad Merdanovic <nmerdan@anine.io>
diff --git a/src/dumpstats.c b/src/dumpstats.c
index 35391ae..885a159 100644
--- a/src/dumpstats.c
+++ b/src/dumpstats.c
@@ -66,6 +66,7 @@
 
 #ifdef USE_OPENSSL
 #include <proto/ssl_sock.h>
+#include <types/ssl_sock.h>
 #endif
 
 /* stats socket states */
@@ -88,6 +89,7 @@
 	STAT_CLI_O_PAT,      /* list all entries of a pattern */
 	STAT_CLI_O_MLOOK,    /* lookup a map entry */
 	STAT_CLI_O_POOLS,    /* dump memory pools */
+	STAT_CLI_O_TLSK,     /* list all TLS ticket keys references */
 };
 
 /* Actions available for the stats admin forms */
@@ -134,6 +136,9 @@
 static int stats_pats_list(struct stream_interface *si);
 static int stats_pat_list(struct stream_interface *si);
 static int stats_map_lookup(struct stream_interface *si);
+#if (defined SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB && TLS_TICKETS_NO > 0)
+static int stats_tlskeys_list(struct stream_interface *si);
+#endif
 static void cli_release_handler(struct appctx *appctx);
 
 /*
@@ -973,6 +978,51 @@
 
 	return sv;
 }
+
+/* This function is used with TLS ticket keys management. It permits to browse
+ * each reference. The variable <getnext> must contain the current node,
+ * <end> point to the root node.
+ */
+#if (defined SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB && TLS_TICKETS_NO > 0)
+static inline
+struct tls_keys_ref *tlskeys_list_get_next(struct tls_keys_ref *getnext, struct list *end)
+{
+	struct tls_keys_ref *ref = getnext;
+
+	while (1) {
+
+		/* Get next list entry. */
+		ref = LIST_NEXT(&ref->list, struct tls_keys_ref *, list);
+
+		/* If the entry is the last of the list, return NULL. */
+		if (&ref->list == end)
+			return NULL;
+
+		return ref;
+	}
+}
+
+static inline
+struct tls_keys_ref *tlskeys_ref_lookup_ref(const char *reference)
+{
+	int id;
+	char *error;
+
+	/* If the reference starts by a '#', this is numeric id. */
+	if (reference[0] == '#') {
+		/* Try to convert the numeric id. If the conversion fails, the lookup fails. */
+		id = strtol(reference + 1, &error, 10);
+		if (*error != '\0')
+			return NULL;
+
+		/* Perform the unique id lookup. */
+		return tlskeys_ref_lookupid(id);
+	}
+
+	/* Perform the string lookup. */
+	return tlskeys_ref_lookup(reference);
+}
+#endif
 
 /* This function is used with map and acl management. It permits to browse
  * each reference. The variable <getnext> must contain the current node,
@@ -1147,6 +1197,17 @@
 		else if (strcmp(args[1], "table") == 0) {
 			stats_sock_table_request(si, args, STAT_CLI_O_TAB);
 		}
+		else if (strcmp(args[1], "tls-keys") == 0) {
+#if (defined SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB && TLS_TICKETS_NO > 0)
+			appctx->st2 = STAT_ST_INIT;
+			appctx->st0 = STAT_CLI_O_TLSK;
+#else
+			appctx->ctx.cli.msg = "HAProxy was compiled against a version of OpenSSL "
+						"that doesn't support specifying TLS ticket keys\n";
+			appctx->st0 = STAT_CLI_PRINT;
+#endif
+			return 1;
+		}
 		else if (strcmp(args[1], "map") == 0 ||
 		         strcmp(args[1], "acl") == 0) {
 
@@ -1812,6 +1873,42 @@
 				return 1;
 #endif
 			}
+			else if (strcmp(args[2], "tls-key") == 0) {
+#if (defined SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB && TLS_TICKETS_NO > 0)
+				/* Expect two parameters: the filename and the new new TLS key in encoding */
+				if (!*args[3] || !*args[4]) {
+					appctx->ctx.cli.msg = "'set ssl tls-key' expects a filename and the new TLS key in base64 encoding.\n";
+					appctx->st0 = STAT_CLI_PRINT;
+					return 1;
+				}
+
+				appctx->ctx.tlskeys.ref = tlskeys_ref_lookup_ref(args[3]);
+				if(!appctx->ctx.tlskeys.ref) {
+					appctx->ctx.cli.msg = "'set ssl tls-key' unable to locate referenced filename\n";
+					appctx->st0 = STAT_CLI_PRINT;
+					return 1;
+				}
+
+				trash.len = base64dec(args[4], strlen(args[4]), trash.str, trash.size);
+				if (trash.len != sizeof(struct tls_sess_key)) {
+					appctx->ctx.cli.msg = "'set ssl tls-key' received invalid base64 encoded TLS key.\n";
+					appctx->st0 = STAT_CLI_PRINT;
+					return 1;
+				}
+
+				memcpy(appctx->ctx.tlskeys.ref->tlskeys + 2 % TLS_TICKETS_NO, trash.str, trash.len);
+				appctx->ctx.tlskeys.ref->tls_ticket_enc_index = appctx->ctx.tlskeys.ref->tls_ticket_enc_index + 1 % TLS_TICKETS_NO;
+
+				appctx->ctx.cli.msg = "TLS ticket key updated!";
+				appctx->st0 = STAT_CLI_PRINT;
+				return 1;
+#else
+				appctx->ctx.cli.msg = "HAProxy was compiled against a version of OpenSSL "
+							"that doesn't support specifying TLS ticket keys\n";
+				appctx->st0 = STAT_CLI_PRINT;
+				return 1;
+#endif
+			}
 			else {
 				appctx->ctx.cli.msg = "'set ssl' only supports 'ocsp-response'.\n";
 				appctx->st0 = STAT_CLI_PRINT;
@@ -2375,6 +2472,12 @@
 				if (stats_dump_pools_to_buffer(si))
 					appctx->st0 = STAT_CLI_PROMPT;
 				break;
+#if (defined SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB && TLS_TICKETS_NO > 0)
+			case STAT_CLI_O_TLSK:
+				if (stats_tlskeys_list(si))
+					appctx->st0 = STAT_CLI_PROMPT;
+				break;
+#endif
 			default: /* abnormal state */
 				cli_release_handler(appctx);
 				appctx->st0 = STAT_CLI_PROMPT;
@@ -5321,6 +5424,64 @@
 	return 1;
 }
 
+#if (defined SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB && TLS_TICKETS_NO > 0)
+static int stats_tlskeys_list(struct stream_interface *si) {
+	struct appctx *appctx = __objt_appctx(si->end);
+
+	switch (appctx->st2) {
+	case STAT_ST_INIT:
+		/* Display the column headers. If the message cannot be sent,
+		 * quit the fucntion with returning 0. The function is called
+		 * later and restart at the state "STAT_ST_INIT".
+		 */
+		chunk_reset(&trash);
+		chunk_appendf(&trash, "# id (file)\n");
+		if (bi_putchk(si_ic(si), &trash) == -1) {
+			si_applet_cant_put(si);
+			return 0;
+		}
+
+		/* Now, we start the browsing of the references lists.
+		 * Note that the following call to LIST_ELEM return bad pointer. The only
+		 * avalaible field of this pointer is <list>. It is used with the function
+		 * tlskeys_list_get_next() for retruning the first avalaible entry
+		 */
+		appctx->ctx.tlskeys.ref = LIST_ELEM(&tlskeys_reference, struct tls_keys_ref *, list);
+		appctx->ctx.tlskeys.ref = tlskeys_list_get_next(appctx->ctx.tlskeys.ref, &tlskeys_reference);
+
+		appctx->st2 = STAT_ST_LIST;
+		/* fall through */
+
+	case STAT_ST_LIST:
+		while (appctx->ctx.tlskeys.ref) {
+			chunk_reset(&trash);
+
+			chunk_appendf(&trash, "%d (%s)\n", appctx->ctx.tlskeys.ref->unique_id,
+			              appctx->ctx.tlskeys.ref->filename);
+
+			if (bi_putchk(si_ic(si), &trash) == -1) {
+				/* let's try again later from this stream. We add ourselves into
+				 * this stream's users so that it can remove us upon termination.
+				 */
+				si_applet_cant_put(si);
+				return 0;
+			}
+
+			/* get next list entry and check the end of the list */
+			appctx->ctx.tlskeys.ref = tlskeys_list_get_next(appctx->ctx.tlskeys.ref, &tlskeys_reference);
+		}
+
+		appctx->st2 = STAT_ST_FIN;
+		/* fall through */
+
+	default:
+		appctx->st2 = STAT_ST_FIN;
+		return 1;
+	}
+	return 0;
+}
+#endif
+
 static int stats_pats_list(struct stream_interface *si)
 {
 	struct appctx *appctx = __objt_appctx(si->end);
diff --git a/src/haproxy.c b/src/haproxy.c
index 233c434..353ff8a 100644
--- a/src/haproxy.c
+++ b/src/haproxy.c
@@ -734,6 +734,9 @@
 	}
 
 	pattern_finalize_config();
+#if (defined SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB && TLS_TICKETS_NO > 0)
+	tlskeys_finalize_config();
+#endif
 
 	err_code |= check_config_validity();
 	if (err_code & (ERR_ABORT|ERR_FATAL)) {
diff --git a/src/ssl_sock.c b/src/ssl_sock.c
index 145b8a9..9a47cf1 100644
--- a/src/ssl_sock.c
+++ b/src/ssl_sock.c
@@ -115,6 +115,10 @@
 int sslconns = 0;
 int totalsslconns = 0;
 
+#if (defined SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB && TLS_TICKETS_NO > 0)
+struct list tlskeys_reference = LIST_HEAD_INIT(tlskeys_reference);
+#endif
+
 #ifndef OPENSSL_NO_DH
 static DH *local_dh_1024 = NULL;
 static DH *local_dh_2048 = NULL;
@@ -435,7 +439,89 @@
 		/* 2 for key renewal, 1 if current key is still valid */
 		return i ? 2 : 1;
 	}
+}
+
+struct tls_keys_ref *tlskeys_ref_lookup(const char *filename)
+{
+        struct tls_keys_ref *ref;
+
+        list_for_each_entry(ref, &tlskeys_reference, list)
+                if (ref->filename && strcmp(filename, ref->filename) == 0)
+                        return ref;
+        return NULL;
+}
+
+struct tls_keys_ref *tlskeys_ref_lookupid(int unique_id)
+{
+        struct tls_keys_ref *ref;
+
+        list_for_each_entry(ref, &tlskeys_reference, list)
+                if (ref->unique_id == unique_id)
+                        return ref;
+        return NULL;
+}
+
+int ssl_sock_update_tlskey(char *filename, struct chunk *tlskey, char **err) {
+	struct tls_keys_ref *ref = tlskeys_ref_lookup(filename);
+
+	if(!ref) {
+		memprintf(err, "Unable to locate the referenced filename: %s", filename);
+		return 1;
+	}
+
+	memcpy((char *) (ref->tlskeys + 2 % TLS_TICKETS_NO), tlskey->str, tlskey->len);
+	ref->tls_ticket_enc_index = ref->tls_ticket_enc_index + 1 % TLS_TICKETS_NO;
+
+	return 0;
 }
+
+/* This function finalize the configuration parsing. Its set all the
+ * automatic ids
+ */
+void tlskeys_finalize_config(void)
+{
+	int i = 0;
+	struct tls_keys_ref *ref, *ref2, *ref3;
+	struct list tkr = LIST_HEAD_INIT(tkr);
+
+	list_for_each_entry(ref, &tlskeys_reference, list) {
+		if (ref->unique_id == -1) {
+			/* Look for the first free id. */
+			while (1) {
+				list_for_each_entry(ref2, &tlskeys_reference, list) {
+					if (ref2->unique_id == i) {
+						i++;
+						break;
+					}
+				}
+				if (&ref2->list == &tlskeys_reference)
+					break;
+			}
+
+			/* Uses the unique id and increment it for the next entry. */
+			ref->unique_id = i;
+			i++;
+		}
+	}
+
+	/* This sort the reference list by id. */
+	list_for_each_entry_safe(ref, ref2, &tlskeys_reference, list) {
+		LIST_DEL(&ref->list);
+		list_for_each_entry(ref3, &tkr, list) {
+			if (ref->unique_id < ref3->unique_id) {
+				LIST_ADDQ(&ref3->list, &ref->list);
+				break;
+			}
+		}
+		if (&ref3->list == &tkr)
+			LIST_ADDQ(&tkr, &ref->list);
+	}
+
+	/* swap root */
+	LIST_ADD(&tkr, &tlskeys_reference);
+	LIST_DEL(&tkr);
+}
+
 #endif /* SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB */
 
 /*
@@ -4340,6 +4426,12 @@
 		return ERR_ALERT | ERR_FATAL;
 	}
 
+	keys_ref = tlskeys_ref_lookup(args[cur_arg + 1]);
+	if(keys_ref) {
+		conf->keys_ref = keys_ref;
+		return 0;
+	}
+
 	keys_ref = malloc(sizeof(struct tls_keys_ref));
 	keys_ref->tlskeys = malloc(TLS_TICKETS_NO * sizeof(struct tls_sess_key));
 
@@ -4379,8 +4471,11 @@
 	/* Use penultimate key for encryption, handle when TLS_TICKETS_NO = 1 */
 	i-=2;
 	keys_ref->tls_ticket_enc_index = i < 0 ? 0 : i;
+	keys_ref->unique_id = -1;
 	conf->keys_ref = keys_ref;
 
+	LIST_ADD(&tlskeys_reference, &keys_ref->list);
+
 	return 0;
 #else
 	if (err)