diff options
Diffstat (limited to '')
-rw-r--r-- | lib/isc/tls.c | 1678 |
1 files changed, 1678 insertions, 0 deletions
diff --git a/lib/isc/tls.c b/lib/isc/tls.c new file mode 100644 index 0000000..e2cd63e --- /dev/null +++ b/lib/isc/tls.c @@ -0,0 +1,1678 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +#include <inttypes.h> +#include <netinet/in.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#if HAVE_LIBNGHTTP2 +#include <nghttp2/nghttp2.h> +#endif /* HAVE_LIBNGHTTP2 */ +#include <arpa/inet.h> + +#include <openssl/bn.h> +#include <openssl/conf.h> +#include <openssl/crypto.h> +#include <openssl/dh.h> +#include <openssl/err.h> +#include <openssl/evp.h> +#include <openssl/opensslv.h> +#include <openssl/rand.h> +#include <openssl/rsa.h> +#include <openssl/x509_vfy.h> +#include <openssl/x509v3.h> + +#include <isc/atomic.h> +#include <isc/ht.h> +#include <isc/log.h> +#include <isc/magic.h> +#include <isc/mutex.h> +#include <isc/mutexblock.h> +#include <isc/once.h> +#include <isc/random.h> +#include <isc/refcount.h> +#include <isc/rwlock.h> +#include <isc/sockaddr.h> +#include <isc/thread.h> +#include <isc/tls.h> +#include <isc/util.h> + +#include "openssl_shim.h" +#include "tls_p.h" + +#define COMMON_SSL_OPTIONS \ + (SSL_OP_NO_COMPRESSION | SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION) + +static isc_once_t init_once = ISC_ONCE_INIT; +static isc_once_t shut_once = ISC_ONCE_INIT; +static atomic_bool init_done = false; +static atomic_bool shut_done = false; + +#if OPENSSL_VERSION_NUMBER < 0x10100000L +static isc_mutex_t *locks = NULL; +static int nlocks; + +static void +isc__tls_lock_callback(int mode, int type, const char *file, int line) { + UNUSED(file); + UNUSED(line); + if ((mode & CRYPTO_LOCK) != 0) { + LOCK(&locks[type]); + } else { + UNLOCK(&locks[type]); + } +} + +static void +isc__tls_set_thread_id(CRYPTO_THREADID *id) { + CRYPTO_THREADID_set_numeric(id, (unsigned long)isc_thread_self()); +} +#endif + +static void +tls_initialize(void) { + REQUIRE(!atomic_load(&init_done)); + +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + RUNTIME_CHECK(OPENSSL_init_ssl(OPENSSL_INIT_ENGINE_ALL_BUILTIN | + OPENSSL_INIT_LOAD_CONFIG, + NULL) == 1); +#else + nlocks = CRYPTO_num_locks(); + /* + * We can't use isc_mem API here, because it's called too + * early and when the isc_mem_debugging flags are changed + * later. + * + * Actually, since this is a single allocation at library load + * and deallocation at library unload, using the standard + * allocator without the tracking is fine for this purpose. + */ + locks = calloc(nlocks, sizeof(locks[0])); + isc_mutexblock_init(locks, nlocks); + CRYPTO_set_locking_callback(isc__tls_lock_callback); + CRYPTO_THREADID_set_callback(isc__tls_set_thread_id); + + CRYPTO_malloc_init(); + ERR_load_crypto_strings(); + SSL_load_error_strings(); + SSL_library_init(); + +#if !defined(OPENSSL_NO_ENGINE) && OPENSSL_API_LEVEL < 30000 + ENGINE_load_builtin_engines(); +#endif + OpenSSL_add_all_algorithms(); + OPENSSL_load_builtin_modules(); + + CONF_modules_load_file(NULL, NULL, + CONF_MFLAGS_DEFAULT_SECTION | + CONF_MFLAGS_IGNORE_MISSING_FILE); +#endif + + /* Protect ourselves against unseeded PRNG */ + if (RAND_status() != 1) { + FATAL_ERROR("OpenSSL pseudorandom number generator " + "cannot be initialized (see the `PRNG not " + "seeded' message in the OpenSSL FAQ)"); + } + + atomic_compare_exchange_enforced(&init_done, &(bool){ false }, true); +} + +void +isc__tls_initialize(void) { + isc_result_t result = isc_once_do(&init_once, tls_initialize); + REQUIRE(result == ISC_R_SUCCESS); + REQUIRE(atomic_load(&init_done)); +} + +static void +tls_shutdown(void) { + REQUIRE(atomic_load(&init_done)); + REQUIRE(!atomic_load(&shut_done)); + +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + OPENSSL_cleanup(); +#else + CONF_modules_unload(1); + OBJ_cleanup(); + EVP_cleanup(); +#if !defined(OPENSSL_NO_ENGINE) && OPENSSL_API_LEVEL < 30000 + ENGINE_cleanup(); +#endif + CRYPTO_cleanup_all_ex_data(); + ERR_remove_thread_state(NULL); + RAND_cleanup(); + ERR_free_strings(); + + CRYPTO_set_locking_callback(NULL); + + if (locks != NULL) { + isc_mutexblock_destroy(locks, nlocks); + free(locks); + locks = NULL; + } +#endif + + atomic_compare_exchange_enforced(&shut_done, &(bool){ false }, true); +} + +void +isc__tls_shutdown(void) { + isc_result_t result = isc_once_do(&shut_once, tls_shutdown); + REQUIRE(result == ISC_R_SUCCESS); + REQUIRE(atomic_load(&shut_done)); +} + +void +isc_tlsctx_free(isc_tlsctx_t **ctxp) { + SSL_CTX *ctx = NULL; + REQUIRE(ctxp != NULL && *ctxp != NULL); + + ctx = *ctxp; + *ctxp = NULL; + + SSL_CTX_free(ctx); +} + +void +isc_tlsctx_attach(isc_tlsctx_t *src, isc_tlsctx_t **ptarget) { + REQUIRE(src != NULL); + REQUIRE(ptarget != NULL && *ptarget == NULL); + + RUNTIME_CHECK(SSL_CTX_up_ref(src) == 1); + + *ptarget = src; +} + +#if HAVE_SSL_CTX_SET_KEYLOG_CALLBACK +/* + * Callback invoked by the SSL library whenever a new TLS pre-master secret + * needs to be logged. + */ +static void +sslkeylogfile_append(const SSL *ssl, const char *line) { + UNUSED(ssl); + + isc_log_write(isc_lctx, ISC_LOGCATEGORY_SSLKEYLOG, ISC_LOGMODULE_NETMGR, + ISC_LOG_INFO, "%s", line); +} + +/* + * Enable TLS pre-master secret logging if the SSLKEYLOGFILE environment + * variable is set. This needs to be done on a per-context basis as that is + * how SSL_CTX_set_keylog_callback() works. + */ +static void +sslkeylogfile_init(isc_tlsctx_t *ctx) { + if (getenv("SSLKEYLOGFILE") != NULL) { + SSL_CTX_set_keylog_callback(ctx, sslkeylogfile_append); + } +} +#else /* HAVE_SSL_CTX_SET_KEYLOG_CALLBACK */ +#define sslkeylogfile_init(ctx) +#endif /* HAVE_SSL_CTX_SET_KEYLOG_CALLBACK */ + +isc_result_t +isc_tlsctx_createclient(isc_tlsctx_t **ctxp) { + unsigned long err; + char errbuf[256]; + SSL_CTX *ctx = NULL; + const SSL_METHOD *method = NULL; + + REQUIRE(ctxp != NULL && *ctxp == NULL); + + method = TLS_client_method(); + if (method == NULL) { + goto ssl_error; + } + ctx = SSL_CTX_new(method); + if (ctx == NULL) { + goto ssl_error; + } + + SSL_CTX_set_options(ctx, COMMON_SSL_OPTIONS); + +#if HAVE_SSL_CTX_SET_MIN_PROTO_VERSION + SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); +#else + SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | + SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1); +#endif + + sslkeylogfile_init(ctx); + + *ctxp = ctx; + + return (ISC_R_SUCCESS); + +ssl_error: + err = ERR_get_error(); + ERR_error_string_n(err, errbuf, sizeof(errbuf)); + isc_log_write(isc_lctx, ISC_LOGCATEGORY_GENERAL, ISC_LOGMODULE_NETMGR, + ISC_LOG_ERROR, "Error initializing TLS context: %s", + errbuf); + + return (ISC_R_TLSERROR); +} + +isc_result_t +isc_tlsctx_load_certificate(isc_tlsctx_t *ctx, const char *keyfile, + const char *certfile) { + int rv; + REQUIRE(ctx != NULL); + REQUIRE(keyfile != NULL); + REQUIRE(certfile != NULL); + + rv = SSL_CTX_use_certificate_chain_file(ctx, certfile); + if (rv != 1) { + return (ISC_R_TLSERROR); + } + rv = SSL_CTX_use_PrivateKey_file(ctx, keyfile, SSL_FILETYPE_PEM); + if (rv != 1) { + return (ISC_R_TLSERROR); + } + + return (ISC_R_SUCCESS); +} + +isc_result_t +isc_tlsctx_createserver(const char *keyfile, const char *certfile, + isc_tlsctx_t **ctxp) { + int rv; + unsigned long err; + bool ephemeral = (keyfile == NULL && certfile == NULL); + X509 *cert = NULL; + EVP_PKEY *pkey = NULL; + SSL_CTX *ctx = NULL; +#if OPENSSL_VERSION_NUMBER < 0x30000000L + EC_KEY *eckey = NULL; +#else + EVP_PKEY_CTX *pkey_ctx = NULL; + EVP_PKEY *params_pkey = NULL; +#endif /* OPENSSL_VERSION_NUMBER < 0x30000000L */ + char errbuf[256]; + const SSL_METHOD *method = NULL; + + REQUIRE(ctxp != NULL && *ctxp == NULL); + REQUIRE((keyfile == NULL) == (certfile == NULL)); + + method = TLS_server_method(); + if (method == NULL) { + goto ssl_error; + } + ctx = SSL_CTX_new(method); + if (ctx == NULL) { + goto ssl_error; + } + RUNTIME_CHECK(ctx != NULL); + + SSL_CTX_set_options(ctx, COMMON_SSL_OPTIONS); + +#if HAVE_SSL_CTX_SET_MIN_PROTO_VERSION + SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); +#else + SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | + SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1); +#endif + + if (ephemeral) { + const int group_nid = NID_X9_62_prime256v1; + +#if OPENSSL_VERSION_NUMBER < 0x30000000L + eckey = EC_KEY_new_by_curve_name(group_nid); + if (eckey == NULL) { + goto ssl_error; + } + + /* Generate the key. */ + rv = EC_KEY_generate_key(eckey); + if (rv != 1) { + goto ssl_error; + } + pkey = EVP_PKEY_new(); + if (pkey == NULL) { + goto ssl_error; + } + rv = EVP_PKEY_set1_EC_KEY(pkey, eckey); + if (rv != 1) { + goto ssl_error; + } + + /* Use a named curve and uncompressed point conversion form. */ +#if HAVE_EVP_PKEY_GET0_EC_KEY + EC_KEY_set_asn1_flag(EVP_PKEY_get0_EC_KEY(pkey), + OPENSSL_EC_NAMED_CURVE); + EC_KEY_set_conv_form(EVP_PKEY_get0_EC_KEY(pkey), + POINT_CONVERSION_UNCOMPRESSED); +#else + EC_KEY_set_asn1_flag(pkey->pkey.ec, OPENSSL_EC_NAMED_CURVE); + EC_KEY_set_conv_form(pkey->pkey.ec, + POINT_CONVERSION_UNCOMPRESSED); +#endif /* HAVE_EVP_PKEY_GET0_EC_KEY */ + +#if defined(SSL_CTX_set_ecdh_auto) + /* + * Using this macro is required for older versions of OpenSSL to + * automatically enable ECDH support. + * + * On later versions this function is no longer needed and is + * deprecated. + */ + (void)SSL_CTX_set_ecdh_auto(ctx, 1); +#endif /* defined(SSL_CTX_set_ecdh_auto) */ + + /* Cleanup */ + EC_KEY_free(eckey); + eckey = NULL; +#else + /* Generate the key's parameters. */ + pkey_ctx = EVP_PKEY_CTX_new_from_name(NULL, "EC", NULL); + if (pkey_ctx == NULL) { + goto ssl_error; + } + rv = EVP_PKEY_paramgen_init(pkey_ctx); + if (rv != 1) { + goto ssl_error; + } + rv = EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pkey_ctx, + group_nid); + if (rv != 1) { + goto ssl_error; + } + rv = EVP_PKEY_paramgen(pkey_ctx, ¶ms_pkey); + if (rv != 1 || params_pkey == NULL) { + goto ssl_error; + } + EVP_PKEY_CTX_free(pkey_ctx); + + /* Generate the key. */ + pkey_ctx = EVP_PKEY_CTX_new(params_pkey, NULL); + if (pkey_ctx == NULL) { + goto ssl_error; + } + rv = EVP_PKEY_keygen_init(pkey_ctx); + if (rv != 1) { + goto ssl_error; + } + rv = EVP_PKEY_keygen(pkey_ctx, &pkey); + if (rv != 1 || pkey == NULL) { + goto ssl_error; + } + + /* Cleanup */ + EVP_PKEY_free(params_pkey); + params_pkey = NULL; + EVP_PKEY_CTX_free(pkey_ctx); + pkey_ctx = NULL; +#endif /* OPENSSL_VERSION_NUMBER < 0x30000000L */ + + cert = X509_new(); + if (cert == NULL) { + goto ssl_error; + } + + ASN1_INTEGER_set(X509_get_serialNumber(cert), + (long)isc_random32()); + + /* + * Set the "not before" property 5 minutes into the past to + * accommodate with some possible clock skew across systems. + */ +#if OPENSSL_VERSION_NUMBER < 0x10101000L + X509_gmtime_adj(X509_get_notBefore(cert), -300); +#else + X509_gmtime_adj(X509_getm_notBefore(cert), -300); +#endif + + /* + * We set the vailidy for 10 years. + */ +#if OPENSSL_VERSION_NUMBER < 0x10101000L + X509_gmtime_adj(X509_get_notAfter(cert), 3650 * 24 * 3600); +#else + X509_gmtime_adj(X509_getm_notAfter(cert), 3650 * 24 * 3600); +#endif + + X509_set_pubkey(cert, pkey); + + X509_NAME *name = X509_get_subject_name(cert); + + X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, + (const unsigned char *)"AQ", -1, -1, + 0); + X509_NAME_add_entry_by_txt( + name, "O", MBSTRING_ASC, + (const unsigned char *)"BIND9 ephemeral " + "certificate", + -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, + (const unsigned char *)"bind9.local", + -1, -1, 0); + + X509_set_issuer_name(cert, name); + X509_sign(cert, pkey, EVP_sha256()); + rv = SSL_CTX_use_certificate(ctx, cert); + if (rv != 1) { + goto ssl_error; + } + rv = SSL_CTX_use_PrivateKey(ctx, pkey); + if (rv != 1) { + goto ssl_error; + } + + X509_free(cert); + EVP_PKEY_free(pkey); + } else { + isc_result_t result; + result = isc_tlsctx_load_certificate(ctx, keyfile, certfile); + if (result != ISC_R_SUCCESS) { + goto ssl_error; + } + } + + sslkeylogfile_init(ctx); + + *ctxp = ctx; + return (ISC_R_SUCCESS); + +ssl_error: + err = ERR_get_error(); + ERR_error_string_n(err, errbuf, sizeof(errbuf)); + isc_log_write(isc_lctx, ISC_LOGCATEGORY_GENERAL, ISC_LOGMODULE_NETMGR, + ISC_LOG_ERROR, "Error initializing TLS context: %s", + errbuf); + + if (ctx != NULL) { + SSL_CTX_free(ctx); + } + if (cert != NULL) { + X509_free(cert); + } + if (pkey != NULL) { + EVP_PKEY_free(pkey); + } +#if OPENSSL_VERSION_NUMBER < 0x30000000L + if (eckey != NULL) { + EC_KEY_free(eckey); + } +#else + if (params_pkey != NULL) { + EVP_PKEY_free(params_pkey); + } + if (pkey_ctx != NULL) { + EVP_PKEY_CTX_free(pkey_ctx); + } +#endif /* OPENSSL_VERSION_NUMBER < 0x30000000L */ + + return (ISC_R_TLSERROR); +} + +static long +get_tls_version_disable_bit(const isc_tls_protocol_version_t tls_ver) { + long bit = 0; + + switch (tls_ver) { + case ISC_TLS_PROTO_VER_1_2: +#ifdef SSL_OP_NO_TLSv1_2 + bit = SSL_OP_NO_TLSv1_2; +#else + bit = 0; +#endif + break; + case ISC_TLS_PROTO_VER_1_3: +#ifdef SSL_OP_NO_TLSv1_3 + bit = SSL_OP_NO_TLSv1_3; +#else + bit = 0; +#endif + break; + default: + UNREACHABLE(); + break; + }; + + return (bit); +} + +bool +isc_tls_protocol_supported(const isc_tls_protocol_version_t tls_ver) { + return (get_tls_version_disable_bit(tls_ver) != 0); +} + +isc_tls_protocol_version_t +isc_tls_protocol_name_to_version(const char *name) { + REQUIRE(name != NULL); + + if (strcasecmp(name, "TLSv1.2") == 0) { + return (ISC_TLS_PROTO_VER_1_2); + } else if (strcasecmp(name, "TLSv1.3") == 0) { + return (ISC_TLS_PROTO_VER_1_3); + } + + return (ISC_TLS_PROTO_VER_UNDEFINED); +} + +void +isc_tlsctx_set_protocols(isc_tlsctx_t *ctx, const uint32_t tls_versions) { + REQUIRE(ctx != NULL); + REQUIRE(tls_versions != 0); + long set_options = 0; + long clear_options = 0; + uint32_t versions = tls_versions; + + /* + * The code below might be initially hard to follow because of the + * double negation that OpenSSL enforces. + * + * Taking into account that OpenSSL provides bits to *disable* + * specific protocol versions, like SSL_OP_NO_TLSv1_2, + * SSL_OP_NO_TLSv1_3, etc., the code has the following logic: + * + * If a protocol version is not specified in the bitmask, get the + * bit that disables it and add it to the set of TLS options to + * set ('set_options'). Otherwise, if a protocol version is set, + * add the bit to the set of options to clear ('clear_options'). + */ + + /* TLS protocol versions are defined as powers of two. */ + for (uint32_t tls_ver = ISC_TLS_PROTO_VER_1_2; + tls_ver < ISC_TLS_PROTO_VER_UNDEFINED; tls_ver <<= 1) + { + if ((tls_versions & tls_ver) == 0) { + set_options |= get_tls_version_disable_bit(tls_ver); + } else { + /* + * Only supported versions should ever be passed to the + * function SSL_CTX_clear_options. For example, in order + * to enable TLS v1.2, we have to clear + * SSL_OP_NO_TLSv1_2. Insist that the configuration file + * was verified properly, so we are not trying to enable + * an unsupported TLS version. + */ + INSIST(isc_tls_protocol_supported(tls_ver)); + clear_options |= get_tls_version_disable_bit(tls_ver); + } + versions &= ~(tls_ver); + } + + /* All versions should be processed at this point, thus the value + * must equal zero. If it is not, then some garbage has been + * passed to the function; this situation is worth + * investigation. */ + INSIST(versions == 0); + + (void)SSL_CTX_set_options(ctx, set_options); + (void)SSL_CTX_clear_options(ctx, clear_options); +} + +bool +isc_tlsctx_load_dhparams(isc_tlsctx_t *ctx, const char *dhparams_file) { + REQUIRE(ctx != NULL); + REQUIRE(dhparams_file != NULL); + REQUIRE(*dhparams_file != '\0'); + +#if OPENSSL_VERSION_NUMBER < 0x30000000L + /* OpenSSL < 3.0 */ + DH *dh = NULL; + FILE *paramfile; + + paramfile = fopen(dhparams_file, "r"); + + if (paramfile) { + int check = 0; + dh = PEM_read_DHparams(paramfile, NULL, NULL, NULL); + fclose(paramfile); + + if (dh == NULL) { + return (false); + } else if (DH_check(dh, &check) != 1 || check != 0) { + DH_free(dh); + return (false); + } + } else { + return (false); + } + + if (SSL_CTX_set_tmp_dh(ctx, dh) != 1) { + DH_free(dh); + return (false); + } + + DH_free(dh); +#else + /* OpenSSL >= 3.0: low level DH APIs are deprecated in OpenSSL 3.0 */ + EVP_PKEY *dh = NULL; + BIO *bio = NULL; + + bio = BIO_new_file(dhparams_file, "r"); + if (bio == NULL) { + return (false); + } + + dh = PEM_read_bio_Parameters(bio, NULL); + if (dh == NULL) { + BIO_free(bio); + return (false); + } + + if (SSL_CTX_set0_tmp_dh_pkey(ctx, dh) != 1) { + BIO_free(bio); + EVP_PKEY_free(dh); + return (false); + } + + /* No need to call EVP_PKEY_free(dh) as the "dh" is owned by the + * SSL context at this point. */ + + BIO_free(bio); +#endif /* OPENSSL_VERSION_NUMBER < 0x30000000L */ + + return (true); +} + +bool +isc_tls_cipherlist_valid(const char *cipherlist) { + isc_tlsctx_t *tmp_ctx = NULL; + const SSL_METHOD *method = NULL; + bool result; + REQUIRE(cipherlist != NULL); + + if (*cipherlist == '\0') { + return (false); + } + + method = TLS_server_method(); + if (method == NULL) { + return (false); + } + tmp_ctx = SSL_CTX_new(method); + if (tmp_ctx == NULL) { + return (false); + } + + result = SSL_CTX_set_cipher_list(tmp_ctx, cipherlist) == 1; + + isc_tlsctx_free(&tmp_ctx); + + return (result); +} + +void +isc_tlsctx_set_cipherlist(isc_tlsctx_t *ctx, const char *cipherlist) { + REQUIRE(ctx != NULL); + REQUIRE(cipherlist != NULL); + REQUIRE(*cipherlist != '\0'); + + RUNTIME_CHECK(SSL_CTX_set_cipher_list(ctx, cipherlist) == 1); +} + +void +isc_tlsctx_prefer_server_ciphers(isc_tlsctx_t *ctx, const bool prefer) { + REQUIRE(ctx != NULL); + + if (prefer) { + (void)SSL_CTX_set_options(ctx, SSL_OP_CIPHER_SERVER_PREFERENCE); + } else { + (void)SSL_CTX_clear_options(ctx, + SSL_OP_CIPHER_SERVER_PREFERENCE); + } +} + +void +isc_tlsctx_session_tickets(isc_tlsctx_t *ctx, const bool use) { + REQUIRE(ctx != NULL); + + if (!use) { + (void)SSL_CTX_set_options(ctx, SSL_OP_NO_TICKET); + } else { + (void)SSL_CTX_clear_options(ctx, SSL_OP_NO_TICKET); + } +} + +isc_tls_t * +isc_tls_create(isc_tlsctx_t *ctx) { + isc_tls_t *newctx = NULL; + + REQUIRE(ctx != NULL); + + newctx = SSL_new(ctx); + if (newctx == NULL) { + char errbuf[256]; + unsigned long err = ERR_get_error(); + + ERR_error_string_n(err, errbuf, sizeof(errbuf)); + fprintf(stderr, "%s:SSL_new(%p) -> %s\n", __func__, ctx, + errbuf); + } + + return (newctx); +} + +void +isc_tls_free(isc_tls_t **tlsp) { + isc_tls_t *tls = NULL; + REQUIRE(tlsp != NULL && *tlsp != NULL); + + tls = *tlsp; + *tlsp = NULL; + SSL_free(tls); +} + +const char * +isc_tls_verify_peer_result_string(isc_tls_t *tls) { + REQUIRE(tls != NULL); + + return (X509_verify_cert_error_string(SSL_get_verify_result(tls))); +} + +#if HAVE_LIBNGHTTP2 +#ifndef OPENSSL_NO_NEXTPROTONEG +/* + * NPN TLS extension client callback. + */ +static int +select_next_proto_cb(SSL *ssl, unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen, void *arg) { + UNUSED(ssl); + UNUSED(arg); + + if (nghttp2_select_next_protocol(out, outlen, in, inlen) <= 0) { + return (SSL_TLSEXT_ERR_NOACK); + } + return (SSL_TLSEXT_ERR_OK); +} +#endif /* !OPENSSL_NO_NEXTPROTONEG */ + +void +isc_tlsctx_enable_http2client_alpn(isc_tlsctx_t *ctx) { + REQUIRE(ctx != NULL); + +#ifndef OPENSSL_NO_NEXTPROTONEG + SSL_CTX_set_next_proto_select_cb(ctx, select_next_proto_cb, NULL); +#endif /* !OPENSSL_NO_NEXTPROTONEG */ + +#if OPENSSL_VERSION_NUMBER >= 0x10002000L + SSL_CTX_set_alpn_protos(ctx, (const unsigned char *)NGHTTP2_PROTO_ALPN, + NGHTTP2_PROTO_ALPN_LEN); +#endif /* OPENSSL_VERSION_NUMBER >= 0x10002000L */ +} + +#ifndef OPENSSL_NO_NEXTPROTONEG +static int +next_proto_cb(isc_tls_t *ssl, const unsigned char **data, unsigned int *len, + void *arg) { + UNUSED(ssl); + UNUSED(arg); + + *data = (const unsigned char *)NGHTTP2_PROTO_ALPN; + *len = (unsigned int)NGHTTP2_PROTO_ALPN_LEN; + return (SSL_TLSEXT_ERR_OK); +} +#endif /* !OPENSSL_NO_NEXTPROTONEG */ + +#if OPENSSL_VERSION_NUMBER >= 0x10002000L +static int +alpn_select_proto_cb(SSL *ssl, const unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen, void *arg) { + int ret; + + UNUSED(ssl); + UNUSED(arg); + + ret = nghttp2_select_next_protocol((unsigned char **)(uintptr_t)out, + outlen, in, inlen); + + if (ret != 1) { + return (SSL_TLSEXT_ERR_NOACK); + } + + return (SSL_TLSEXT_ERR_OK); +} +#endif /* OPENSSL_VERSION_NUMBER >= 0x10002000L */ + +void +isc_tlsctx_enable_http2server_alpn(isc_tlsctx_t *tls) { + REQUIRE(tls != NULL); + +#ifndef OPENSSL_NO_NEXTPROTONEG + SSL_CTX_set_next_protos_advertised_cb(tls, next_proto_cb, NULL); +#endif // OPENSSL_NO_NEXTPROTONEG +#if OPENSSL_VERSION_NUMBER >= 0x10002000L + SSL_CTX_set_alpn_select_cb(tls, alpn_select_proto_cb, NULL); +#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L +} +#endif /* HAVE_LIBNGHTTP2 */ + +void +isc_tls_get_selected_alpn(isc_tls_t *tls, const unsigned char **alpn, + unsigned int *alpnlen) { + REQUIRE(tls != NULL); + REQUIRE(alpn != NULL); + REQUIRE(alpnlen != NULL); + +#ifndef OPENSSL_NO_NEXTPROTONEG + SSL_get0_next_proto_negotiated(tls, alpn, alpnlen); +#endif +#if OPENSSL_VERSION_NUMBER >= 0x10002000L + if (*alpn == NULL) { + SSL_get0_alpn_selected(tls, alpn, alpnlen); + } +#endif +} + +static bool +protoneg_check_protocol(const uint8_t **pout, uint8_t *pout_len, + const uint8_t *in, size_t in_len, const uint8_t *key, + size_t key_len) { + for (size_t i = 0; i + key_len <= in_len; i += (size_t)(in[i] + 1)) { + if (memcmp(&in[i], key, key_len) == 0) { + *pout = (const uint8_t *)(&in[i + 1]); + *pout_len = in[i]; + return (true); + } + } + return (false); +} + +/* dot prepended by its length (3 bytes) */ +#define DOT_PROTO_ALPN "\x3" ISC_TLS_DOT_PROTO_ALPN_ID +#define DOT_PROTO_ALPN_LEN (sizeof(DOT_PROTO_ALPN) - 1) + +static bool +dot_select_next_protocol(const uint8_t **pout, uint8_t *pout_len, + const uint8_t *in, size_t in_len) { + return (protoneg_check_protocol(pout, pout_len, in, in_len, + (const uint8_t *)DOT_PROTO_ALPN, + DOT_PROTO_ALPN_LEN)); +} + +void +isc_tlsctx_enable_dot_client_alpn(isc_tlsctx_t *ctx) { + REQUIRE(ctx != NULL); + +#if OPENSSL_VERSION_NUMBER >= 0x10002000L + SSL_CTX_set_alpn_protos(ctx, (const uint8_t *)DOT_PROTO_ALPN, + DOT_PROTO_ALPN_LEN); +#endif /* OPENSSL_VERSION_NUMBER >= 0x10002000L */ +} + +#if OPENSSL_VERSION_NUMBER >= 0x10002000L +static int +dot_alpn_select_proto_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + bool ret; + + UNUSED(ssl); + UNUSED(arg); + + ret = dot_select_next_protocol(out, outlen, in, inlen); + + if (!ret) { + return (SSL_TLSEXT_ERR_NOACK); + } + + return (SSL_TLSEXT_ERR_OK); +} +#endif /* OPENSSL_VERSION_NUMBER >= 0x10002000L */ + +void +isc_tlsctx_enable_dot_server_alpn(isc_tlsctx_t *tls) { + REQUIRE(tls != NULL); + +#if OPENSSL_VERSION_NUMBER >= 0x10002000L + SSL_CTX_set_alpn_select_cb(tls, dot_alpn_select_proto_cb, NULL); +#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L +} + +isc_result_t +isc_tlsctx_enable_peer_verification(isc_tlsctx_t *tlsctx, const bool is_server, + isc_tls_cert_store_t *store, + const char *hostname, + bool hostname_ignore_subject) { + int ret = 0; + REQUIRE(tlsctx != NULL); + REQUIRE(store != NULL); + + /* Set the hostname/IP address. */ + if (!is_server && hostname != NULL && *hostname != '\0') { + struct in6_addr sa6; + struct in_addr sa; + X509_VERIFY_PARAM *param = SSL_CTX_get0_param(tlsctx); + unsigned int hostflags = X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS; + + /* It might be an IP address. */ + if (inet_pton(AF_INET6, hostname, &sa6) == 1 || + inet_pton(AF_INET, hostname, &sa) == 1) + { + ret = X509_VERIFY_PARAM_set1_ip_asc(param, hostname); + } else { + /* It seems that it is a host name. Let's set it. */ + ret = X509_VERIFY_PARAM_set1_host(param, hostname, 0); + } + if (ret != 1) { + ERR_clear_error(); + return (ISC_R_FAILURE); + } + +#ifdef X509_CHECK_FLAG_NEVER_CHECK_SUBJECT + /* + * According to the RFC 8310, Section 8.1, Subject field MUST + * NOT be inspected when verifying a hostname when using + * DoT. Only SubjectAltName must be checked instead. That is + * not the case for HTTPS, though. + * + * Unfortunately, some quite old versions of OpenSSL (< 1.1.1) + * might lack the functionality to implement that. It should + * have very little real-world consequences, as most of the + * production-ready certificates issued by real CAs will have + * SubjectAltName set. In such a case, the Subject field is + * ignored. + */ + if (hostname_ignore_subject) { + hostflags |= X509_CHECK_FLAG_NEVER_CHECK_SUBJECT; + } +#else + UNUSED(hostname_ignore_subject); +#endif + X509_VERIFY_PARAM_set_hostflags(param, hostflags); + } + + /* "Attach" the cert store to the context */ + SSL_CTX_set1_cert_store(tlsctx, store); + + /* enable verification */ + if (is_server) { + SSL_CTX_set_verify(tlsctx, + SSL_VERIFY_PEER | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + NULL); + } else { + SSL_CTX_set_verify(tlsctx, SSL_VERIFY_PEER, NULL); + } + + return (ISC_R_SUCCESS); +} + +isc_result_t +isc_tlsctx_load_client_ca_names(isc_tlsctx_t *ctx, const char *ca_bundle_file) { + STACK_OF(X509_NAME) * cert_names; + REQUIRE(ctx != NULL); + REQUIRE(ca_bundle_file != NULL); + + cert_names = SSL_load_client_CA_file(ca_bundle_file); + if (cert_names == NULL) { + ERR_clear_error(); + return (ISC_R_FAILURE); + } + + SSL_CTX_set_client_CA_list(ctx, cert_names); + + return (ISC_R_SUCCESS); +} + +isc_result_t +isc_tls_cert_store_create(const char *ca_bundle_filename, + isc_tls_cert_store_t **pstore) { + int ret = 0; + isc_tls_cert_store_t *store = NULL; + REQUIRE(pstore != NULL && *pstore == NULL); + + store = X509_STORE_new(); + if (store == NULL) { + goto error; + } + + /* Let's treat empty string as the default (system wide) store */ + if (ca_bundle_filename != NULL && *ca_bundle_filename == '\0') { + ca_bundle_filename = NULL; + } + + if (ca_bundle_filename == NULL) { + ret = X509_STORE_set_default_paths(store); + } else { + ret = X509_STORE_load_locations(store, ca_bundle_filename, + NULL); + } + + if (ret == 0) { + goto error; + } + + *pstore = store; + return (ISC_R_SUCCESS); + +error: + ERR_clear_error(); + if (store != NULL) { + X509_STORE_free(store); + } + return (ISC_R_FAILURE); +} + +void +isc_tls_cert_store_free(isc_tls_cert_store_t **pstore) { + isc_tls_cert_store_t *store; + REQUIRE(pstore != NULL && *pstore != NULL); + + store = *pstore; + + X509_STORE_free(store); + + *pstore = NULL; +} + +#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 + * IPv6 in order to avoid cluttering a context-specific + * session-resumption cache. + */ + isc_tlsctx_t *ctx[isc_tlsctx_cache_count - 1][2]; + isc_tlsctx_client_session_cache_t + *client_sess_cache[isc_tlsctx_cache_count - 1][2]; + /* + * One certificate store is enough for all the contexts defined + * above. We need that for peer validation. + */ + isc_tls_cert_store_t *ca_store; +} isc_tlsctx_cache_entry_t; + +struct isc_tlsctx_cache { + uint32_t magic; + isc_refcount_t references; + isc_mem_t *mctx; + + isc_rwlock_t rwlock; + isc_ht_t *data; +}; + +void +isc_tlsctx_cache_create(isc_mem_t *mctx, isc_tlsctx_cache_t **cachep) { + isc_tlsctx_cache_t *nc; + + REQUIRE(cachep != NULL && *cachep == NULL); + nc = isc_mem_get(mctx, sizeof(*nc)); + + *nc = (isc_tlsctx_cache_t){ .magic = TLSCTX_CACHE_MAGIC }; + isc_refcount_init(&nc->references, 1); + isc_mem_attach(mctx, &nc->mctx); + + isc_ht_init(&nc->data, mctx, 5, ISC_HT_CASE_SENSITIVE); + isc_rwlock_init(&nc->rwlock, 0, 0); + + *cachep = nc; +} + +void +isc_tlsctx_cache_attach(isc_tlsctx_cache_t *source, + isc_tlsctx_cache_t **targetp) { + REQUIRE(VALID_TLSCTX_CACHE(source)); + REQUIRE(targetp != NULL && *targetp == NULL); + + isc_refcount_increment(&source->references); + + *targetp = source; +} + +static void +tlsctx_cache_entry_destroy(isc_mem_t *mctx, isc_tlsctx_cache_entry_t *entry) { + size_t i, k; + + for (i = 0; i < (isc_tlsctx_cache_count - 1); i++) { + for (k = 0; k < 2; k++) { + if (entry->ctx[i][k] != NULL) { + isc_tlsctx_free(&entry->ctx[i][k]); + } + + if (entry->client_sess_cache[i][k] != NULL) { + isc_tlsctx_client_session_cache_detach( + &entry->client_sess_cache[i][k]); + } + } + } + if (entry->ca_store != NULL) { + isc_tls_cert_store_free(&entry->ca_store); + } + isc_mem_put(mctx, entry, sizeof(*entry)); +} + +static void +tlsctx_cache_destroy(isc_tlsctx_cache_t *cache) { + isc_ht_iter_t *it = NULL; + isc_result_t result; + + cache->magic = 0; + + isc_refcount_destroy(&cache->references); + + isc_ht_iter_create(cache->data, &it); + for (result = isc_ht_iter_first(it); result == ISC_R_SUCCESS; + result = isc_ht_iter_delcurrent_next(it)) + { + isc_tlsctx_cache_entry_t *entry = NULL; + isc_ht_iter_current(it, (void **)&entry); + tlsctx_cache_entry_destroy(cache->mctx, entry); + } + + isc_ht_iter_destroy(&it); + isc_ht_destroy(&cache->data); + isc_rwlock_destroy(&cache->rwlock); + isc_mem_putanddetach(&cache->mctx, cache, sizeof(*cache)); +} + +void +isc_tlsctx_cache_detach(isc_tlsctx_cache_t **cachep) { + isc_tlsctx_cache_t *cache = NULL; + + REQUIRE(cachep != NULL); + + cache = *cachep; + *cachep = NULL; + + REQUIRE(VALID_TLSCTX_CACHE(cache)); + + if (isc_refcount_decrement(&cache->references) == 1) { + tlsctx_cache_destroy(cache); + } +} + +isc_result_t +isc_tlsctx_cache_add( + isc_tlsctx_cache_t *cache, const char *name, + const isc_tlsctx_cache_transport_t transport, const uint16_t family, + isc_tlsctx_t *ctx, isc_tls_cert_store_t *store, + isc_tlsctx_client_session_cache_t *client_sess_cache, + isc_tlsctx_t **pfound, isc_tls_cert_store_t **pfound_store, + isc_tlsctx_client_session_cache_t **pfound_client_sess_cache) { + isc_result_t result = ISC_R_FAILURE; + size_t name_len, tr_offset; + isc_tlsctx_cache_entry_t *entry = NULL; + bool ipv6; + + REQUIRE(VALID_TLSCTX_CACHE(cache)); + REQUIRE(client_sess_cache == NULL || + VALID_TLSCTX_CLIENT_SESSION_CACHE(client_sess_cache)); + REQUIRE(name != NULL && *name != '\0'); + REQUIRE(transport > isc_tlsctx_cache_none && + transport < isc_tlsctx_cache_count); + REQUIRE(family == AF_INET || family == AF_INET6); + REQUIRE(ctx != NULL); + + tr_offset = (transport - 1); + ipv6 = (family == AF_INET6); + + RWLOCK(&cache->rwlock, isc_rwlocktype_write); + + name_len = strlen(name); + result = isc_ht_find(cache->data, (const uint8_t *)name, name_len, + (void **)&entry); + if (result == ISC_R_SUCCESS && entry->ctx[tr_offset][ipv6] != NULL) { + isc_tlsctx_client_session_cache_t *found_client_sess_cache; + /* The entry exists. */ + if (pfound != NULL) { + INSIST(*pfound == NULL); + *pfound = entry->ctx[tr_offset][ipv6]; + } + + if (pfound_store != NULL && entry->ca_store != NULL) { + INSIST(*pfound_store == NULL); + *pfound_store = entry->ca_store; + } + + found_client_sess_cache = + entry->client_sess_cache[tr_offset][ipv6]; + if (pfound_client_sess_cache != NULL && + found_client_sess_cache != NULL) + { + INSIST(*pfound_client_sess_cache == NULL); + *pfound_client_sess_cache = found_client_sess_cache; + } + result = ISC_R_EXISTS; + } else if (result == ISC_R_SUCCESS && + entry->ctx[tr_offset][ipv6] == NULL) + { + /* + * The hash table entry exists, but is not filled for this + * particular transport/IP type combination. + */ + entry->ctx[tr_offset][ipv6] = ctx; + entry->client_sess_cache[tr_offset][ipv6] = client_sess_cache; + /* + * As the passed certificates store object is supposed + * to be internally managed by the cache object anyway, + * we might destroy the unneeded store object right now. + */ + if (store != NULL && store != entry->ca_store) { + isc_tls_cert_store_free(&store); + } + result = ISC_R_SUCCESS; + } else { + /* + * The hash table entry does not exist, let's create one. + */ + INSIST(result != ISC_R_SUCCESS); + entry = isc_mem_get(cache->mctx, sizeof(*entry)); + /* Oracle/Red Hat Linux, GCC bug #53119 */ + memset(entry, 0, sizeof(*entry)); + entry->ctx[tr_offset][ipv6] = ctx; + entry->client_sess_cache[tr_offset][ipv6] = client_sess_cache; + entry->ca_store = store; + RUNTIME_CHECK(isc_ht_add(cache->data, (const uint8_t *)name, + name_len, + (void *)entry) == ISC_R_SUCCESS); + result = ISC_R_SUCCESS; + } + + RWUNLOCK(&cache->rwlock, isc_rwlocktype_write); + + return (result); +} + +isc_result_t +isc_tlsctx_cache_find( + isc_tlsctx_cache_t *cache, const char *name, + const isc_tlsctx_cache_transport_t transport, const uint16_t family, + isc_tlsctx_t **pctx, isc_tls_cert_store_t **pstore, + isc_tlsctx_client_session_cache_t **pfound_client_sess_cache) { + isc_result_t result = ISC_R_FAILURE; + size_t tr_offset; + isc_tlsctx_cache_entry_t *entry = NULL; + bool ipv6; + + REQUIRE(VALID_TLSCTX_CACHE(cache)); + REQUIRE(name != NULL && *name != '\0'); + REQUIRE(transport > isc_tlsctx_cache_none && + transport < isc_tlsctx_cache_count); + REQUIRE(family == AF_INET || family == AF_INET6); + REQUIRE(pctx != NULL && *pctx == NULL); + + tr_offset = (transport - 1); + ipv6 = (family == AF_INET6); + + RWLOCK(&cache->rwlock, isc_rwlocktype_read); + + result = isc_ht_find(cache->data, (const uint8_t *)name, strlen(name), + (void **)&entry); + + if (result == ISC_R_SUCCESS && pstore != NULL && + entry->ca_store != NULL) + { + *pstore = entry->ca_store; + } + + if (result == ISC_R_SUCCESS && entry->ctx[tr_offset][ipv6] != NULL) { + isc_tlsctx_client_session_cache_t *found_client_sess_cache = + entry->client_sess_cache[tr_offset][ipv6]; + + *pctx = entry->ctx[tr_offset][ipv6]; + + if (pfound_client_sess_cache != NULL && + found_client_sess_cache != NULL) + { + INSIST(*pfound_client_sess_cache == NULL); + *pfound_client_sess_cache = found_client_sess_cache; + } + } else if (result == ISC_R_SUCCESS && + entry->ctx[tr_offset][ipv6] == NULL) + { + result = ISC_R_NOTFOUND; + } else { + INSIST(result != ISC_R_SUCCESS); + } + + RWUNLOCK(&cache->rwlock, isc_rwlocktype_read); + + 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; +}; + +void +isc_tlsctx_client_session_cache_create( + isc_mem_t *mctx, isc_tlsctx_t *ctx, const size_t max_entries, + isc_tlsctx_client_session_cache_t **cachep) { + isc_tlsctx_client_session_cache_t *nc; + + REQUIRE(ctx != NULL); + REQUIRE(max_entries > 0); + REQUIRE(cachep != NULL && *cachep == NULL); + + 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; + + *cachep = 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)); +} + +static bool +ssl_session_seems_resumable(const SSL_SESSION *sess) { +#ifdef HAVE_SSL_SESSION_IS_RESUMABLE + /* + * If SSL_SESSION_is_resumable() is available, let's use that. It + * is expected to be available on OpenSSL >= 1.1.1 and its modern + * siblings. + */ + return (SSL_SESSION_is_resumable(sess) != 0); +#elif (OPENSSL_VERSION_NUMBER >= 0x10100000L) + /* + * Taking into consideration that OpenSSL 1.1.0 uses opaque + * pointers for SSL_SESSION, we cannot implement a replacement for + * SSL_SESSION_is_resumable() manually. Let's use a sensible + * approximation for that, then: if there is an associated session + * ticket or session ID, then, most likely, the session is + * resumable. + */ + unsigned int session_id_len = 0; + (void)SSL_SESSION_get_id(sess, &session_id_len); + return (SSL_SESSION_has_ticket(sess) || session_id_len > 0); +#else + return (!sess->not_resumable && + (sess->session_id_length > 0 || sess->tlsext_ticklen > 0)); +#endif +} + +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) { + ERR_clear_error(); + return; + } else if (!ssl_session_seems_resumable(sess)) { + 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); +} + +void +isc_tlsctx_set_random_session_id_context(isc_tlsctx_t *ctx) { + uint8_t session_id_ctx[SSL_MAX_SID_CTX_LENGTH] = { 0 }; + const size_t len = ISC_MIN(20, sizeof(session_id_ctx)); + + REQUIRE(ctx != NULL); + + RUNTIME_CHECK(RAND_bytes(session_id_ctx, len) == 1); + + RUNTIME_CHECK( + SSL_CTX_set_session_id_context(ctx, session_id_ctx, len) == 1); +} |