diff --git a/README.md b/README.md index 4e7b228..d5ed553 100644 --- a/README.md +++ b/README.md @@ -438,42 +438,68 @@ First, you'll need to make sure you include the SSL header file: #include "hiredis_ssl.h" ``` -SSL can only be enabled on a `redisContext` connection after the connection has -been established and before any command has been processed. For example: +You will also need to link against `libhiredis_ssl`, **in addition** to +`libhiredis` and add `-lssl -lcrypto` to satisfy its dependencies. + +Hiredis implements SSL/TLS on top of its normal `redisContext` or +`redisAsyncContext`, so you will need to establish a connection first and then +initiate an SSL/TLS handshake. + +#### Hiredis OpenSSL Wrappers + +Before Hiredis can negotiate an SSL/TLS connection, it is necessary to +initialize OpenSSL and create a context. You can do that in two ways: + +1. Work directly with the OpenSSL API to initialize the library's global context + and create `SSL_CTX *` and `SSL *` contexts. With an `SSL *` object you can + call `redisInitiateSSL()`. +2. Work with a set of Hiredis-provided wrappers around OpenSSL, create a + `redisSSLContext` object to hold configuration and use + `redisInitiateSSLWithContext()` to initiate the SSL/TLS handshake. ```c +/* An Hiredis SSL context. It holds SSL configuration and can be reused across + * many contexts. + */ +redisSSLContext *ssl; + +/* An error variable to indicate what went wrong, if the context fails to + * initialize. + */ +redisSSLContextError ssl_error; + +/* Initialize global OpenSSL state. + * + * You should call this only once when your app initializes, and only if + * you don't explicitly or implicitly initialize OpenSSL it elsewhere. + */ +redisInitOpenSSL(); + +/* Create SSL context */ +ssl = redisCreateSSLContext( + "cacertbundle.crt", /* File name of trusted CA/ca bundle file, optional */ + "/path/to/certs", /* Path of trusted certificates, optional */ + "client_cert.pem", /* File name of client certificate file, optional */ + "client_key.pem", /* File name of client private key, optional */ + "redis.mydomain.com", /* Server name to request (SNI), optional */ + &ssl_error + ) != REDIS_OK) { + printf("SSL error: %s\n", redisSSLContextGetError(ssl_error); + /* Abort... */ + } + +/* Create Redis context and establish connection */ c = redisConnect("localhost", 6443); if (c == NULL || c->err) { /* Handle error and abort... */ } -if (redisSecureConnection(c, - "cacertbundle.crt", /* File name of trusted CA/ca bundle file */ - "client_cert.pem", /* File name of client certificate file */ - "client_key.pem", /* File name of client private key */ - "redis.mydomain.com" /* Server name to request (SNI) */ - ) != REDIS_OK) { - printf("SSL error: %s\n", c->errstr); - /* Abort... */ +/* Negotiate SSL/TLS */ +if (redisInitiateSSLWithContext(c, ssl) != REDIS_OK) { + /* Handle error, in c->err / c->errstr */ } ``` -You will also need to link against `libhiredis_ssl`, **in addition** to -`libhiredis` and add `-lssl -lcrypto` to satisfy its dependencies. - -### OpenSSL Global State Initialization - -OpenSSL needs to have certain global state initialized before it can be used. -Using `redisSecureConnection()` will handle this automatically on the first -call. - -**If the calling application itself also initializes and uses OpenSSL directly, -`redisSecureConnection()` must not be used.** - -Instead, use `redisInitiateSSL()` which also provides greater control over the -configuration of the SSL connection, as the caller is responsible to create a -connection context using `SSL_new()` and configure it as required. - ## AUTHORS Hiredis was written by Salvatore Sanfilippo (antirez at gmail) and diff --git a/examples/example-libevent-ssl.c b/examples/example-libevent-ssl.c index 1021113..aac5770 100644 --- a/examples/example-libevent-ssl.c +++ b/examples/example-libevent-ssl.c @@ -52,13 +52,25 @@ int main (int argc, char **argv) { const char *certKey = argv[5]; const char *caCert = argc > 5 ? argv[6] : NULL; + redisSSLContext *ssl; + redisSSLContextError ssl_error; + + redisInitOpenSSL(); + + ssl = redisCreateSSLContext(caCert, NULL, + cert, certKey, NULL, &ssl_error); + if (!ssl) { + printf("Error: %s\n", redisSSLContextGetError(ssl_error)); + return 1; + } + redisAsyncContext *c = redisAsyncConnect(hostname, port); if (c->err) { /* Let *c leak for now... */ printf("Error: %s\n", c->errstr); return 1; } - if (redisSecureConnection(&c->c, caCert, cert, certKey, "sni") != REDIS_OK) { + if (redisInitiateSSLWithContext(&c->c, ssl) != REDIS_OK) { printf("SSL Error!\n"); exit(1); } @@ -69,5 +81,7 @@ int main (int argc, char **argv) { redisAsyncCommand(c, NULL, NULL, "SET key %b", value, nvalue); redisAsyncCommand(c, getCallback, (char*)"end-1", "GET key"); event_base_dispatch(base); + + redisFreeSSLContext(ssl); return 0; } diff --git a/examples/example-ssl.c b/examples/example-ssl.c index 81f4648..c676ed8 100644 --- a/examples/example-ssl.c +++ b/examples/example-ssl.c @@ -7,6 +7,8 @@ int main(int argc, char **argv) { unsigned int j; + redisSSLContext *ssl; + redisSSLContextError ssl_error; redisContext *c; redisReply *reply; if (argc < 4) { @@ -19,6 +21,14 @@ int main(int argc, char **argv) { const char *key = argv[4]; const char *ca = argc > 4 ? argv[5] : NULL; + redisInitOpenSSL(); + ssl = redisCreateSSLContext(ca, NULL, cert, key, NULL, &ssl_error); + if (!ssl) { + printf("SSL Context error: %s\n", + redisSSLContextGetError(ssl_error)); + exit(1); + } + struct timeval tv = { 1, 500000 }; // 1.5 seconds redisOptions options = {0}; REDIS_OPTIONS_SET_TCP(&options, hostname, port); @@ -35,7 +45,7 @@ int main(int argc, char **argv) { exit(1); } - if (redisSecureConnection(c, ca, cert, key, "sni") != REDIS_OK) { + if (redisInitiateSSLWithContext(c, ssl) != REDIS_OK) { printf("Couldn't initialize SSL!\n"); printf("Error: %s\n", c->errstr); redisFree(c); @@ -93,5 +103,7 @@ int main(int argc, char **argv) { /* Disconnects and frees the context */ redisFree(c); + redisFreeSSLContext(ssl); + return 0; } diff --git a/hiredis_ssl.h b/hiredis_ssl.h index 21e8580..604efe0 100644 --- a/hiredis_ssl.h +++ b/hiredis_ssl.h @@ -41,15 +41,81 @@ extern "C" { */ struct ssl_st; -/** - * Secure the connection using SSL. This should be done before any command is - * executed on the connection. +/* A wrapper around OpenSSL SSL_CTX to allow easy SSL use without directly + * calling OpenSSL. */ -int redisSecureConnection(redisContext *c, const char *capath, const char *certpath, - const char *keypath, const char *servername); +typedef struct redisSSLContext redisSSLContext; /** - * Initiate SSL/TLS negotiation on a provided context. + * Initialization errors that redisCreateSSLContext() may return. + */ + +typedef enum { + REDIS_SSL_CTX_NONE = 0, /* No Error */ + REDIS_SSL_CTX_CREATE_FAILED, /* Failed to create OpenSSL SSL_CTX */ + REDIS_SSL_CTX_CERT_KEY_REQUIRED, /* Client cert and key must both be specified or skipped */ + REDIS_SSL_CTX_CA_CERT_LOAD_FAILED, /* Failed to load CA Certificate or CA Path */ + REDIS_SSL_CTX_CLIENT_CERT_LOAD_FAILED, /* Failed to load client certificate */ + REDIS_SSL_CTX_PRIVATE_KEY_LOAD_FAILED /* Failed to load private key */ +} redisSSLContextError; + +/** + * Return the error message corresponding with the specified error code. + */ + +const char *redisSSLContextGetError(redisSSLContextError error); + +/** + * Helper function to initialize the OpenSSL library. + * + * OpenSSL requires one-time initialization before it can be used. Callers should + * call this function only once, and only if OpenSSL is not directly initialized + * elsewhere. + */ +int redisInitOpenSSL(void); + +/** + * Helper function to initialize an OpenSSL context that can be used + * to initiate SSL connections. + * + * cacert_filename is an optional name of a CA certificate/bundle file to load + * and use for validation. + * + * capath is an optional directory path where trusted CA certificate files are + * stored in an OpenSSL-compatible structure. + * + * cert_filename and private_key_filename are optional names of a client side + * certificate and private key files to use for authentication. They need to + * be both specified or omitted. + * + * server_name is an optional and will be used as a server name indication + * (SNI) TLS extension. + * + * If error is non-null, it will be populated in case the context creation fails + * (returning a NULL). + */ + +redisSSLContext *redisCreateSSLContext(const char *cacert_filename, const char *capath, + const char *cert_filename, const char *private_key_filename, + const char *server_name, redisSSLContextError *error); + +/** + * Free a previously created OpenSSL context. + */ +void redisFreeSSLContext(redisSSLContext *redis_ssl_ctx); + +/** + * Initiate SSL on an existing redisContext. + * + * This is similar to redisInitiateSSL() but does not require the caller + * to directly interact with OpenSSL, and instead uses a redisSSLContext + * previously created using redisCreateSSLContext(). + */ + +int redisInitiateSSLWithContext(redisContext *c, redisSSLContext *redis_ssl_ctx); + +/** + * Initiate SSL/TLS negotiation on a provided OpenSSL SSL object. */ int redisInitiateSSL(redisContext *c, struct ssl_st *ssl); diff --git a/ssl.c b/ssl.c index 8cb133a..f25dce2 100644 --- a/ssl.c +++ b/ssl.c @@ -47,17 +47,20 @@ #include "win32.h" #include "async_private.h" +#include "hiredis_ssl.h" void __redisSetError(redisContext *c, int type, const char *str); -/* The SSL context is attached to SSL/TLS connections as a privdata. */ -typedef struct redisSSLContext { - /** - * OpenSSL SSL_CTX; It is optional and will not be set when using - * user-supplied SSL. - */ +struct redisSSLContext { + /* Associated OpenSSL SSL_CTX as created by redisCreateSSLContext() */ SSL_CTX *ssl_ctx; + /* Requested SNI, or NULL */ + char *server_name; +}; + +/* The SSL connection context is attached to SSL/TLS connections as a privdata. */ +typedef struct redisSSL { /** * OpenSSL SSL object. */ @@ -77,43 +80,11 @@ typedef struct redisSSLContext { * should resume whenever a read takes place, if possible */ int pendingWrite; -} redisSSLContext; +} redisSSL; /* Forward declaration */ redisContextFuncs redisContextSSLFuncs; -#ifdef HIREDIS_SSL_TRACE -/** - * Callback used for debugging - */ -static void sslLogCallback(const SSL *ssl, int where, int ret) { - const char *retstr; - int should_log = 0; - /* Ignore low-level SSL stuff */ - - if (where & SSL_CB_ALERT) { - should_log = 1; - } - if (where == SSL_CB_HANDSHAKE_START || where == SSL_CB_HANDSHAKE_DONE) { - should_log = 1; - } - if ((where & SSL_CB_EXIT) && ret == 0) { - should_log = 1; - } - - if (!should_log) { - return; - } - - retstr = SSL_alert_type_string(ret); - printf("ST(0x%x). %s. R(0x%x)%s\n", where, SSL_state_string_long(ssl), ret, retstr); - - if (where == SSL_CB_HANDSHAKE_DONE) { - printf("Using SSL version %s. Cipher=%s\n", SSL_get_version(ssl), SSL_get_cipher_name(ssl)); - } -} -#endif - /** * OpenSSL global initialization and locking handling callbacks. * Note that this is only required for OpenSSL < 1.1.0. @@ -182,26 +153,130 @@ static int initOpensslLocks(void) { } #endif /* HIREDIS_USE_CRYPTO_LOCKS */ +int redisInitOpenSSL(void) +{ + SSL_library_init(); +#ifdef HIREDIS_USE_CRYPTO_LOCKS + initOpensslLocks(); +#endif + + return REDIS_OK; +} + +/** + * redisSSLContext helper context destruction. + */ + +const char *redisSSLContextGetError(redisSSLContextError error) +{ + switch (error) { + case REDIS_SSL_CTX_NONE: + return "No Error"; + case REDIS_SSL_CTX_CREATE_FAILED: + return "Failed to create OpenSSL SSL_CTX"; + case REDIS_SSL_CTX_CERT_KEY_REQUIRED: + return "Client cert and key must both be specified or skipped"; + case REDIS_SSL_CTX_CA_CERT_LOAD_FAILED: + return "Failed to load CA Certificate or CA Path"; + case REDIS_SSL_CTX_CLIENT_CERT_LOAD_FAILED: + return "Failed to load client certificate"; + case REDIS_SSL_CTX_PRIVATE_KEY_LOAD_FAILED: + return "Failed to load private key"; + default: + return "Unknown error code"; + } +} + +void redisFreeSSLContext(redisSSLContext *ctx) +{ + if (!ctx) + return; + + if (ctx->server_name) { + hi_free(ctx->server_name); + ctx->server_name = NULL; + } + + if (ctx->ssl_ctx) { + SSL_CTX_free(ctx->ssl_ctx); + ctx->ssl_ctx = NULL; + } + + hi_free(ctx); +} + + +/** + * redisSSLContext helper context initialization. + */ + +redisSSLContext *redisCreateSSLContext(const char *cacert_filename, const char *capath, + const char *cert_filename, const char *private_key_filename, + const char *server_name, redisSSLContextError *error) +{ + redisSSLContext *ctx = hi_calloc(1, sizeof(redisSSLContext)); + + ctx->ssl_ctx = SSL_CTX_new(SSLv23_client_method()); + if (!ctx->ssl_ctx) { + if (error) *error = REDIS_SSL_CTX_CREATE_FAILED; + goto error; + } + + SSL_CTX_set_options(ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); + SSL_CTX_set_verify(ctx->ssl_ctx, SSL_VERIFY_PEER, NULL); + + if ((cert_filename != NULL && private_key_filename == NULL) || + (private_key_filename != NULL && cert_filename == NULL)) { + if (error) *error = REDIS_SSL_CTX_CERT_KEY_REQUIRED; + goto error; + } + + if (capath || cacert_filename) { + if (!SSL_CTX_load_verify_locations(ctx->ssl_ctx, cacert_filename, capath)) { + if (error) *error = REDIS_SSL_CTX_CA_CERT_LOAD_FAILED; + goto error; + } + } + + if (cert_filename) { + if (!SSL_CTX_use_certificate_chain_file(ctx->ssl_ctx, cert_filename)) { + if (error) *error = REDIS_SSL_CTX_CLIENT_CERT_LOAD_FAILED; + goto error; + } + if (!SSL_CTX_use_PrivateKey_file(ctx->ssl_ctx, private_key_filename, SSL_FILETYPE_PEM)) { + if (error) *error = REDIS_SSL_CTX_PRIVATE_KEY_LOAD_FAILED; + goto error; + } + } + + if (server_name) + ctx->server_name = hi_strdup(server_name); + + return ctx; + +error: + redisFreeSSLContext(ctx); + return NULL; +} + /** * SSL Connection initialization. */ -static int redisSSLConnect(redisContext *c, SSL_CTX *ssl_ctx, SSL *ssl) { - redisSSLContext *rssl; +static int redisSSLConnect(redisContext *c, SSL *ssl) { if (c->privdata) { __redisSetError(c, REDIS_ERR_OTHER, "redisContext was already associated"); return REDIS_ERR; } - rssl = hi_calloc(1, sizeof(redisSSLContext)); + redisSSL *rssl = hi_calloc(1, sizeof(redisSSL)); if (rssl == NULL) { __redisSetError(c, REDIS_ERR_OOM, "Out of memory"); return REDIS_ERR; } c->funcs = &redisContextSSLFuncs; - rssl->ssl_ctx = ssl_ctx; rssl->ssl = ssl; SSL_set_mode(rssl->ssl, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); @@ -238,84 +313,53 @@ static int redisSSLConnect(redisContext *c, SSL_CTX *ssl_ctx, SSL *ssl) { return REDIS_ERR; } +/** + * A wrapper around redisSSLConnect() for users who manage their own context and + * create their own SSL object. + */ + int redisInitiateSSL(redisContext *c, SSL *ssl) { - return redisSSLConnect(c, NULL, ssl); + return redisSSLConnect(c, ssl); } -int redisSecureConnection(redisContext *c, const char *capath, - const char *certpath, const char *keypath, const char *servername) { +/** + * A wrapper around redisSSLConnect() for users who use redisSSLContext and don't + * manage their own SSL objects. + */ - SSL_CTX *ssl_ctx = NULL; - SSL *ssl = NULL; +int redisInitiateSSLWithContext(redisContext *c, redisSSLContext *redis_ssl_ctx) +{ + if (!c || !redis_ssl_ctx) + return REDIS_ERR; - /* Initialize global OpenSSL stuff */ - static int isInit = 0; - if (!isInit) { - isInit = 1; - SSL_library_init(); -#ifdef HIREDIS_USE_CRYPTO_LOCKS - if (initOpensslLocks() == REDIS_ERR) { - __redisSetError(c, REDIS_ERR_OOM, "Out of memory"); - goto error; - } -#endif - } + /* We want to verify that redisSSLConnect() won't fail on this, as it will + * not own the SSL object in that case and we'll end up leaking. + */ + if (c->privdata) + return REDIS_ERR; - ssl_ctx = SSL_CTX_new(SSLv23_client_method()); - if (!ssl_ctx) { - __redisSetError(c, REDIS_ERR_OTHER, "Failed to create SSL_CTX"); - goto error; - } - -#ifdef HIREDIS_SSL_TRACE - SSL_CTX_set_info_callback(ssl_ctx, sslLogCallback); -#endif - SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); - SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL); - if ((certpath != NULL && keypath == NULL) || (keypath != NULL && certpath == NULL)) { - __redisSetError(c, REDIS_ERR_OTHER, "certpath and keypath must be specified together"); - goto error; - } - - if (capath) { - if (!SSL_CTX_load_verify_locations(ssl_ctx, capath, NULL)) { - __redisSetError(c, REDIS_ERR_OTHER, "Invalid CA certificate"); - goto error; - } - } - if (certpath) { - if (!SSL_CTX_use_certificate_chain_file(ssl_ctx, certpath)) { - __redisSetError(c, REDIS_ERR_OTHER, "Invalid client certificate"); - goto error; - } - if (!SSL_CTX_use_PrivateKey_file(ssl_ctx, keypath, SSL_FILETYPE_PEM)) { - __redisSetError(c, REDIS_ERR_OTHER, "Invalid client key"); - goto error; - } - } - - ssl = SSL_new(ssl_ctx); + SSL *ssl = SSL_new(redis_ssl_ctx->ssl_ctx); if (!ssl) { __redisSetError(c, REDIS_ERR_OTHER, "Couldn't create new SSL instance"); goto error; } - if (servername) { - if (!SSL_set_tlsext_host_name(ssl, servername)) { - __redisSetError(c, REDIS_ERR_OTHER, "Couldn't set server name indication"); + + if (redis_ssl_ctx->server_name) { + if (!SSL_set_tlsext_host_name(ssl, redis_ssl_ctx->server_name)) { + __redisSetError(c, REDIS_ERR_OTHER, "Failed to set server_name/SNI"); goto error; } } - if (redisSSLConnect(c, ssl_ctx, ssl) == REDIS_OK) - return REDIS_OK; + return redisSSLConnect(c, ssl); error: - if (ssl) SSL_free(ssl); - if (ssl_ctx) SSL_CTX_free(ssl_ctx); + if (ssl) + SSL_free(ssl); return REDIS_ERR; } -static int maybeCheckWant(redisSSLContext *rssl, int rv) { +static int maybeCheckWant(redisSSL *rssl, int rv) { /** * If the error is WANT_READ or WANT_WRITE, the appropriate flags are set * and true is returned. False is returned otherwise @@ -335,23 +379,19 @@ static int maybeCheckWant(redisSSLContext *rssl, int rv) { * Implementation of redisContextFuncs for SSL connections. */ -static void redisSSLFreeContext(void *privdata){ - redisSSLContext *rsc = privdata; +static void redisSSLFree(void *privdata){ + redisSSL *rsc = privdata; if (!rsc) return; if (rsc->ssl) { SSL_free(rsc->ssl); rsc->ssl = NULL; } - if (rsc->ssl_ctx) { - SSL_CTX_free(rsc->ssl_ctx); - rsc->ssl_ctx = NULL; - } hi_free(rsc); } static int redisSSLRead(redisContext *c, char *buf, size_t bufcap) { - redisSSLContext *rssl = c->privdata; + redisSSL *rssl = c->privdata; int nread = SSL_read(rssl->ssl, buf, bufcap); if (nread > 0) { @@ -393,7 +433,7 @@ static int redisSSLRead(redisContext *c, char *buf, size_t bufcap) { } static int redisSSLWrite(redisContext *c) { - redisSSLContext *rssl = c->privdata; + redisSSL *rssl = c->privdata; size_t len = rssl->lastLen ? rssl->lastLen : sdslen(c->obuf); int rv = SSL_write(rssl->ssl, c->obuf, len); @@ -416,7 +456,7 @@ static int redisSSLWrite(redisContext *c) { static void redisSSLAsyncRead(redisAsyncContext *ac) { int rv; - redisSSLContext *rssl = ac->c.privdata; + redisSSL *rssl = ac->c.privdata; redisContext *c = &ac->c; rssl->wantRead = 0; @@ -446,7 +486,7 @@ static void redisSSLAsyncRead(redisAsyncContext *ac) { static void redisSSLAsyncWrite(redisAsyncContext *ac) { int rv, done = 0; - redisSSLContext *rssl = ac->c.privdata; + redisSSL *rssl = ac->c.privdata; redisContext *c = &ac->c; rssl->pendingWrite = 0; @@ -475,7 +515,7 @@ static void redisSSLAsyncWrite(redisAsyncContext *ac) { } redisContextFuncs redisContextSSLFuncs = { - .free_privdata = redisSSLFreeContext, + .free_privdata = redisSSLFree, .async_read = redisSSLAsyncRead, .async_write = redisSSLAsyncWrite, .read = redisSSLRead, diff --git a/test.c b/test.c index 48d36d0..fba8eba 100644 --- a/test.c +++ b/test.c @@ -48,6 +48,10 @@ struct config { } ssl; }; +#ifdef HIREDIS_TEST_SSL +redisSSLContext *_ssl_ctx = NULL; +#endif + /* The following lines make up our testing "framework" :) */ static int tests = 0, fails = 0, skips = 0; #define test(_s) { printf("#%02d ", ++tests); printf(_s); } @@ -113,9 +117,9 @@ static int disconnect(redisContext *c, int keep_fd) { return -1; } -static void do_ssl_handshake(redisContext *c, struct config config) { +static void do_ssl_handshake(redisContext *c) { #ifdef HIREDIS_TEST_SSL - redisSecureConnection(c, config.ssl.ca_cert, config.ssl.cert, config.ssl.key, NULL); + redisInitiateSSLWithContext(c, _ssl_ctx); if (c->err) { printf("SSL error: %s\n", c->errstr); redisFree(c); @@ -123,7 +127,6 @@ static void do_ssl_handshake(redisContext *c, struct config config) { } #else (void) c; - (void) config; #endif } @@ -158,7 +161,7 @@ static redisContext *do_connect(struct config config) { } if (config.type == CONN_SSL) { - do_ssl_handshake(c, config); + do_ssl_handshake(c); } return select_database(c); @@ -168,7 +171,7 @@ static void do_reconnect(redisContext *c, struct config config) { redisReconnect(c); if (config.type == CONN_SSL) { - do_ssl_handshake(c, config); + do_ssl_handshake(c); } } @@ -1147,6 +1150,11 @@ int main(int argc, char **argv) { #ifdef HIREDIS_TEST_SSL if (cfg.ssl.port && cfg.ssl.host) { + + redisInitOpenSSL(); + _ssl_ctx = redisCreateSSLContext(cfg.ssl.ca_cert, NULL, cfg.ssl.cert, cfg.ssl.key, NULL, NULL); + assert(_ssl_ctx != NULL); + printf("\nTesting against SSL connection (%s:%d):\n", cfg.ssl.host, cfg.ssl.port); cfg.type = CONN_SSL; @@ -1156,6 +1164,9 @@ int main(int argc, char **argv) { test_invalid_timeout_errors(cfg); test_append_formatted_commands(cfg); if (throughput) test_throughput(cfg); + + redisFreeSSLContext(_ssl_ctx); + _ssl_ctx = NULL; } #endif