From 4ef40988f3e059236f77472368e92222bf397094 Mon Sep 17 00:00:00 2001 From: Artem Boldariev Date: Thu, 21 Apr 2022 15:29:45 +0300 Subject: [PATCH] Add TLS client session cache implementation This commit adds an implementation of a client TLS session cache. TLS client session cache is an object which allows efficient storing and retrieval of previously saved TLS sessions so that they can be resumed. This object is supposed to be a foundation for implementing TLS session resumption - a standard technique to reduce the cost of re-establishing a connection to the remote server endpoint. OpenSSL does server-side TLS session caching transparently by default. However, on the client-side, a TLS session to resume must be manually specified when establishing the TLS connection. The TLS client session cache is precisely the foundation for that. --- lib/isc/include/isc/tls.h | 147 +++++++++++++++++++ lib/isc/tls.c | 294 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 441 insertions(+) diff --git a/lib/isc/include/isc/tls.h b/lib/isc/include/isc/tls.h index e05e2c4228..ab44a23484 100644 --- a/lib/isc/include/isc/tls.h +++ b/lib/isc/include/isc/tls.h @@ -267,6 +267,153 @@ isc_tls_cert_store_free(isc_tls_cert_store_t **pstore); *\li 'pstore' is a valid pointer to a pointer containing a non-'NULL' value. */ +typedef struct isc_tlsctx_client_session_cache isc_tlsctx_client_session_cache_t; +/*%< + * TLS client session cache is an object which allows efficient + * storing and retrieval of previously saved TLS sessions so that they + * can be resumed. This object is supposed to be a foundation for + * implementing TLS session resumption - a standard technique to + * reduce the cost of re-establishing a connection to the remote + * server endpoint. + * + * OpenSSL does server-side TLS session caching transparently by + * default. However, on the client-side, a TLS session to resume must + * be manually specified when establishing the TLS connection. The TLS + * client session cache is precisely the foundation for that. + * + * The cache has been designed to have the following characteristics: + * + * - Fixed maximal number of entries to not keep too many obsolete + * sessions within the cache; + * + * - The cache is indexed by character string keys. Each string is a + * key representing a remote endpoint (unique remote endpoint name, + * e.g. combination of the remote IP address and port); + * + * - Least Recently Used (LRU) cache replacement policy while allowing + * easy removal of obsolete entries; + * + * - Ability to store multiple TLS sessions associated with the given + * key (remote endpoint name). This characteristic is important if + * multiple connections to the same remote server can be established; + * + * - Ability to efficiently retrieve the most recent TLS sessions + * associated with the key (remote endpoint name). + * + * Because of these characteristics, the cache will end up having the + * necessary amount of resumable TLS session parameters to the most + * used remote endpoints ("hot entries") after a short period of + * initial use ("warmup"). + * + * Attempting to resume TLS sessions is an optimisation, which is not + * guaranteed to succeed because it requires the same session to be + * present in the server session caches. If it is not the case, the + * usual handshake procedure takes place. However, when session + * resumption is successful, it reduces the amount of the + * computational resources required as well as the amount of data to + * be transmitted when (re)establishing the connection. Also, it + * reduces round trip time (by reducing the number of packets to + * transmit). + * + * This optimisation is important in the context of DNS because the + * amount of data within the full handshake messages might be + * comparable to or surpass the size of a typical DNS message. + */ + +isc_tlsctx_client_session_cache_t * +isc_tlsctx_client_session_cache_new(isc_mem_t *mctx, isc_tlsctx_t *ctx, + const size_t max_entries); +/*%< + * Create a new TLS client session cache object. + * + * Requires: + *\li 'mctx' is a valid memory context object; + *\li 'ctx' is a valid TLS context object; + *\li 'max_entries' is a positive number; + */ + +void +isc_tlsctx_client_session_cache_attach( + isc_tlsctx_client_session_cache_t *source, + isc_tlsctx_client_session_cache_t **targetp); +/*%< + * Create a reference to the TLS client session cache object. + * + * Requires: + *\li 'source' is a valid TLS client session cache object; + *\li 'targetp' is a valid pointer to a pointer which must equal NULL. + */ + +void +isc_tlsctx_client_session_cache_detach( + isc_tlsctx_client_session_cache_t **cachep); +/*%< + * Remove a reference to the TLS client session cache object. + * + * Requires: + *\li 'cachep' is a pointer to a pointer to a valid TLS client session cache + *object. + */ + +void +isc_tlsctx_client_session_cache_keep(isc_tlsctx_client_session_cache_t *cache, + char *remote_peer_name, isc_tls_t *tls); +/*%< + * Add a resumable TLS client session within 'tls' to the TLS client + * session cache object 'cache' and associate it with + * 'remote_server_name' string. Also, the oldest entry from the cache + * might get removed if the cache is full. + * + * Requires: + *\li 'cache' is a pointer to a valid TLS client session cache object; + *\li 'remote_peer_name' is a pointer to a non empty character string. + *\li 'tls' is a valid, non-'NULL' pointer to a TLS connection state object. + */ + +void +isc_tlsctx_client_session_cache_keep_sockaddr( + isc_tlsctx_client_session_cache_t *cache, isc_sockaddr_t *remote_peer, + isc_tls_t *tls); +/*%< + * The same as 'isc_tlsctx_client_session_cache_keep()', but using a + * 'isc_sockaddr_t' as a key, instead of a character string. + * + * Requires: + *\li 'remote_peer' is a valid, non-'NULL' pointer to an 'isc_sockaddr_t' + *object. + */ + +void +isc_tlsctx_client_session_cache_reuse(isc_tlsctx_client_session_cache_t *cache, + char *remote_server_name, isc_tls_t *tls); +/*% + * Try to restore a previously stored TLS session denoted by a remote + * server name as a key ('remote_server_name') into the given TLS + * connection state object ('tls'). The successfully restored session + * gets removed from the cache. + * + * Requires: + *\li 'cache' is a pointer to a valid TLS client session cache object; + *\li 'remote_peer_name' is a pointer to a non empty character string; + *\li 'tls' is a valid, non-'NULL', pointer to a TLS connection state object. + */ + +void +isc_tlsctx_client_session_cache_reuse_sockaddr( + isc_tlsctx_client_session_cache_t *cache, isc_sockaddr_t *remote_peer, + isc_tls_t *tls); +/*%< + * The same as 'isc_tlsctx_client_session_cache_reuse()', but uses a + * 'isc_sockaddr_t' as a key, instead of a character string. + * + * Requires: + *\li 'remote_peer' is a valid, non-'NULL' pointer to an 'isc_sockaddr_t' + *object. + */ + +const isc_tlsctx_t * +isc_tlsctx_client_session_cache_getctx(isc_tlsctx_client_session_cache_t *cache); + typedef struct isc_tlsctx_cache isc_tlsctx_cache_t; /*%< * The TLS context cache is an object which allows retrieving a diff --git a/lib/isc/tls.c b/lib/isc/tls.c index 3eb0af155f..282ec45f7e 100644 --- a/lib/isc/tls.c +++ b/lib/isc/tls.c @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -1074,6 +1075,10 @@ isc_tls_cert_store_free(isc_tls_cert_store_t **pstore) { #define TLSCTX_CACHE_MAGIC ISC_MAGIC('T', 'l', 'S', 'c') #define VALID_TLSCTX_CACHE(t) ISC_MAGIC_VALID(t, TLSCTX_CACHE_MAGIC) +#define TLSCTX_CLIENT_SESSION_CACHE_MAGIC ISC_MAGIC('T', 'l', 'C', 'c') +#define VALID_TLSCTX_CLIENT_SESSION_CACHE(t) \ + ISC_MAGIC_VALID(t, TLSCTX_CLIENT_SESSION_CACHE_MAGIC) + typedef struct isc_tlsctx_cache_entry { /* * We need a TLS context entry for each transport on both IPv4 and @@ -1299,3 +1304,292 @@ isc_tlsctx_cache_find(isc_tlsctx_cache_t *cache, const char *name, return (result); } + +typedef struct client_session_cache_entry client_session_cache_entry_t; + +typedef struct client_session_cache_bucket { + char *bucket_key; + size_t bucket_key_len; + /* Cache entries within the bucket (from the oldest to the newest). */ + ISC_LIST(client_session_cache_entry_t) entries; +} client_session_cache_bucket_t; + +struct client_session_cache_entry { + SSL_SESSION *session; + client_session_cache_bucket_t *bucket; /* "Parent" bucket pointer. */ + ISC_LINK(client_session_cache_entry_t) bucket_link; + ISC_LINK(client_session_cache_entry_t) cache_link; +}; + +struct isc_tlsctx_client_session_cache { + uint32_t magic; + isc_refcount_t references; + isc_mem_t *mctx; + + /* + * We need to keep a reference to the related TLS context in order + * to ensure that it remains valid while the TLS client sessions + * cache object is valid, as every TLS session object + * (SSL_SESSION) is "tied" to a particular context. + */ + isc_tlsctx_t *ctx; + + /* + * The idea is to have one bucket per remote server. Each bucket, + * can maintain multiple TLS sessions to that server, as BIND + * might want to establish multiple TLS connections to the remote + * server at once. + */ + isc_ht_t *buckets; + + /* + * The list of all current entries within the cache maintained in + * LRU-manner, so that the oldest entry might be efficiently + * removed. + */ + ISC_LIST(client_session_cache_entry_t) lru_entries; + /* Number of the entries within the cache. */ + size_t nentries; + /* Maximum number of the entries within the cache. */ + size_t max_entries; + + isc_mutex_t lock; +}; + +isc_tlsctx_client_session_cache_t * +isc_tlsctx_client_session_cache_new(isc_mem_t *mctx, isc_tlsctx_t *ctx, + const size_t max_entries) { + isc_tlsctx_client_session_cache_t *nc; + + REQUIRE(ctx != NULL); + REQUIRE(max_entries > 0); + + nc = isc_mem_get(mctx, sizeof(*nc)); + + *nc = (isc_tlsctx_client_session_cache_t){ .max_entries = max_entries }; + isc_refcount_init(&nc->references, 1); + isc_mem_attach(mctx, &nc->mctx); + isc_tlsctx_attach(ctx, &nc->ctx); + + isc_ht_init(&nc->buckets, mctx, 5, ISC_HT_CASE_SENSITIVE); + ISC_LIST_INIT(nc->lru_entries); + isc_mutex_init(&nc->lock); + + nc->magic = TLSCTX_CLIENT_SESSION_CACHE_MAGIC; + + return (nc); +} + +void +isc_tlsctx_client_session_cache_attach( + isc_tlsctx_client_session_cache_t *source, + isc_tlsctx_client_session_cache_t **targetp) { + REQUIRE(VALID_TLSCTX_CLIENT_SESSION_CACHE(source)); + REQUIRE(targetp != NULL && *targetp == NULL); + + isc_refcount_increment(&source->references); + + *targetp = source; +} + +static void +client_cache_entry_delete(isc_tlsctx_client_session_cache_t *restrict cache, + client_session_cache_entry_t *restrict entry) { + client_session_cache_bucket_t *restrict bucket = entry->bucket; + + /* Unlink and free the cache entry */ + ISC_LIST_UNLINK(bucket->entries, entry, bucket_link); + ISC_LIST_UNLINK(cache->lru_entries, entry, cache_link); + cache->nentries--; + (void)SSL_SESSION_free(entry->session); + isc_mem_put(cache->mctx, entry, sizeof(*entry)); + + /* The bucket is empty - let's remove it */ + if (ISC_LIST_EMPTY(bucket->entries)) { + RUNTIME_CHECK(isc_ht_delete(cache->buckets, + (const uint8_t *)bucket->bucket_key, + bucket->bucket_key_len) == + ISC_R_SUCCESS); + + isc_mem_free(cache->mctx, bucket->bucket_key); + isc_mem_put(cache->mctx, bucket, sizeof(*bucket)); + } +} + +void +isc_tlsctx_client_session_cache_detach( + isc_tlsctx_client_session_cache_t **cachep) { + isc_tlsctx_client_session_cache_t *cache = NULL; + client_session_cache_entry_t *entry = NULL, *next = NULL; + + REQUIRE(cachep != NULL); + + cache = *cachep; + *cachep = NULL; + + REQUIRE(VALID_TLSCTX_CLIENT_SESSION_CACHE(cache)); + + if (isc_refcount_decrement(&cache->references) != 1) { + return; + } + + cache->magic = 0; + + isc_refcount_destroy(&cache->references); + + entry = ISC_LIST_HEAD(cache->lru_entries); + while (entry != NULL) { + next = ISC_LIST_NEXT(entry, cache_link); + client_cache_entry_delete(cache, entry); + entry = next; + } + + RUNTIME_CHECK(isc_ht_count(cache->buckets) == 0); + isc_ht_destroy(&cache->buckets); + + isc_mutex_destroy(&cache->lock); + isc_tlsctx_free(&cache->ctx); + isc_mem_putanddetach(&cache->mctx, cache, sizeof(*cache)); +} + +void +isc_tlsctx_client_session_cache_keep(isc_tlsctx_client_session_cache_t *cache, + char *remote_peer_name, isc_tls_t *tls) { + size_t name_len; + isc_result_t result; + SSL_SESSION *sess; + client_session_cache_bucket_t *restrict bucket = NULL; + client_session_cache_entry_t *restrict entry = NULL; + + REQUIRE(VALID_TLSCTX_CLIENT_SESSION_CACHE(cache)); + REQUIRE(remote_peer_name != NULL && *remote_peer_name != '\0'); + REQUIRE(tls != NULL); + + sess = SSL_get1_session(tls); + if (sess == NULL) { + return; + } else if (SSL_SESSION_is_resumable(sess) == 0) { + SSL_SESSION_free(sess); + return; + } + + isc_mutex_lock(&cache->lock); + + name_len = strlen(remote_peer_name); + result = isc_ht_find(cache->buckets, (const uint8_t *)remote_peer_name, + name_len, (void **)&bucket); + + if (result != ISC_R_SUCCESS) { + /* Let's create a new bucket */ + INSIST(bucket == NULL); + bucket = isc_mem_get(cache->mctx, sizeof(*bucket)); + *bucket = (client_session_cache_bucket_t){ + .bucket_key = isc_mem_strdup(cache->mctx, + remote_peer_name), + .bucket_key_len = name_len + }; + ISC_LIST_INIT(bucket->entries); + RUNTIME_CHECK(isc_ht_add(cache->buckets, + (const uint8_t *)remote_peer_name, + name_len, + (void *)bucket) == ISC_R_SUCCESS); + } + + /* Let's add a new cache entry to the new/found bucket */ + entry = isc_mem_get(cache->mctx, sizeof(*entry)); + *entry = (client_session_cache_entry_t){ .session = sess, + .bucket = bucket }; + ISC_LINK_INIT(entry, bucket_link); + ISC_LINK_INIT(entry, cache_link); + + ISC_LIST_APPEND(bucket->entries, entry, bucket_link); + + ISC_LIST_APPEND(cache->lru_entries, entry, cache_link); + cache->nentries++; + + if (cache->nentries > cache->max_entries) { + /* + * Cache overrun. We need to remove the oldest entry from the + * cache + */ + client_session_cache_entry_t *restrict oldest; + INSIST((cache->nentries - 1) == cache->max_entries); + + oldest = ISC_LIST_HEAD(cache->lru_entries); + client_cache_entry_delete(cache, oldest); + } + + isc_mutex_unlock(&cache->lock); +} + +void +isc_tlsctx_client_session_cache_reuse(isc_tlsctx_client_session_cache_t *cache, + char *remote_peer_name, isc_tls_t *tls) { + client_session_cache_bucket_t *restrict bucket = NULL; + client_session_cache_entry_t *restrict entry; + size_t name_len; + isc_result_t result; + + REQUIRE(VALID_TLSCTX_CLIENT_SESSION_CACHE(cache)); + REQUIRE(remote_peer_name != NULL && *remote_peer_name != '\0'); + REQUIRE(tls != NULL); + + isc_mutex_lock(&cache->lock); + + /* Let's find the bucket */ + name_len = strlen(remote_peer_name); + result = isc_ht_find(cache->buckets, (const uint8_t *)remote_peer_name, + name_len, (void **)&bucket); + + if (result != ISC_R_SUCCESS) { + goto exit; + } + + INSIST(bucket != NULL); + + /* + * If the bucket has been found, let's use the newest session from + * the bucket, as it has the highest chance to be successfully + * resumed. + */ + INSIST(!ISC_LIST_EMPTY(bucket->entries)); + entry = ISC_LIST_TAIL(bucket->entries); + RUNTIME_CHECK(SSL_set_session(tls, entry->session) == 1); + client_cache_entry_delete(cache, entry); + +exit: + isc_mutex_unlock(&cache->lock); +} + +void +isc_tlsctx_client_session_cache_keep_sockaddr( + isc_tlsctx_client_session_cache_t *cache, isc_sockaddr_t *remote_peer, + isc_tls_t *tls) { + char peername[ISC_SOCKADDR_FORMATSIZE] = { 0 }; + + REQUIRE(remote_peer != NULL); + + isc_sockaddr_format(remote_peer, peername, sizeof(peername)); + + isc_tlsctx_client_session_cache_keep(cache, peername, tls); +} + +void +isc_tlsctx_client_session_cache_reuse_sockaddr( + isc_tlsctx_client_session_cache_t *cache, isc_sockaddr_t *remote_peer, + isc_tls_t *tls) { + char peername[ISC_SOCKADDR_FORMATSIZE] = { 0 }; + + REQUIRE(remote_peer != NULL); + + isc_sockaddr_format(remote_peer, peername, sizeof(peername)); + + isc_tlsctx_client_session_cache_reuse(cache, peername, tls); +} + +const isc_tlsctx_t * +isc_tlsctx_client_session_cache_getctx( + isc_tlsctx_client_session_cache_t *cache) { + REQUIRE(VALID_TLSCTX_CLIENT_SESSION_CACHE(cache)); + return (cache->ctx); +}