MEDIUM: Add support for configurable TLS ticket keys
Until now, the TLS ticket keys couldn't have been configured and
shared between multiple instances or multiple servers running HAproxy.
The result was that if a request got a TLS ticket from one instance/server
and it hits another one afterwards, it will have to go through the full
SSL handshake and negotation.
This patch enables adding a ticket file to the bind line, which will be
used for all SSL contexts created from that bind line. We can use the
same file on all instances or servers to mitigate this issue and have
consistent TLS tickets assigned. Clients will no longer have to negotiate
every time they change the handling process.
Signed-off-by: Nenad Merdanovic <nmerdan@anine.io>
diff --git a/include/common/defaults.h b/include/common/defaults.h
index 0e37dac..3b31849 100644
--- a/include/common/defaults.h
+++ b/include/common/defaults.h
@@ -290,4 +290,9 @@
#ifndef OCSP_MAX_RESPONSE_TIME_SKEW
#define OCSP_MAX_RESPONSE_TIME_SKEW 300
#endif
+
+/* Number of TLS tickets to check, used for rotation */
+#ifndef TLS_TICKETS_NO
+#define TLS_TICKETS_NO 3
+#endif
#endif /* _COMMON_DEFAULTS_H */
diff --git a/include/types/listener.h b/include/types/listener.h
index 6dcaacc..2f5d566 100644
--- a/include/types/listener.h
+++ b/include/types/listener.h
@@ -132,6 +132,8 @@
int strict_sni; /* refuse negotiation if sni doesn't match a certificate */
struct eb_root sni_ctx; /* sni_ctx tree of all known certs full-names sorted by name */
struct eb_root sni_w_ctx; /* sni_ctx tree of all known certs wildcards sorted by name */
+ struct tls_sess_key *tls_ticket_keys; /* TLS ticket keys */
+ int tls_ticket_enc_index; /* array index of the key to use for encryption */
#endif
int is_ssl; /* SSL is required for these listeners */
unsigned long bind_proc; /* bitmask of processes allowed to use these listeners */
diff --git a/include/types/ssl_sock.h b/include/types/ssl_sock.h
index a0b2d79..d769acd 100644
--- a/include/types/ssl_sock.h
+++ b/include/types/ssl_sock.h
@@ -32,4 +32,10 @@
struct ebmb_node name; /* node holding the servername value */
};
+struct tls_sess_key {
+ unsigned char name[16];
+ unsigned char aes_key[16];
+ unsigned char hmac_key[16];
+} __attribute__((packed));
+
#endif /* _TYPES_SSL_SOCK_H */
diff --git a/src/cfgparse.c b/src/cfgparse.c
index ba07794..f2dc839 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -7740,6 +7740,7 @@
free(bind_conf->ciphers);
free(bind_conf->ecdhe);
free(bind_conf->crl_file);
+ free(bind_conf->tls_ticket_keys);
#endif /* USE_OPENSSL */
}
diff --git a/src/ssl_sock.c b/src/ssl_sock.c
index 8739e8b..bcf70c9 100644
--- a/src/ssl_sock.c
+++ b/src/ssl_sock.c
@@ -57,6 +57,7 @@
#include <common/ticks.h>
#include <common/time.h>
#include <common/cfgparse.h>
+#include <common/base64.h>
#include <ebsttree.h>
@@ -95,6 +96,13 @@
#define SSL_SOCK_ST_TO_CAEDEPTH(s) ((s >> (6+16)) & 15)
#define SSL_SOCK_ST_TO_CRTERROR(s) ((s >> (4+6+16)) & 63)
+/* Supported hash function for TLS tickets */
+#ifdef OPENSSL_NO_SHA256
+#define HASH_FUNCT EVP_sha1
+#else
+#define HASH_FUNCT EVP_sha256
+#endif /* OPENSSL_NO_SHA256 */
+
/* server and bind verify method, it uses a global value as default */
enum {
SSL_SOCK_VERIFY_DEFAULT = 0,
@@ -388,6 +396,47 @@
return ret;
}
+#if (defined SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB && TLS_TICKETS_NO > 0)
+static int ssl_tlsext_ticket_key_cb(SSL *s, unsigned char key_name[16], unsigned char *iv, EVP_CIPHER_CTX *ectx, HMAC_CTX *hctx, int enc)
+{
+ struct tls_sess_key *keys;
+ struct connection *conn;
+ int head;
+ int i;
+
+ conn = (struct connection *)SSL_get_app_data(s);
+ keys = objt_listener(conn->target)->bind_conf->tls_ticket_keys;
+ head = objt_listener(conn->target)->bind_conf->tls_ticket_enc_index;
+
+ if (enc) {
+ memcpy(key_name, keys[head].name, 16);
+
+ if(!RAND_pseudo_bytes(iv, EVP_MAX_IV_LENGTH))
+ return -1;
+
+ if(!EVP_EncryptInit_ex(ectx, EVP_aes_128_cbc(), NULL, keys[head].aes_key, iv))
+ return -1;
+
+ HMAC_Init_ex(hctx, keys[head].hmac_key, 16, HASH_FUNCT(), NULL);
+
+ return 1;
+ } else {
+ for (i = 0; i < TLS_TICKETS_NO; i++) {
+ if (!memcmp(key_name, keys[(head + i) % TLS_TICKETS_NO].name, 16))
+ goto found;
+ }
+ return 0;
+
+ found:
+ HMAC_Init_ex(hctx, keys[(head + i) % TLS_TICKETS_NO].hmac_key, 16, HASH_FUNCT(), NULL);
+ if(!EVP_DecryptInit_ex(ectx, EVP_aes_128_cbc(), NULL, keys[(head + i) % TLS_TICKETS_NO].aes_key, iv))
+ return -1;
+ /* 2 for key renewal, 1 if current key is still valid */
+ return i ? 2 : 1;
+ }
+}
+#endif /* SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB */
+
/*
* Callback used to set OCSP status extension content in server hello.
*/
@@ -1585,6 +1634,16 @@
ERR_clear_error();
}
+#if (defined SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB && TLS_TICKETS_NO > 0)
+ if(bind_conf->tls_ticket_keys) {
+ if (!SSL_CTX_set_tlsext_ticket_key_cb(ctx, ssl_tlsext_ticket_key_cb)) {
+ Alert("Proxy '%s': unable to set callback for TLS ticket validation for bind '%s' at [%s:%d].\n",
+ curproxy->id, bind_conf->arg, bind_conf->file, bind_conf->line);
+ cfgerr++;
+ }
+ }
+#endif
+
if (global.tune.ssllifetime)
SSL_CTX_set_timeout(ctx, global.tune.ssllifetime);
@@ -4218,6 +4277,65 @@
{
conf->strict_sni = 1;
return 0;
+}
+
+/* parse the "tls-ticket-keys" bind keyword */
+static int bind_parse_tls_ticket_keys(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err)
+{
+#if (defined SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB && TLS_TICKETS_NO > 0)
+ FILE *f;
+ int i = 0;
+ char thisline[LINESIZE];
+
+ if (!*args[cur_arg + 1]) {
+ if (err)
+ memprintf(err, "'%s' : missing TLS ticket keys file path", args[cur_arg]);
+ return ERR_ALERT | ERR_FATAL;
+ }
+
+ conf->tls_ticket_keys = malloc(TLS_TICKETS_NO * sizeof(struct tls_sess_key));
+
+ if ((f = fopen(args[cur_arg + 1], "r")) == NULL) {
+ if (err)
+ memprintf(err, "'%s' : unable to load ssl tickets keys file", args[cur_arg+1]);
+ return ERR_ALERT | ERR_FATAL;
+ }
+
+ while (fgets(thisline, sizeof(thisline), f) != NULL) {
+ int len = strlen(thisline);
+ /* Strip newline characters from the end */
+ if(thisline[len - 1] == '\n')
+ thisline[--len] = 0;
+
+ if(thisline[len - 1] == '\r')
+ thisline[--len] = 0;
+
+ if (base64dec(thisline, len, (char *) (conf->tls_ticket_keys + i % TLS_TICKETS_NO), sizeof(struct tls_sess_key)) != sizeof(struct tls_sess_key)) {
+ if (err)
+ memprintf(err, "'%s' : unable to decode base64 key on line %d", args[cur_arg+1], i + 1);
+ return ERR_ALERT | ERR_FATAL;
+ }
+ i++;
+ }
+
+ if (i < TLS_TICKETS_NO) {
+ if (err)
+ memprintf(err, "'%s' : please supply at least %d keys in the tls-tickets-file", args[cur_arg+1], TLS_TICKETS_NO);
+ return ERR_ALERT | ERR_FATAL;
+ }
+
+ fclose(f);
+
+ /* Use penultimate key for encryption, handle when TLS_TICKETS_NO = 1 */
+ i-=2;
+ conf->tls_ticket_enc_index = i < 0 ? 0 : i;
+
+ return 0;
+#else
+ if (err)
+ memprintf(err, "'%s' : TLS ticket callback extension not supported", args[cur_arg]);
+ return ERR_ALERT | ERR_FATAL;
+#endif /* SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB */
}
/* parse the "verify" bind keyword */
@@ -4643,28 +4761,29 @@
* not enabled.
*/
static struct bind_kw_list bind_kws = { "SSL", { }, {
- { "alpn", bind_parse_alpn, 1 }, /* set ALPN supported protocols */
- { "ca-file", bind_parse_ca_file, 1 }, /* set CAfile to process verify on client cert */
- { "ca-ignore-err", bind_parse_ignore_err, 1 }, /* set error IDs to ignore on verify depth > 0 */
- { "ciphers", bind_parse_ciphers, 1 }, /* set SSL cipher suite */
- { "crl-file", bind_parse_crl_file, 1 }, /* set certificat revocation list file use on client cert verify */
- { "crt", bind_parse_crt, 1 }, /* load SSL certificates from this location */
- { "crt-ignore-err", bind_parse_ignore_err, 1 }, /* set error IDs to ingore on verify depth == 0 */
- { "crt-list", bind_parse_crt_list, 1 }, /* load a list of crt from this location */
- { "ecdhe", bind_parse_ecdhe, 1 }, /* defines named curve for elliptic curve Diffie-Hellman */
- { "force-sslv3", bind_parse_force_sslv3, 0 }, /* force SSLv3 */
- { "force-tlsv10", bind_parse_force_tlsv10, 0 }, /* force TLSv10 */
- { "force-tlsv11", bind_parse_force_tlsv11, 0 }, /* force TLSv11 */
- { "force-tlsv12", bind_parse_force_tlsv12, 0 }, /* force TLSv12 */
- { "no-sslv3", bind_parse_no_sslv3, 0 }, /* disable SSLv3 */
- { "no-tlsv10", bind_parse_no_tlsv10, 0 }, /* disable TLSv10 */
- { "no-tlsv11", bind_parse_no_tlsv11, 0 }, /* disable TLSv11 */
- { "no-tlsv12", bind_parse_no_tlsv12, 0 }, /* disable TLSv12 */
- { "no-tls-tickets", bind_parse_no_tls_tickets, 0 }, /* disable session resumption tickets */
- { "ssl", bind_parse_ssl, 0 }, /* enable SSL processing */
- { "strict-sni", bind_parse_strict_sni, 0 }, /* refuse negotiation if sni doesn't match a certificate */
- { "verify", bind_parse_verify, 1 }, /* set SSL verify method */
- { "npn", bind_parse_npn, 1 }, /* set NPN supported protocols */
+ { "alpn", bind_parse_alpn, 1 }, /* set ALPN supported protocols */
+ { "ca-file", bind_parse_ca_file, 1 }, /* set CAfile to process verify on client cert */
+ { "ca-ignore-err", bind_parse_ignore_err, 1 }, /* set error IDs to ignore on verify depth > 0 */
+ { "ciphers", bind_parse_ciphers, 1 }, /* set SSL cipher suite */
+ { "crl-file", bind_parse_crl_file, 1 }, /* set certificat revocation list file use on client cert verify */
+ { "crt", bind_parse_crt, 1 }, /* load SSL certificates from this location */
+ { "crt-ignore-err", bind_parse_ignore_err, 1 }, /* set error IDs to ingore on verify depth == 0 */
+ { "crt-list", bind_parse_crt_list, 1 }, /* load a list of crt from this location */
+ { "ecdhe", bind_parse_ecdhe, 1 }, /* defines named curve for elliptic curve Diffie-Hellman */
+ { "force-sslv3", bind_parse_force_sslv3, 0 }, /* force SSLv3 */
+ { "force-tlsv10", bind_parse_force_tlsv10, 0 }, /* force TLSv10 */
+ { "force-tlsv11", bind_parse_force_tlsv11, 0 }, /* force TLSv11 */
+ { "force-tlsv12", bind_parse_force_tlsv12, 0 }, /* force TLSv12 */
+ { "no-sslv3", bind_parse_no_sslv3, 0 }, /* disable SSLv3 */
+ { "no-tlsv10", bind_parse_no_tlsv10, 0 }, /* disable TLSv10 */
+ { "no-tlsv11", bind_parse_no_tlsv11, 0 }, /* disable TLSv11 */
+ { "no-tlsv12", bind_parse_no_tlsv12, 0 }, /* disable TLSv12 */
+ { "no-tls-tickets", bind_parse_no_tls_tickets, 0 }, /* disable session resumption tickets */
+ { "ssl", bind_parse_ssl, 0 }, /* enable SSL processing */
+ { "strict-sni", bind_parse_strict_sni, 0 }, /* refuse negotiation if sni doesn't match a certificate */
+ { "tls-ticket-keys", bind_parse_tls_ticket_keys, 1 }, /* set file to load TLS ticket keys from */
+ { "verify", bind_parse_verify, 1 }, /* set SSL verify method */
+ { "npn", bind_parse_npn, 1 }, /* set NPN supported protocols */
{ NULL, NULL, 0 },
}};