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); +}