[PATCH 11 of 11] SSL: automatic rotation of session ticket keys
Maxim Dounin
mdounin at mdounin.ru
Fri Sep 16 21:08:19 UTC 2022
Hello!
On Thu, Sep 15, 2022 at 09:50:24AM +0400, Sergey Kandaurov wrote:
> > On 26 Aug 2022, at 07:01, Maxim Dounin <mdounin at mdounin.ru> wrote:
> >
> > # HG changeset patch
> > # User Maxim Dounin <mdounin at mdounin.ru>
> > # Date 1661481958 -10800
> > # Fri Aug 26 05:45:58 2022 +0300
> > # Node ID 5c26fe5f6ab0bf4c0d18cae8f6f6483348243d4b
> > # Parent 2487bf5766f79c813b3397b3bb897424c3590445
> > SSL: automatic rotation of session ticket keys.
> >
> > As long as ssl_session_cache in shared memory is configured, session ticket
> > keys are now automatically generated in shared memory, and rotated
> > periodically.
>
> It looks odd how session cache is (ab)used to store ticket keys.
> I understand that it is comfortable to reuse existing shared zone
> (and not to touch configuration) but probably a more correct way
> is to create a separate zone to store keys?
> Something pretty much the same as ssl_session_cache syntax:
> ssl_session_ticket_key file | shared:name:size
>
> What do you think?
I don't think it is a good idea to introduce additional shared
zones here, especially given that it only needs about 160 bytes of
shared memory. It really complicates things for users for no real
reason.
Rather, I was thinking about using nginx_shared_zone, which is
always available, but it is rather special and will require
additional integration code. Further, as a single contention
point it might be a problem, at least unless lock avoidance is
also implemented (see below). And a single key for all servers
might not be the best solution either. On the other hand,
ssl_session_cache in shared memory is readily available and
logically related, and I see no reasons not to use it to rotate
ticket keys if it's configured.
If there will be understanding that we need to rotate ticket keys
in all cases, even without ssl_session_cache in shared memory
being configured, the idea of using nginx_shared_zone might be
revisited.
> > This can be beneficial from forward secrecy point of view,
> > and also avoids increased CPU usage after configuration reloads.
> >
>
> You can also mention that this fixes resuming sessions with BoringSSL
> that implements its own ticket key rotation (if ticket keys callback
> isn't installed) that doesn't work in multiple workers configuration.
Thanks, forgot about this BoringSSL misfeature. Added the
following paragraph:
This also helps BoringSSL to properly resume sessions in configurations
with multiple worker processes and no ssl_session_ticket_key directives,
as BoringSSL tries to automatically rotate session ticket keys and does
this independently in different worker processes, thus breaking session
resumption between worker processes.
>
> > diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c
> > --- a/src/event/ngx_event_openssl.c
> > +++ b/src/event/ngx_event_openssl.c
> > @@ -74,6 +74,7 @@ static void ngx_ssl_session_rbtree_inser
> > static int ngx_ssl_ticket_key_callback(ngx_ssl_conn_t *ssl_conn,
> > unsigned char *name, unsigned char *iv, EVP_CIPHER_CTX *ectx,
> > HMAC_CTX *hctx, int enc);
> > +static ngx_int_t ngx_ssl_rotate_ticket_keys(SSL_CTX *ssl_ctx, ngx_log_t *log);
> > static void ngx_ssl_ticket_keys_cleanup(void *data);
> > #endif
> >
> > @@ -3767,6 +3768,9 @@ ngx_ssl_session_cache_init(ngx_shm_zone_
> >
> > ngx_queue_init(&cache->expire_queue);
> >
> > + cache->ticket_keys[0].expire = 0;
> > + cache->ticket_keys[1].expire = 0;
> > +
> > len = sizeof(" in SSL session shared cache \"\"") + shm_zone->shm.name.len;
> >
> > shpool->log_ctx = ngx_slab_alloc(shpool, len);
> > @@ -4234,11 +4238,13 @@ ngx_ssl_session_ticket_keys(ngx_conf_t *
> > ngx_pool_cleanup_t *cln;
> > ngx_ssl_ticket_key_t *key;
> >
> > - if (paths == NULL) {
> > + if (paths == NULL
> > + && SSL_CTX_get_ex_data(ssl->ctx, ngx_ssl_session_cache_index) == NULL)
> > + {
> > return NGX_OK;
> > }
> >
> > - keys = ngx_array_create(cf->pool, paths->nelts,
> > + keys = ngx_array_create(cf->pool, paths ? paths->nelts : 2,
> > sizeof(ngx_ssl_ticket_key_t));
> > if (keys == NULL) {
> > return NGX_ERROR;
> > @@ -4252,6 +4258,34 @@ ngx_ssl_session_ticket_keys(ngx_conf_t *
> > cln->handler = ngx_ssl_ticket_keys_cleanup;
> > cln->data = keys;
> >
> > + if (SSL_CTX_set_ex_data(ssl->ctx, ngx_ssl_ticket_keys_index, keys) == 0) {
> > + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> > + "SSL_CTX_set_ex_data() failed");
> > + return NGX_ERROR;
> > + }
> > +
> > + if (SSL_CTX_set_tlsext_ticket_key_cb(ssl->ctx, ngx_ssl_ticket_key_callback)
> > + == 0)
> > + {
> > + ngx_log_error(NGX_LOG_WARN, cf->log, 0,
> > + "nginx was built with Session Tickets support, however, "
> > + "now it is linked dynamically to an OpenSSL library "
> > + "which has no tlsext support, therefore Session Tickets "
> > + "are not available");
> > + return NGX_OK;
> > + }
> > +
> > + if (paths == NULL) {
> > +
> > + /* placeholder for keys in shared memory */
> > +
> > + key = ngx_array_push_n(keys, 2);
> > + key[0].shared = 1;
> > + key[1].shared = 1;
> > +
> > + return NGX_OK;
> > + }
> > +
> > path = paths->elts;
> > for (i = 0; i < paths->nelts; i++) {
> >
> > @@ -4306,6 +4340,8 @@ ngx_ssl_session_ticket_keys(ngx_conf_t *
> > goto failed;
> > }
> >
> > + key->shared = 0;
> > +
> > if (size == 48) {
> > key->size = 48;
> > ngx_memcpy(key->name, buf, 16);
> > @@ -4327,22 +4363,6 @@ ngx_ssl_session_ticket_keys(ngx_conf_t *
> > ngx_explicit_memzero(&buf, 80);
> > }
> >
> > - if (SSL_CTX_set_ex_data(ssl->ctx, ngx_ssl_ticket_keys_index, keys) == 0) {
> > - ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> > - "SSL_CTX_set_ex_data() failed");
> > - return NGX_ERROR;
> > - }
> > -
> > - if (SSL_CTX_set_tlsext_ticket_key_cb(ssl->ctx, ngx_ssl_ticket_key_callback)
> > - == 0)
> > - {
> > - ngx_log_error(NGX_LOG_WARN, cf->log, 0,
> > - "nginx was built with Session Tickets support, however, "
> > - "now it is linked dynamically to an OpenSSL library "
> > - "which has no tlsext support, therefore Session Tickets "
> > - "are not available");
> > - }
> > -
> > return NGX_OK;
> >
> > failed:
> > @@ -4375,6 +4395,10 @@ ngx_ssl_ticket_key_callback(ngx_ssl_conn
> > c = ngx_ssl_get_connection(ssl_conn);
> > ssl_ctx = c->ssl->session_ctx;
> >
> > + if (ngx_ssl_rotate_ticket_keys(ssl_ctx, c->log) != NGX_OK) {
> > + return -1;
> > + }
> > +
> > #ifdef OPENSSL_NO_SHA256
> > digest = EVP_sha1();
> > #else
> > @@ -4493,6 +4517,113 @@ ngx_ssl_ticket_key_callback(ngx_ssl_conn
> > }
> >
> >
> > +static ngx_int_t
> > +ngx_ssl_rotate_ticket_keys(SSL_CTX *ssl_ctx, ngx_log_t *log)
> > +{
> > + time_t now, expire;
> > + ngx_array_t *keys;
> > + ngx_shm_zone_t *shm_zone;
> > + ngx_slab_pool_t *shpool;
> > + ngx_ssl_ticket_key_t *key;
> > + ngx_ssl_session_cache_t *cache;
> > + u_char buf[80];
>
> BTW, the same buf[] variable in ngx_ssl_session_ticket_keys()
> is placed at the beginning of declaration block.
> Looks like a chance to improve style consistency (but see below).
There are enough style changes in this series already.
> > +
> > + keys = SSL_CTX_get_ex_data(ssl_ctx, ngx_ssl_ticket_keys_index);
> > + if (keys == NULL) {
> > + return NGX_OK;
> > + }
> > +
> > + key = keys->elts;
> > +
> > + if (!key[0].shared) {
> > + return NGX_OK;
> > + }
> > +
> > + now = ngx_time();
> > + expire = now + SSL_CTX_get_timeout(ssl_ctx);
> > +
> > + shm_zone = SSL_CTX_get_ex_data(ssl_ctx, ngx_ssl_session_cache_index);
> > +
> > + cache = shm_zone->data;
> > + shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;
> > +
> > + ngx_shmtx_lock(&shpool->mutex);
> > +
> > + key = cache->ticket_keys;
> > +
> > + if (key[0].expire == 0) {
> > +
> > + /* initialize the current key */
> > +
> > + if (RAND_bytes(buf, 80) != 1) {
> > + ngx_ssl_error(NGX_LOG_ALERT, log, 0, "RAND_bytes() failed");
> > + ngx_shmtx_unlock(&shpool->mutex);
> > + return NGX_ERROR;
> > + }
> > +
> > + key->shared = 1;
> > + key->expire = expire;
> > + key->size = 80;
> > + ngx_memcpy(key->name, buf, 16);
> > + ngx_memcpy(key->hmac_key, buf + 16, 32);
> > + ngx_memcpy(key->aes_key, buf + 48, 32);
> > +
> > + ngx_explicit_memzero(&buf, 80);
> > +
> > + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
> > + "ssl ticket key: \"%*xs\"",
> > + (size_t) 16, key->name);
> > + }
> > +
> > + if (key[1].expire < now) {
> > +
> > + /*
> > + * if the previous key is no longer needed (or not initialized),
> > + * replace it with the current key and generate new current key
> > + */
> > +
> > + key[1] = key[0];
> > +
> > + if (RAND_bytes(buf, 80) != 1) {
> > + ngx_ssl_error(NGX_LOG_ALERT, log, 0, "RAND_bytes() failed");
> > + ngx_shmtx_unlock(&shpool->mutex);
> > + return NGX_ERROR;
> > + }
> > +
> > + key->shared = 1;
> > + key->expire = expire;
> > + key->size = 80;
> > + ngx_memcpy(key->name, buf, 16);
> > + ngx_memcpy(key->hmac_key, buf + 16, 32);
> > + ngx_memcpy(key->aes_key, buf + 48, 32);
> > +
> > + ngx_explicit_memzero(&buf, 80);
> > +
> > + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
> > + "ssl ticket key: \"%*xs\"",
> > + (size_t) 16, key->name);
> > + }
>
> I wonder if cpu intensive ops can be moved from under the lock.
> At least, buf[] can be removed if write random data directly
> to ngx_ssl_ticket_key_t. This eliminates 3 memory copies
> (likely optimized by compiler to a single) and explicit write.
Micro-optimizations are not something important here: new keys are
generated once per SSL session timeout, that is, once per 300
seconds by default. As such, the code is kept as close as
possible to the ticket keys reading in
ngx_ssl_session_ticket_keys(), and not dependant on the
ngx_ssl_ticket_key_t structure layout.
What can be interesting here is to avoid locking at all unless we
are going to update keys (change expiration of the current key, or
switch to the next key).
To do so without races, the code has to maintains 3 keys: current,
previous, and next. If a worker will switch to the next key
earlier, other workers will still be able to decrypt new tickets,
since they will be encrypted with the next key.
But with the SSL session cache in shared memory we have to lock
shared zone anyway to create sessions, and this was never seen to
be a performance bottleneck. Given that, and the added code
complexity, I've dropped the patch which avoids locking, at least
for now.
This might worth revisiting though. Patch provided below for
reference.
>
> diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c
> --- a/src/event/ngx_event_openssl.c
> +++ b/src/event/ngx_event_openssl.c
> @@ -4527,7 +4527,6 @@ ngx_ssl_rotate_ticket_keys(SSL_CTX *ssl_
> ngx_slab_pool_t *shpool;
> ngx_ssl_ticket_key_t *key;
> ngx_ssl_session_cache_t *cache;
> - u_char buf[80];
>
> keys = SSL_CTX_get_ex_data(ssl_ctx, ngx_ssl_ticket_keys_index);
> if (keys == NULL) {
> @@ -4556,7 +4555,7 @@ ngx_ssl_rotate_ticket_keys(SSL_CTX *ssl_
>
> /* initialize the current key */
>
> - if (RAND_bytes(buf, 80) != 1) {
> + if (RAND_bytes(key->name, 80) != 1) {
> ngx_ssl_error(NGX_LOG_ALERT, log, 0, "RAND_bytes() failed");
> ngx_shmtx_unlock(&shpool->mutex);
> return NGX_ERROR;
> @@ -4565,11 +4564,6 @@ ngx_ssl_rotate_ticket_keys(SSL_CTX *ssl_
> key->shared = 1;
> key->expire = expire;
> key->size = 80;
> - ngx_memcpy(key->name, buf, 16);
> - ngx_memcpy(key->hmac_key, buf + 16, 32);
> - ngx_memcpy(key->aes_key, buf + 48, 32);
> -
> - ngx_explicit_memzero(&buf, 80);
>
> ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
> "ssl ticket key: \"%*xs\"",
> @@ -4585,7 +4579,7 @@ ngx_ssl_rotate_ticket_keys(SSL_CTX *ssl_
>
> key[1] = key[0];
>
> - if (RAND_bytes(buf, 80) != 1) {
> + if (RAND_bytes(key->name, 80) != 1) {
> ngx_ssl_error(NGX_LOG_ALERT, log, 0, "RAND_bytes() failed");
> ngx_shmtx_unlock(&shpool->mutex);
> return NGX_ERROR;
> @@ -4594,11 +4588,6 @@ ngx_ssl_rotate_ticket_keys(SSL_CTX *ssl_
> key->shared = 1;
> key->expire = expire;
> key->size = 80;
> - ngx_memcpy(key->name, buf, 16);
> - ngx_memcpy(key->hmac_key, buf + 16, 32);
> - ngx_memcpy(key->aes_key, buf + 48, 32);
> -
> - ngx_explicit_memzero(&buf, 80);
>
> ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
> "ssl ticket key: \"%*xs\"",
>
> Moving RAND_bytes() can be achieved by trying to elide shmem lock
> and store random data elsewhere if condition is met.
> But this comes in conflict with buf[] optimization :).
You can't really move RAND_bytes() out of the lock unless you are
ok with doing unneeded RAND_bytes() calls in workers which weren't
first to update keys. And see above, you are trying to
micro-optimize something which happens once per 300 seconds or
even less frequently.
> > +
> > + /*
> > + * update expiration of the current key: it is going to be needed
> > + * at least till the session being created expires
> > + */
> > +
> > + if (expire > key[0].expire) {
> > + key[0].expire = expire;
> > + }
> > +
> > + /* sync keys to the worker process memory */
> > +
> > + ngx_memcpy(keys->elts, cache->ticket_keys,
> > + 2 * sizeof(ngx_ssl_ticket_key_t));
> > +
> > + ngx_shmtx_unlock(&shpool->mutex);
> > +
> > + return NGX_OK;
> > +}
> > +
> > +
> > static void
> > ngx_ssl_ticket_keys_cleanup(void *data)
> > {
> > diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h
> > --- a/src/event/ngx_event_openssl.h
> > +++ b/src/event/ngx_event_openssl.h
> > @@ -147,25 +147,24 @@ struct ngx_ssl_sess_id_s {
> >
> >
> > typedef struct {
> > + u_char name[16];
> > + u_char hmac_key[32];
> > + u_char aes_key[32];
> > + time_t expire;
> > + unsigned size:8;
> > + unsigned shared:1;
> > +} ngx_ssl_ticket_key_t;
> > +
> > +
> > +typedef struct {
> > ngx_rbtree_t session_rbtree;
> > ngx_rbtree_node_t sentinel;
> > ngx_queue_t expire_queue;
> > + ngx_ssl_ticket_key_t ticket_keys[2];
>
> Again, with the 1st patch applied, this makes using
> of ticket_keys mutually exclusive to other fields.
> This suggests that it doesn't belong here.
It look like you are incorrectly assuming that all clients support
session tickets. While this is true in TLSv1.3, it's not for
TLSv1.2 and below.
Further, the same ssl_session_cache can be used in multiple server
blocks with different ssl_session_ticket settings.
> > time_t fail_time;
> > } ngx_ssl_session_cache_t;
> >
> >
> > -#ifdef SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB
> > -
> > -typedef struct {
> > - size_t size;
> > - u_char name[16];
> > - u_char hmac_key[32];
> > - u_char aes_key[32];
> > -} ngx_ssl_ticket_key_t;
> > -
> > -#endif
> > -
> > -
> > #define NGX_SSL_SSLv2 0x0002
> > #define NGX_SSL_SSLv3 0x0004
> > #define NGX_SSL_TLSv1 0x0008
Lock avoidance patch below.
# HG changeset patch
# User Maxim Dounin <mdounin at mdounin.ru>
# Date 1663356500 -10800
# Fri Sep 16 22:28:20 2022 +0300
# Node ID 93cd7641604d85488ee4c6144e51cdeb60caa123
# Parent 6d94781680d6750268cd697fb72f7b72b73e0df1
SSL: optimized rotation of session ticket keys.
Instead of syncing keys with shared memory on each ticket operation,
the code now does this only when the worker is going to change expiration
of the current key, or going to switch to a new key: that is, usually
at most once per second.
To do so without races, the code maintains 3 keys: current, previous,
and next. If a worker will switch to the next key earlier, other workers
will still be able to decrypt new tickets, since they will be encrypted
with the next key.
diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c
--- a/src/event/ngx_event_openssl.c
+++ b/src/event/ngx_event_openssl.c
@@ -3773,6 +3773,7 @@ ngx_ssl_session_cache_init(ngx_shm_zone_
cache->ticket_keys[0].expire = 0;
cache->ticket_keys[1].expire = 0;
+ cache->ticket_keys[2].expire = 0;
cache->fail_time = 0;
@@ -4249,7 +4250,7 @@ ngx_ssl_session_ticket_keys(ngx_conf_t *
return NGX_OK;
}
- keys = ngx_array_create(cf->pool, paths ? paths->nelts : 2,
+ keys = ngx_array_create(cf->pool, paths ? paths->nelts : 3,
sizeof(ngx_ssl_ticket_key_t));
if (keys == NULL) {
return NGX_ERROR;
@@ -4284,9 +4285,13 @@ ngx_ssl_session_ticket_keys(ngx_conf_t *
/* placeholder for keys in shared memory */
- key = ngx_array_push_n(keys, 2);
+ key = ngx_array_push_n(keys, 3);
key[0].shared = 1;
+ key[0].expire = 0;
key[1].shared = 1;
+ key[1].expire = 0;
+ key[2].shared = 1;
+ key[2].expire = 0;
return NGX_OK;
}
@@ -4346,6 +4351,7 @@ ngx_ssl_session_ticket_keys(ngx_conf_t *
}
key->shared = 0;
+ key->expire = 1;
if (size == 48) {
key->size = 48;
@@ -4513,7 +4519,7 @@ ngx_ssl_ticket_key_callback(ngx_ssl_conn
/* renew if non-default key */
- if (i != 0) {
+ if (i != 0 && key[i].expire) {
return 2;
}
@@ -4544,9 +4550,21 @@ ngx_ssl_rotate_ticket_keys(SSL_CTX *ssl_
return NGX_OK;
}
+ /*
+ * if we don't need to update expiration of the current key
+ * and the previous key is still needed, don't sync with shared
+ * memory to save some work; in the worst case other worker process
+ * will switch to the next key, but this process will still be able
+ * to decrypt tickets encrypted with it
+ */
+
now = ngx_time();
expire = now + SSL_CTX_get_timeout(ssl_ctx);
+ if (key[0].expire >= expire && key[1].expire >= now) {
+ return NGX_OK;
+ }
+
shm_zone = SSL_CTX_get_ex_data(ssl_ctx, ngx_ssl_session_cache_index);
cache = shm_zone->data;
@@ -4566,28 +4584,38 @@ ngx_ssl_rotate_ticket_keys(SSL_CTX *ssl_
return NGX_ERROR;
}
- key->shared = 1;
- key->expire = expire;
- key->size = 80;
- ngx_memcpy(key->name, buf, 16);
- ngx_memcpy(key->hmac_key, buf + 16, 32);
- ngx_memcpy(key->aes_key, buf + 48, 32);
+ key[0].shared = 1;
+ key[0].expire = expire;
+ key[0].size = 80;
+ ngx_memcpy(key[0].name, buf, 16);
+ ngx_memcpy(key[0].hmac_key, buf + 16, 32);
+ ngx_memcpy(key[0].aes_key, buf + 48, 32);
ngx_explicit_memzero(&buf, 80);
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
"ssl ticket key: \"%*xs\"",
- (size_t) 16, key->name);
+ (size_t) 16, key[0].name);
+
+ /*
+ * copy the current key to the next key, as initialization of
+ * the previous key will replace the current key with the next
+ * key
+ */
+
+ key[2] = key[0];
}
if (key[1].expire < now) {
/*
* if the previous key is no longer needed (or not initialized),
- * replace it with the current key and generate new current key
+ * replace it with the current key, replace the current key with
+ * the next key, and generate new next key
*/
key[1] = key[0];
+ key[0] = key[2];
if (RAND_bytes(buf, 80) != 1) {
ngx_ssl_error(NGX_LOG_ALERT, log, 0, "RAND_bytes() failed");
@@ -4595,18 +4623,18 @@ ngx_ssl_rotate_ticket_keys(SSL_CTX *ssl_
return NGX_ERROR;
}
- key->shared = 1;
- key->expire = expire;
- key->size = 80;
- ngx_memcpy(key->name, buf, 16);
- ngx_memcpy(key->hmac_key, buf + 16, 32);
- ngx_memcpy(key->aes_key, buf + 48, 32);
+ key[2].shared = 1;
+ key[2].expire = 0;
+ key[2].size = 80;
+ ngx_memcpy(key[2].name, buf, 16);
+ ngx_memcpy(key[2].hmac_key, buf + 16, 32);
+ ngx_memcpy(key[2].aes_key, buf + 48, 32);
ngx_explicit_memzero(&buf, 80);
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
"ssl ticket key: \"%*xs\"",
- (size_t) 16, key->name);
+ (size_t) 16, key[2].name);
}
/*
diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h
--- a/src/event/ngx_event_openssl.h
+++ b/src/event/ngx_event_openssl.h
@@ -160,7 +160,7 @@ typedef struct {
ngx_rbtree_t session_rbtree;
ngx_rbtree_node_t sentinel;
ngx_queue_t expire_queue;
- ngx_ssl_ticket_key_t ticket_keys[2];
+ ngx_ssl_ticket_key_t ticket_keys[3];
time_t fail_time;
} ngx_ssl_session_cache_t;
--
Maxim Dounin
http://mdounin.ru/
More information about the nginx-devel
mailing list